From 7f4c6955a6e2968e8a6b6f734457d5c19f14f3b5 Mon Sep 17 00:00:00 2001 From: Vladimir Espinola Date: Thu, 4 Jul 2024 12:15:23 -0400 Subject: [PATCH 1/3] created class to hide creative management complexity --- Sources/ATTNInfoEvent.swift | 4 +- Sources/ATTNWebViewHandling.swift | 216 ++++++++++++++++++++ Sources/Public/SDK/ATTNSDK.swift | 19 +- attentive-ios-sdk.xcodeproj/project.pbxproj | 4 + 4 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 Sources/ATTNWebViewHandling.swift diff --git a/Sources/ATTNInfoEvent.swift b/Sources/ATTNInfoEvent.swift index 45aca0e..0ad9683 100644 --- a/Sources/ATTNInfoEvent.swift +++ b/Sources/ATTNInfoEvent.swift @@ -7,6 +7,6 @@ import Foundation -public final class ATTNInfoEvent: NSObject, ATTNEvent { - public override init() {} +final class ATTNInfoEvent: NSObject, ATTNEvent { + override init() {} } diff --git a/Sources/ATTNWebViewHandling.swift b/Sources/ATTNWebViewHandling.swift new file mode 100644 index 0000000..8381763 --- /dev/null +++ b/Sources/ATTNWebViewHandling.swift @@ -0,0 +1,216 @@ +// +// ATTNWebViewHandling.swift +// attentive-ios-sdk-framework +// +// Created by Vladimir - Work on 2024-07-04. +// + +import Foundation +import WebKit + +protocol ATTNWebViewHandling { + func launchCreative(parentView view: UIView, creativeId: String?, handler: ATTNCreativeTriggerCompletionHandler?) + func closeCreative() +} + +final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { + private enum Constants { + static var visibilityEvent: String { "document-visibility:" } + static var scriptMessageHandlerName: String { "log" } + } + + private enum ScriptStatus { + case success + case timeout + case unknown(String) + + static func getRawValue(from value: Any) -> ScriptStatus? { + guard let stringValue = value as? String else { return nil } + switch stringValue { + case "SUCCESS": + return .success + case "TIMED OUT": + return .timeout + default: + return .unknown(stringValue) + } + } + } + + private weak var sdk: ATTNSDK? + private var urlBuilder: ATTNCreativeUrlProviding + + init(sdk: ATTNSDK, creativeUrlBuilder: ATTNCreativeUrlProviding) { + self.sdk = sdk + self.urlBuilder = creativeUrlBuilder + } + + func launchCreative( + parentView view: UIView, + creativeId: String? = nil, + handler: ATTNCreativeTriggerCompletionHandler? = nil + ) { + guard let sdk = sdk else { + Loggers.creative.debug("Not showing the Attentive creative because the iOS version is too old.") + sdk?.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + return + } + + sdk.parentView = view + sdk.triggerHandler = handler + + let domain = sdk.getDomain() + let mode = sdk.getMode() + let userIdentity = sdk.userIdentity + + Loggers.creative.debug("Called showWebView in creativeSDK with domain: \(domain, privacy: .public)") + + guard !ATTNSDK.isCreativeOpen else { + Loggers.creative.debug("Attempted to trigger creative, but creative is currently open. Taking no action") + return + } + + Loggers.creative.debug("The iOS version is new enough, continuing to show the Attentive creative.") + + let creativePageUrl = urlBuilder.buildCompanyCreativeUrl( + configuration: ATTNCreativeUrlConfig( + domain: domain, + creativeId: creativeId, + skipFatigue: sdk.skipFatigueOnCreative, + mode: mode.rawValue, + userIdentity: userIdentity + ) + ) + + Loggers.creative.debug("Requesting creative page url: \(creativePageUrl)" ) + + guard let url = URL(string: creativePageUrl) else { + Loggers.creative.debug("URL could not be created.") + return + } + + let request = URLRequest(url: url) + + let configuration = WKWebViewConfiguration() + configuration.userContentController.add(self, name: Constants.scriptMessageHandlerName) + + let userScriptWithEventListener = String(format: "window.addEventListener('message', (event) => {if (event.data && event.data.__attentive) {window.webkit.messageHandlers.log.postMessage(event.data.__attentive.action);}}, false);window.addEventListener('visibilitychange', (event) => {window.webkit.messageHandlers.log.postMessage(`%@ ${document.hidden}`);}, false);", Constants.visibilityEvent) + let userScript = WKUserScript(source: userScriptWithEventListener, injectionTime: .atDocumentStart, forMainFrameOnly: false) + configuration.userContentController.addUserScript(userScript) + + sdk.webView = WKWebView(frame: view.frame, configuration: configuration) + + guard let webView = sdk.webView else { return } + + webView.navigationDelegate = self + webView.load(request) + + if mode == .debug { + sdk.parentView?.addSubview(webView) + } else { + webView.isOpaque = false + webView.backgroundColor = .clear + } + } + + func closeCreative() { + sdk?.removeWebView() + ATTNSDK.isCreativeOpen = false + sdk?.triggerHandler?(ATTNCreativeTriggerStatus.closed) + Loggers.creative.debug("Successfully closed creative") + } +} + +extension ATTNWebViewHandler: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard #available(iOS 14.0, *) else { return } + let asyncJs = + """ + var p = new Promise(resolve => { + var timeoutHandle = null; + const interval = setInterval(function() { + e = document.querySelector('iframe'); + if(e && e.id === 'attentive_creative') { + clearInterval(interval); + resolve('SUCCESS'); + if (timeoutHandle != null) { + clearTimeout(timeoutHandle); + } + } + }, 100); + timeoutHandle = setTimeout(function() { + clearInterval(interval); + resolve('TIMED OUT'); + }, 5000); + }); + var status = await p; + return status; + """ + webView.callAsyncJavaScript( + asyncJs, + in: nil, + in: .defaultClient + ) { [weak self] result in + guard let self = self, let sdk = self.sdk else { return } + guard case let .success(statusAny) = result else { + Loggers.creative.debug("No status returned from JS. Not showing WebView.") + self.sdk?.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + return + } + + switch ScriptStatus.getRawValue(from: statusAny) { + case .success: + Loggers.creative.debug("Found creative iframe, showing WebView.") + if sdk.getMode() == .production { + sdk.parentView?.addSubview(webView) + } + sdk.triggerHandler?(ATTNCreativeTriggerStatus.opened) + case .timeout: + Loggers.creative.error("Creative timed out. Not showing WebView.") + sdk.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + case .unknown(let statusString): + Loggers.creative.error("Received unknown status: \(statusString). Not showing WebView") + sdk.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + default: break + } + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + decisionHandler(.cancel) + return + } + + if url.scheme == "sms" { + UIApplication.shared.open(url) + decisionHandler(.cancel) + } else if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" { + if navigationAction.targetFrame == nil { + UIApplication.shared.open(url) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } +} + +extension ATTNWebViewHandler: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + let messageBody = message.body as? String ?? "" + Loggers.creative.debug("Web event message: \(messageBody). isCreativeOpen: \(ATTNSDK.isCreativeOpen ? "YES" : "NO")") + + if messageBody == "CLOSE" { + closeCreative() + } else if messageBody == "IMPRESSION" { + Loggers.creative.debug("Creative opened and generated impression event") + ATTNSDK.isCreativeOpen = true + } else if messageBody == String(format: "%@ true", Constants.visibilityEvent), ATTNSDK.isCreativeOpen { + Loggers.creative.debug("Nav away from creative, closing") + closeCreative() + } + } +} diff --git a/Sources/Public/SDK/ATTNSDK.swift b/Sources/Public/SDK/ATTNSDK.swift index 03192b3..814ad85 100644 --- a/Sources/Public/SDK/ATTNSDK.swift +++ b/Sources/Public/SDK/ATTNSDK.swift @@ -37,13 +37,13 @@ public final class ATTNSDK: NSObject { } // MARK: Static Properties - private static var isCreativeOpen = false + static var isCreativeOpen = false // MARK: Instance Properties - private var parentView: UIView? - private var webView: WKWebView? - private var triggerHandler: ATTNCreativeTriggerCompletionHandler? - + var parentView: UIView? + var triggerHandler: ATTNCreativeTriggerCompletionHandler? + var webView: WKWebView? + private(set) var api: ATTNAPIProtocol private(set) var userIdentity: ATTNUserIdentity @@ -316,4 +316,13 @@ extension ATTNSDK { func getDomain() -> String { domain } + + func getMode() -> ATTNSDKMode { + mode + } + + func removeWebView() { + webView?.removeFromSuperview() + webView = nil + } } diff --git a/attentive-ios-sdk.xcodeproj/project.pbxproj b/attentive-ios-sdk.xcodeproj/project.pbxproj index c305064..68fbfc8 100644 --- a/attentive-ios-sdk.xcodeproj/project.pbxproj +++ b/attentive-ios-sdk.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ FB35C1972C0E52F3009FA048 /* ATTNProductViewEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB35C1962C0E52F3009FA048 /* ATTNProductViewEvent+Extension.swift */; }; FB35C1992C0E5365009FA048 /* ATTNInfoEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB35C1982C0E5365009FA048 /* ATTNInfoEvent+Extension.swift */; }; FB35C19B2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB35C19A2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift */; }; + FB4E3FE52C36F7BF004B8FF0 /* ATTNWebViewHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */; }; FB56D4DA2C208BAD00AF7530 /* ATTNSDK+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4D92C208BAD00AF7530 /* ATTNSDK+Extension.swift */; }; FB56D4DC2C208D6100AF7530 /* Boolean+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4DB2C208D6100AF7530 /* Boolean+Extension.swift */; }; FB56D4DE2C208DC100AF7530 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4DD2C208DC100AF7530 /* String+Extension.swift */; }; @@ -144,6 +145,7 @@ FB35C1962C0E52F3009FA048 /* ATTNProductViewEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNProductViewEvent+Extension.swift"; sourceTree = ""; }; FB35C1982C0E5365009FA048 /* ATTNInfoEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNInfoEvent+Extension.swift"; sourceTree = ""; }; FB35C19A2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNCustomEvent+Extension.swift"; sourceTree = ""; }; + FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATTNWebViewHandling.swift; sourceTree = ""; }; FB56D4D92C208BAD00AF7530 /* ATTNSDK+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNSDK+Extension.swift"; sourceTree = ""; }; FB56D4DB2C208D6100AF7530 /* Boolean+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Boolean+Extension.swift"; sourceTree = ""; }; FB56D4DD2C208DC100AF7530 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; @@ -471,6 +473,7 @@ FBA9F9EE2C0A77AB00C65024 /* ATTNUserAgentBuilder.swift */, FBA9F9EF2C0A77AB00C65024 /* ATTNVisitorService.swift */, FB35C17C2C0E039E009FA048 /* ATTNConstants.swift */, + FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */, FBA9F9FF2C0A77AB00C65024 /* Public */, FBA9FA022C0A77AB00C65024 /* Resources */, ); @@ -636,6 +639,7 @@ FBA9FA122C0A77AB00C65024 /* ATTNCustomEvent.swift in Sources */, FB35C1992C0E5365009FA048 /* ATTNInfoEvent+Extension.swift in Sources */, FB35C1892C0E3AF8009FA048 /* ATTNExternalVendorTypes.swift in Sources */, + FB4E3FE52C36F7BF004B8FF0 /* ATTNWebViewHandling.swift in Sources */, FB080C7F2C1CC9E500834BAA /* ATTNCreativeUrlConfig.swift in Sources */, FBA9FA092C0A77AB00C65024 /* ATTNAppInfo.swift in Sources */, FB35C1792C0E030E009FA048 /* ATTNCreativeTriggerStatus.swift in Sources */, From e905125fbfb9c8d7f9bf91fd72417d59d7810cae Mon Sep 17 00:00:00 2001 From: Vladimir Espinola Date: Thu, 4 Jul 2024 14:23:48 -0400 Subject: [PATCH 2/3] removed delegates from SDK --- Sources/ATTNWebViewHandling.swift | 2 +- Sources/Public/SDK/ATTNSDK.swift | 202 ++---------------------------- 2 files changed, 9 insertions(+), 195 deletions(-) diff --git a/Sources/ATTNWebViewHandling.swift b/Sources/ATTNWebViewHandling.swift index 8381763..bdf75af 100644 --- a/Sources/ATTNWebViewHandling.swift +++ b/Sources/ATTNWebViewHandling.swift @@ -40,7 +40,7 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { private weak var sdk: ATTNSDK? private var urlBuilder: ATTNCreativeUrlProviding - init(sdk: ATTNSDK, creativeUrlBuilder: ATTNCreativeUrlProviding) { + init(sdk: ATTNSDK, creativeUrlBuilder: ATTNCreativeUrlProviding = ATTNCreativeUrlProvider()) { self.sdk = sdk self.urlBuilder = creativeUrlBuilder } diff --git a/Sources/Public/SDK/ATTNSDK.swift b/Sources/Public/SDK/ATTNSDK.swift index 814ad85..d15ba85 100644 --- a/Sources/Public/SDK/ATTNSDK.swift +++ b/Sources/Public/SDK/ATTNSDK.swift @@ -12,30 +12,6 @@ public typealias ATTNCreativeTriggerCompletionHandler = (String) -> Void @objc(ATTNSDK) public final class ATTNSDK: NSObject { - // MARK: Constants - private enum Constants { - static var visibilityEvent: String { "document-visibility:" } - static var scriptMessageHandlerName: String { "log" } - } - - private enum ScriptStatus { - case success - case timeout - case unknown(String) - - static func getRawValue(from value: Any) -> ScriptStatus? { - guard let stringValue = value as? String else { return nil } - switch stringValue { - case "SUCCESS": - return .success - case "TIMED OUT": - return .timeout - default: - return .unknown(stringValue) - } - } - } - // MARK: Static Properties static var isCreativeOpen = false @@ -43,13 +19,13 @@ public final class ATTNSDK: NSObject { var parentView: UIView? var triggerHandler: ATTNCreativeTriggerCompletionHandler? var webView: WKWebView? - + private(set) var api: ATTNAPIProtocol private(set) var userIdentity: ATTNUserIdentity private var domain: String private var mode: ATTNSDKMode - private var urlBuilder: ATTNCreativeUrlProviding = ATTNCreativeUrlProvider() + private var webViewHandler: ATTNWebViewHandling? /// Determinates if fatigue rules evaluation will be skipped for Creative. Default value is false. @objc public var skipFatigueOnCreative: Bool = false @@ -65,6 +41,7 @@ public final class ATTNSDK: NSObject { super.init() + self.webViewHandler = ATTNWebViewHandler(sdk: self) self.sendInfoEvent() self.initializeSkipFatigueOnCreatives() } @@ -131,171 +108,12 @@ fileprivate extension ATTNSDK { api.send(event: ATTNInfoEvent(), userIdentity: userIdentity) } - func closeCreative() { - webView?.removeFromSuperview() - webView = nil - ATTNSDK.isCreativeOpen = false - triggerHandler?(ATTNCreativeTriggerStatus.closed) - Loggers.creative.debug("Successfully closed creative") - } - func launchCreative( parentView view: UIView, creativeId: String? = nil, handler: ATTNCreativeTriggerCompletionHandler? = nil ) { - parentView = view - triggerHandler = handler - - Loggers.creative.debug("Called showWebView in creativeSDK with domain: \(self.domain, privacy: .public)") - - guard !ATTNSDK.isCreativeOpen else { - Loggers.creative.debug("Attempted to trigger creative, but creative is currently open. Taking no action") - return - } - - guard #available(iOS 14, *) else { - Loggers.creative.debug("Not showing the Attentive creative because the iOS version is too old.") - triggerHandler?(ATTNCreativeTriggerStatus.notOpened) - return - } - Loggers.creative.debug("The iOS version is new enough, continuing to show the Attentive creative.") - - let creativePageUrl = urlBuilder.buildCompanyCreativeUrl( - configuration: ATTNCreativeUrlConfig( - domain: domain, - creativeId: creativeId, - skipFatigue: skipFatigueOnCreative, - mode: mode.rawValue, - userIdentity: userIdentity - ) - ) - - Loggers.creative.debug("Requesting creative page url: \(creativePageUrl)" ) - - guard let url = URL(string: creativePageUrl) else { - Loggers.creative.debug("URL could not be created.") - return - } - - let request = URLRequest(url: url) - - let configuration = WKWebViewConfiguration() - configuration.userContentController.add(self, name: Constants.scriptMessageHandlerName) - - let userScriptWithEventListener = String(format: "window.addEventListener('message', (event) => {if (event.data && event.data.__attentive) {window.webkit.messageHandlers.log.postMessage(event.data.__attentive.action);}}, false);window.addEventListener('visibilitychange', (event) => {window.webkit.messageHandlers.log.postMessage(`%@ ${document.hidden}`);}, false);", Constants.visibilityEvent) - let userScript = WKUserScript(source: userScriptWithEventListener, injectionTime: .atDocumentStart, forMainFrameOnly: false) - configuration.userContentController.addUserScript(userScript) - - webView = WKWebView(frame: view.frame, configuration: configuration) - - guard let webView = webView else { return } - - webView.navigationDelegate = self - webView.load(request) - - if mode == .debug { - parentView?.addSubview(webView) - } else { - webView.isOpaque = false - webView.backgroundColor = .clear - } - } -} - -// MARK: WKScriptMessageHandler -extension ATTNSDK: WKScriptMessageHandler { - public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - let messageBody = message.body as? String ?? "" - Loggers.creative.debug("Web event message: \(messageBody). isCreativeOpen: \(ATTNSDK.isCreativeOpen ? "YES" : "NO")") - - if messageBody == "CLOSE" { - closeCreative() - } else if messageBody == "IMPRESSION" { - Loggers.creative.debug("Creative opened and generated impression event") - ATTNSDK.isCreativeOpen = true - } else if messageBody == String(format: "%@ true", Constants.visibilityEvent), ATTNSDK.isCreativeOpen { - Loggers.creative.debug("Nav away from creative, closing") - closeCreative() - } - } -} - -// MARK: WKNavigationDelegate -extension ATTNSDK: WKNavigationDelegate { - public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - guard #available(iOS 14.0, *) else { return } - let asyncJs = - """ - var p = new Promise(resolve => { - var timeoutHandle = null; - const interval = setInterval(function() { - e = document.querySelector('iframe'); - if(e && e.id === 'attentive_creative') { - clearInterval(interval); - resolve('SUCCESS'); - if (timeoutHandle != null) { - clearTimeout(timeoutHandle); - } - } - }, 100); - timeoutHandle = setTimeout(function() { - clearInterval(interval); - resolve('TIMED OUT'); - }, 5000); - }); - var status = await p; - return status; - """ - webView.callAsyncJavaScript( - asyncJs, - in: nil, - in: .defaultClient - ) { [weak self] result in - guard let self = self else { return } - guard case let .success(statusAny) = result else { - Loggers.creative.debug("No status returned from JS. Not showing WebView.") - self.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) - return - } - - switch ScriptStatus.getRawValue(from: statusAny) { - case .success: - Loggers.creative.debug("Found creative iframe, showing WebView.") - if self.mode == .production { - self.parentView?.addSubview(webView) - } - self.triggerHandler?(ATTNCreativeTriggerStatus.opened) - case .timeout: - Loggers.creative.error("Creative timed out. Not showing WebView.") - self.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) - case .unknown(let statusString): - Loggers.creative.error("Received unknown status: \(statusString). Not showing WebView") - self.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) - default: break - } - } - } - - public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard let url = navigationAction.request.url else { - decisionHandler(.cancel) - return - } - - if url.scheme == "sms" { - UIApplication.shared.open(url) - decisionHandler(.cancel) - } else if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" { - if navigationAction.targetFrame == nil { - UIApplication.shared.open(url) - decisionHandler(.cancel) - } else { - decisionHandler(.allow) - } - } else { - decisionHandler(.allow) - } + webViewHandler?.launchCreative(parentView: view, creativeId: creativeId, handler: handler) } } @@ -303,23 +121,19 @@ extension ATTNSDK: WKNavigationDelegate { extension ATTNSDK { convenience init(domain: String, mode: ATTNSDKMode, urlBuilder: ATTNCreativeUrlProviding) { self.init(domain: domain, mode: mode) - self.urlBuilder = urlBuilder + self.webViewHandler = ATTNWebViewHandler(sdk: self, creativeUrlBuilder: urlBuilder) } convenience init(api: ATTNAPIProtocol, urlBuilder: ATTNCreativeUrlProviding? = nil) { self.init(domain: api.domain) self.api = api guard let urlBuilder = urlBuilder else { return } - self.urlBuilder = urlBuilder + self.webViewHandler = ATTNWebViewHandler(sdk: self, creativeUrlBuilder: urlBuilder) } - func getDomain() -> String { - domain - } + func getDomain() -> String { domain } - func getMode() -> ATTNSDKMode { - mode - } + func getMode() -> ATTNSDKMode { mode } func removeWebView() { webView?.removeFromSuperview() From 5546913243be56bd4a38cc8037fba9e9c63fa66f Mon Sep 17 00:00:00 2001 From: Vladimir Espinola Date: Thu, 4 Jul 2024 15:49:30 -0400 Subject: [PATCH 3/3] removed dependency with sdk --- Sources/ATTNWebViewHandling.swift | 83 +++++++++++++-------- Sources/ATTNWebViewProviding.swift | 20 +++++ Sources/Public/SDK/ATTNSDK.swift | 27 ++++--- attentive-ios-sdk.xcodeproj/project.pbxproj | 4 + 4 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 Sources/ATTNWebViewProviding.swift diff --git a/Sources/ATTNWebViewHandling.swift b/Sources/ATTNWebViewHandling.swift index bdf75af..fe8b941 100644 --- a/Sources/ATTNWebViewHandling.swift +++ b/Sources/ATTNWebViewHandling.swift @@ -37,11 +37,11 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { } } - private weak var sdk: ATTNSDK? + private weak var webViewProvider: ATTNWebViewProviding? private var urlBuilder: ATTNCreativeUrlProviding - init(sdk: ATTNSDK, creativeUrlBuilder: ATTNCreativeUrlProviding = ATTNCreativeUrlProvider()) { - self.sdk = sdk + init(webViewProvider: ATTNWebViewProviding, creativeUrlBuilder: ATTNCreativeUrlProviding = ATTNCreativeUrlProvider()) { + self.webViewProvider = webViewProvider self.urlBuilder = creativeUrlBuilder } @@ -50,22 +50,18 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { creativeId: String? = nil, handler: ATTNCreativeTriggerCompletionHandler? = nil ) { - guard let sdk = sdk else { + guard let webViewProvider = webViewProvider else { Loggers.creative.debug("Not showing the Attentive creative because the iOS version is too old.") - sdk?.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + webViewProvider?.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) return } - sdk.parentView = view - sdk.triggerHandler = handler + webViewProvider.parentView = view + webViewProvider.triggerHandler = handler - let domain = sdk.getDomain() - let mode = sdk.getMode() - let userIdentity = sdk.userIdentity + Loggers.creative.debug("Called showWebView in creativeSDK with domain: \(self.domain, privacy: .public)") - Loggers.creative.debug("Called showWebView in creativeSDK with domain: \(domain, privacy: .public)") - - guard !ATTNSDK.isCreativeOpen else { + guard !isCreativeOpen else { Loggers.creative.debug("Attempted to trigger creative, but creative is currently open. Taking no action") return } @@ -76,7 +72,7 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { configuration: ATTNCreativeUrlConfig( domain: domain, creativeId: creativeId, - skipFatigue: sdk.skipFatigueOnCreative, + skipFatigue: webViewProvider.skipFatigueOnCreative, mode: mode.rawValue, userIdentity: userIdentity ) @@ -98,15 +94,15 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { let userScript = WKUserScript(source: userScriptWithEventListener, injectionTime: .atDocumentStart, forMainFrameOnly: false) configuration.userContentController.addUserScript(userScript) - sdk.webView = WKWebView(frame: view.frame, configuration: configuration) + webViewProvider.webView = WKWebView(frame: view.frame, configuration: configuration) - guard let webView = sdk.webView else { return } + guard let webView = webViewProvider.webView else { return } webView.navigationDelegate = self webView.load(request) if mode == .debug { - sdk.parentView?.addSubview(webView) + webViewProvider.parentView?.addSubview(webView) } else { webView.isOpaque = false webView.backgroundColor = .clear @@ -114,9 +110,11 @@ final class ATTNWebViewHandler: NSObject, ATTNWebViewHandling { } func closeCreative() { - sdk?.removeWebView() - ATTNSDK.isCreativeOpen = false - sdk?.triggerHandler?(ATTNCreativeTriggerStatus.closed) + webViewProvider?.webView?.removeFromSuperview() + webViewProvider?.webView = nil + + isCreativeOpen = false + webViewProvider?.triggerHandler?(ATTNCreativeTriggerStatus.closed) Loggers.creative.debug("Successfully closed creative") } } @@ -151,26 +149,26 @@ extension ATTNWebViewHandler: WKNavigationDelegate { in: nil, in: .defaultClient ) { [weak self] result in - guard let self = self, let sdk = self.sdk else { return } + guard let self = self, let webViewProvider = self.webViewProvider else { return } guard case let .success(statusAny) = result else { Loggers.creative.debug("No status returned from JS. Not showing WebView.") - self.sdk?.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + webViewProvider.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) return } switch ScriptStatus.getRawValue(from: statusAny) { case .success: Loggers.creative.debug("Found creative iframe, showing WebView.") - if sdk.getMode() == .production { - sdk.parentView?.addSubview(webView) + if self.mode == .production { + webViewProvider.parentView?.addSubview(webView) } - sdk.triggerHandler?(ATTNCreativeTriggerStatus.opened) + webViewProvider.triggerHandler?(ATTNCreativeTriggerStatus.opened) case .timeout: Loggers.creative.error("Creative timed out. Not showing WebView.") - sdk.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + webViewProvider.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) case .unknown(let statusString): Loggers.creative.error("Received unknown status: \(statusString). Not showing WebView") - sdk.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) + webViewProvider.triggerHandler?(ATTNCreativeTriggerStatus.notOpened) default: break } } @@ -200,17 +198,40 @@ extension ATTNWebViewHandler: WKNavigationDelegate { extension ATTNWebViewHandler: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - let messageBody = message.body as? String ?? "" - Loggers.creative.debug("Web event message: \(messageBody). isCreativeOpen: \(ATTNSDK.isCreativeOpen ? "YES" : "NO")") + let messageBody = message.body as? String ?? "'Empty'" + Loggers.creative.debug("Web event message: \(messageBody). isCreativeOpen: \(self.isCreativeOpen ? "YES" : "NO")") if messageBody == "CLOSE" { closeCreative() } else if messageBody == "IMPRESSION" { Loggers.creative.debug("Creative opened and generated impression event") - ATTNSDK.isCreativeOpen = true - } else if messageBody == String(format: "%@ true", Constants.visibilityEvent), ATTNSDK.isCreativeOpen { + isCreativeOpen = true + } else if messageBody == String(format: "%@ true", Constants.visibilityEvent), isCreativeOpen { Loggers.creative.debug("Nav away from creative, closing") closeCreative() } } } + +fileprivate extension ATTNWebViewHandler { + var domain: String { + webViewProvider?.getDomain() ?? "" + } + + var mode: ATTNSDKMode { + webViewProvider?.getMode() ?? .production + } + + var userIdentity: ATTNUserIdentity { + webViewProvider?.getUserIdentity() ?? .init() + } + + var skipFatigueOnCreative: Bool { + webViewProvider?.skipFatigueOnCreative ?? false + } + + var isCreativeOpen: Bool { + get { webViewProvider?.isCreativeOpen ?? false } + set { webViewProvider?.isCreativeOpen = newValue } + } +} diff --git a/Sources/ATTNWebViewProviding.swift b/Sources/ATTNWebViewProviding.swift new file mode 100644 index 0000000..78454d8 --- /dev/null +++ b/Sources/ATTNWebViewProviding.swift @@ -0,0 +1,20 @@ +// +// ATTNWebViewProviding.swift +// attentive-ios-sdk-framework +// +// Created by Vladimir - Work on 2024-07-04. +// + +import WebKit + +protocol ATTNWebViewProviding: NSObjectProtocol { + var parentView: UIView? { get set } + var webView: WKWebView? { get set } + var skipFatigueOnCreative: Bool { get set } + var triggerHandler: ATTNCreativeTriggerCompletionHandler? { get set } + var isCreativeOpen: Bool { get set } + + func getDomain() -> String + func getMode() -> ATTNSDKMode + func getUserIdentity() -> ATTNUserIdentity +} diff --git a/Sources/Public/SDK/ATTNSDK.swift b/Sources/Public/SDK/ATTNSDK.swift index d15ba85..c572175 100644 --- a/Sources/Public/SDK/ATTNSDK.swift +++ b/Sources/Public/SDK/ATTNSDK.swift @@ -12,8 +12,7 @@ public typealias ATTNCreativeTriggerCompletionHandler = (String) -> Void @objc(ATTNSDK) public final class ATTNSDK: NSObject { - // MARK: Static Properties - static var isCreativeOpen = false + var isCreativeOpen = false // MARK: Instance Properties var parentView: UIView? @@ -41,7 +40,7 @@ public final class ATTNSDK: NSObject { super.init() - self.webViewHandler = ATTNWebViewHandler(sdk: self) + self.webViewHandler = ATTNWebViewHandler(webViewProvider: self) self.sendInfoEvent() self.initializeSkipFatigueOnCreatives() } @@ -102,6 +101,15 @@ public final class ATTNSDK: NSObject { } } +// MARK: ATTNWebViewProviding +extension ATTNSDK: ATTNWebViewProviding { + func getDomain() -> String { domain } + + func getMode() -> ATTNSDKMode { mode } + + func getUserIdentity() -> ATTNUserIdentity { userIdentity } +} + // MARK: Private Helpers fileprivate extension ATTNSDK { func sendInfoEvent() { @@ -121,22 +129,13 @@ fileprivate extension ATTNSDK { extension ATTNSDK { convenience init(domain: String, mode: ATTNSDKMode, urlBuilder: ATTNCreativeUrlProviding) { self.init(domain: domain, mode: mode) - self.webViewHandler = ATTNWebViewHandler(sdk: self, creativeUrlBuilder: urlBuilder) + self.webViewHandler = ATTNWebViewHandler(webViewProvider: self, creativeUrlBuilder: urlBuilder) } convenience init(api: ATTNAPIProtocol, urlBuilder: ATTNCreativeUrlProviding? = nil) { self.init(domain: api.domain) self.api = api guard let urlBuilder = urlBuilder else { return } - self.webViewHandler = ATTNWebViewHandler(sdk: self, creativeUrlBuilder: urlBuilder) - } - - func getDomain() -> String { domain } - - func getMode() -> ATTNSDKMode { mode } - - func removeWebView() { - webView?.removeFromSuperview() - webView = nil + self.webViewHandler = ATTNWebViewHandler(webViewProvider: self, creativeUrlBuilder: urlBuilder) } } diff --git a/attentive-ios-sdk.xcodeproj/project.pbxproj b/attentive-ios-sdk.xcodeproj/project.pbxproj index 68fbfc8..a8bf9b0 100644 --- a/attentive-ios-sdk.xcodeproj/project.pbxproj +++ b/attentive-ios-sdk.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ FB35C1992C0E5365009FA048 /* ATTNInfoEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB35C1982C0E5365009FA048 /* ATTNInfoEvent+Extension.swift */; }; FB35C19B2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB35C19A2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift */; }; FB4E3FE52C36F7BF004B8FF0 /* ATTNWebViewHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */; }; + FB4E3FE72C372C54004B8FF0 /* ATTNWebViewProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4E3FE62C372C54004B8FF0 /* ATTNWebViewProviding.swift */; }; FB56D4DA2C208BAD00AF7530 /* ATTNSDK+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4D92C208BAD00AF7530 /* ATTNSDK+Extension.swift */; }; FB56D4DC2C208D6100AF7530 /* Boolean+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4DB2C208D6100AF7530 /* Boolean+Extension.swift */; }; FB56D4DE2C208DC100AF7530 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB56D4DD2C208DC100AF7530 /* String+Extension.swift */; }; @@ -146,6 +147,7 @@ FB35C1982C0E5365009FA048 /* ATTNInfoEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNInfoEvent+Extension.swift"; sourceTree = ""; }; FB35C19A2C0E53F9009FA048 /* ATTNCustomEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNCustomEvent+Extension.swift"; sourceTree = ""; }; FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATTNWebViewHandling.swift; sourceTree = ""; }; + FB4E3FE62C372C54004B8FF0 /* ATTNWebViewProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATTNWebViewProviding.swift; sourceTree = ""; }; FB56D4D92C208BAD00AF7530 /* ATTNSDK+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ATTNSDK+Extension.swift"; sourceTree = ""; }; FB56D4DB2C208D6100AF7530 /* Boolean+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Boolean+Extension.swift"; sourceTree = ""; }; FB56D4DD2C208DC100AF7530 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; @@ -474,6 +476,7 @@ FBA9F9EF2C0A77AB00C65024 /* ATTNVisitorService.swift */, FB35C17C2C0E039E009FA048 /* ATTNConstants.swift */, FB4E3FE42C36F7BF004B8FF0 /* ATTNWebViewHandling.swift */, + FB4E3FE62C372C54004B8FF0 /* ATTNWebViewProviding.swift */, FBA9F9FF2C0A77AB00C65024 /* Public */, FBA9FA022C0A77AB00C65024 /* Resources */, ); @@ -674,6 +677,7 @@ FBA9FA112C0A77AB00C65024 /* ATTNCart.swift in Sources */, FBA9FA102C0A77AB00C65024 /* ATTNAddToCartEvent.swift in Sources */, FB35C17B2C0E0353009FA048 /* ATTNIdentifierType.swift in Sources */, + FB4E3FE72C372C54004B8FF0 /* ATTNWebViewProviding.swift in Sources */, FBA9FA0A2C0A77AB00C65024 /* ATTNCreativeUrlProvider.swift in Sources */, FB65536C2C1B74A9008DB3B1 /* ATTNAPIProtocol.swift in Sources */, FB35C17D2C0E039E009FA048 /* ATTNConstants.swift in Sources */,