diff --git a/Documentation/APP_INBOX.md b/Documentation/APP_INBOX.md index 25048b7b..c7e8af81 100644 --- a/Documentation/APP_INBOX.md +++ b/Documentation/APP_INBOX.md @@ -222,3 +222,26 @@ If you want to avoid to consider tracking, you may use `Exponea.shared.trackAppI To track an invoking of action, you should use method `Exponea.shared.trackAppInboxClick(MessageItemAction, MessageItem)` with clicked message action and data. The behaviour of `trackAppInboxClick` may be affected by the tracking consent feature, which in enabled mode considers the requirement of explicit consent for tracking. Read more in [tracking consent documentation](./TRACKING_CONSENT.md). If you want to avoid to consider tracking, you may use `Exponea.shared.trackAppInboxClickWithoutTrackingConsent` instead. This method will do track event ignoring tracking consent state. + +## Determine button action URL handling behaviour for HTML message + +Button action URLs are automatically processed by SDK based on URL like: if URL starts with `http` or `https`, action type is set to `browser`, else is set to `deep-link` value. To force behaviour based on your expectation, you can specify optional attribude `data-actiontype` with following values: + +* `browser` - for Web URL to open browser +* `deep-link` - for custom URL scheme and Universal Link to process it + +You can do it in HTML builder by inserting the param to specific action button as described in example below: + +```html +
+
Action
+
+``` + +> This atrribute is also supported for ` + Click me + +``` diff --git a/Documentation/IN_APP_CONTENT_BLOCKS.md b/Documentation/IN_APP_CONTENT_BLOCKS.md index 03d6bd80..791c1d59 100644 --- a/Documentation/IN_APP_CONTENT_BLOCKS.md +++ b/Documentation/IN_APP_CONTENT_BLOCKS.md @@ -266,3 +266,38 @@ class CustomView: UIViewController, InAppCbViewDelegate { ``` That is all, now your CustomView will receive all In-app Content Block data. + +## Determine button action URL handling behaviour for HTML message + +Button action URLs are automatically processed by SDK based on URL like: if URL starts with `http` or `https`, action type is set to `browser`, else is set to `deep-link` value. To force behaviour based on your expectation, you can specify optional attribude `data-actiontype` with following values: + +* `browser` - for Web URL to open browser +* `deep-link` - for custom URL scheme and Universal Link to process it + +You can do it in HTML builder by inserting the param to specific action button as described in example below: + +```html +
+
Action
+
+``` + +> This atrribute is also supported for ` + Click me + +``` + +You can do it in Visual builder as well as described in example below: + +Steps: + +1) Click on the button you want to setup a URL +2) On the right side in editor scroll down +3) Under "Attributes" section click on `ADD NEW ATTRIBUTE` +4) Select `data-actiontype` +5) Insert a value ( `browser` or `deep-link`) + +![Screenshot](/ExponeaSDK/Example/Resources/beefree-actiontype.png) diff --git a/Documentation/IN_APP_MESSAGES.md b/Documentation/IN_APP_MESSAGES.md index f10ffc95..83434d33 100644 --- a/Documentation/IN_APP_MESSAGES.md +++ b/Documentation/IN_APP_MESSAGES.md @@ -117,3 +117,38 @@ Method `trackInAppMessageClose` will track a 'close' event with 'interaction' fi > The behaviour of `trackInAppMessageClick` and `trackInAppMessageClose` may be affected by the tracking consent feature, which in enabled mode considers the requirement of explicit consent for tracking. Read more in [tracking consent documentation](./TRACKING_CONSENT.md). > Note: Invoking of `Exponea.anonymize` does fetch In-apps immediately but `Exponea.identifyCustomer` needs to be sent to backend successfully. The reason is to register customer IDs on backend properly to correctly assign an In-app messages. If you have set other then `Exponea.flushMode = FlushMode.IMMEDIATE` you need to call `Exponea.flushData()` to finalize `identifyCustomer` process and trigger a In-app messages fetch. + +## Determine button action URL handling behaviour for HTML message + +Button action URLs are automatically processed by SDK based on URL like: if URL starts with `http` or `https`, action type is set to `browser`, else is set to `deep-link` value. To force behaviour based on your expectation, you can specify optional attribude `data-actiontype` with following values: + +* `browser` - for Web URL to open browser +* `deep-link` - for custom URL scheme and Universal Link to process it + +You can do it in HTML builder by inserting the param to specific action button as described in example below: + +```html +
+
Action
+
+``` + +> This atrribute is also supported for ` + Click me + +``` + +You can do it in Visual builder as well as described in example below: + +Steps: + +1) Click on the button you want to setup a URL +2) On the right side in editor scroll down +3) Under "Attributes" section click on `ADD NEW ATTRIBUTE` +4) Select `data-actiontype` +5) Insert a value ( `browser` or `deep-link`) + +![Screenshot](/ExponeaSDK/Example/Resources/beefree-actiontype.png) diff --git a/ExponeaSDK/Example/Resources/beefree-actiontype.png b/ExponeaSDK/Example/Resources/beefree-actiontype.png new file mode 100644 index 00000000..5f6e67fc Binary files /dev/null and b/ExponeaSDK/Example/Resources/beefree-actiontype.png differ diff --git a/ExponeaSDK/ExponeaSDK.xcodeproj/project.pbxproj b/ExponeaSDK/ExponeaSDK.xcodeproj/project.pbxproj index 21b7b04c..b9a05ea1 100644 --- a/ExponeaSDK/ExponeaSDK.xcodeproj/project.pbxproj +++ b/ExponeaSDK/ExponeaSDK.xcodeproj/project.pbxproj @@ -205,6 +205,7 @@ 05FEBDC023A7BA2A007C2372 /* InAppMessageTrackingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FEBDBF23A7BA2A007C2372 /* InAppMessageTrackingDelegate.swift */; }; 05FEBDC223A7C940007C2372 /* MockInAppMessageTrackingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FEBDC123A7C940007C2372 /* MockInAppMessageTrackingDelegate.swift */; }; 23E725E4214AA7A900B552B8 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E725E3214AA7A900B552B8 /* Reachability.swift */; }; + 315DCDFF2B6A67E9004BD7A7 /* beefree-actiontype.png in Resources */ = {isa = PBXBuildFile; fileRef = 315DCDFE2B6A67E9004BD7A7 /* beefree-actiontype.png */; }; 318B552A2A80E52B00934902 /* DeeplinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 318B55292A80E52B00934902 /* DeeplinkManager.swift */; }; 31C7B4242A822848001BA5E2 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C7B4232A822848001BA5E2 /* Coordinator.swift */; }; 31C7B4262A822FE7001BA5E2 /* ExponeaTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C7B4252A822FE7001BA5E2 /* ExponeaTabBarController.swift */; }; @@ -709,6 +710,7 @@ 05FEBDBF23A7BA2A007C2372 /* InAppMessageTrackingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessageTrackingDelegate.swift; sourceTree = ""; }; 05FEBDC123A7C940007C2372 /* MockInAppMessageTrackingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInAppMessageTrackingDelegate.swift; sourceTree = ""; }; 23E725E3214AA7A900B552B8 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; + 315DCDFE2B6A67E9004BD7A7 /* beefree-actiontype.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "beefree-actiontype.png"; sourceTree = ""; }; 318B55292A80E52B00934902 /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = ""; }; 31C7B4232A822848001BA5E2 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 31C7B4252A822FE7001BA5E2 /* ExponeaTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExponeaTabBarController.swift; sourceTree = ""; }; @@ -1749,6 +1751,7 @@ C0EA5FCD20CE95EE00660802 /* Resources */ = { isa = PBXGroup; children = ( + 315DCDFE2B6A67E9004BD7A7 /* beefree-actiontype.png */, C0F6C4E92098A1AC00834E21 /* Assets.xcassets */, C0F6C4EE2098A1AC00834E21 /* Info.plist */, C0F6C4EB2098A1AC00834E21 /* LaunchScreen.storyboard */, @@ -2534,6 +2537,7 @@ files = ( 55EFD79B292245A5002569FB /* Localizable.strings in Resources */, C0F6C4ED2098A1AC00834E21 /* LaunchScreen.storyboard in Resources */, + 315DCDFF2B6A67E9004BD7A7 /* beefree-actiontype.png in Resources */, C03B23D421AAF34000360EE6 /* beep.wav in Resources */, C0F6C4EA2098A1AC00834E21 /* Assets.xcassets in Resources */, C0F6C4E82098A1AB00834E21 /* Main.storyboard in Resources */, diff --git a/ExponeaSDK/ExponeaSDK/Classes/InAppContentBlocks/StaticInAppContentBlockView.swift b/ExponeaSDK/ExponeaSDK/Classes/InAppContentBlocks/StaticInAppContentBlockView.swift index 9d99f3d8..eabd65f1 100644 --- a/ExponeaSDK/ExponeaSDK/Classes/InAppContentBlocks/StaticInAppContentBlockView.swift +++ b/ExponeaSDK/ExponeaSDK/Classes/InAppContentBlocks/StaticInAppContentBlockView.swift @@ -184,13 +184,20 @@ public final class StaticInAppContentBlockView: UIView, WKNavigationDelegate { } private func determineActionType(_ action: ActionInfo) -> InAppContentBlockActionType { - if action.actionUrl == "https://exponea.com/close_action" { - return .close - } - if action.actionUrl.starts(with: "http://") || action.actionUrl.starts(with: "https://") { + switch action.actionType { + case .browser: return .browser + case .deeplink: + return .deeplink + case .unknown: + if action.actionUrl == "https://exponea.com/close_action" { + return .close + } + if action.actionUrl.starts(with: "http://") || action.actionUrl.starts(with: "https://") { + return .browser + } + return .deeplink } - return .deeplink } // directly calls `contentReadyCompletion` with given contentReady flag diff --git a/ExponeaSDK/ExponeaSDK/Classes/InAppMessages/View/InAppMessageWebView.swift b/ExponeaSDK/ExponeaSDK/Classes/InAppMessages/View/InAppMessageWebView.swift index fddda075..5a4ae367 100644 --- a/ExponeaSDK/ExponeaSDK/Classes/InAppMessages/View/InAppMessageWebView.swift +++ b/ExponeaSDK/ExponeaSDK/Classes/InAppMessages/View/InAppMessageWebView.swift @@ -11,7 +11,7 @@ final class InAppMessageWebView: UIView, InAppMessageView { private var inAppContentBlocksManager: InAppContentBlocksManagerType = InAppContentBlocksManager.manager var normalizedPayload: NormalizedResult? - + var actionManager: WebActionManager? required init( @@ -133,17 +133,24 @@ final class InAppMessageWebView: UIView, InAppMessageView { private func toPayloadButton(_ action: ActionInfo) -> InAppMessagePayloadButton { InAppMessagePayloadButton( buttonText: action.buttonText, - rawButtonType: detectActionType(action.actionUrl.cleanedURL()!).rawValue, + rawButtonType: detectActionType(action).rawValue, buttonLink: action.actionUrl, buttonTextColor: nil, buttonBackgroundColor: nil ) } - private func detectActionType(_ url: URL) -> InAppMessageButtonType { - if url.scheme == "http" || url.scheme == "https" { + private func detectActionType(_ action: ActionInfo) -> InAppMessageButtonType { + switch action.actionType { + case .browser: return .browser + case .deeplink: + return .deeplink + case .unknown: + if action.actionUrl.cleanedURL()!.scheme == "http" || action.actionUrl.cleanedURL()!.scheme == "https" { + return .browser + } + return .deeplink } - return .deeplink } } diff --git a/ExponeaSDK/ExponeaSDK/Classes/Models/InAppMessage/InAppMessagePayload.swift b/ExponeaSDK/ExponeaSDK/Classes/Models/InAppMessage/InAppMessagePayload.swift index 584abbf7..a12e26f4 100644 --- a/ExponeaSDK/ExponeaSDK/Classes/Models/InAppMessage/InAppMessagePayload.swift +++ b/ExponeaSDK/ExponeaSDK/Classes/Models/InAppMessage/InAppMessagePayload.swift @@ -86,5 +86,5 @@ public struct InAppMessagePayloadButton: Codable, Equatable { public enum InAppMessageButtonType: String { case cancel case deeplink = "deep-link" - case browser = "browser" + case browser } diff --git a/ExponeaSDK/ExponeaSDK/Classes/Others/HtmlNormalizer.swift b/ExponeaSDK/ExponeaSDK/Classes/Others/HtmlNormalizer.swift index 6aeca644..fed5c8f8 100644 --- a/ExponeaSDK/ExponeaSDK/Classes/Others/HtmlNormalizer.swift +++ b/ExponeaSDK/ExponeaSDK/Classes/Others/HtmlNormalizer.swift @@ -12,6 +12,7 @@ public class HtmlNormalizer { private let closeButtonAttrDef = "data-actiontype='close'" private let closeButtonSelector = "[data-actiontype='close']" private let actionButtonAttr = "data-link" + private let dataLinkTypeAttr = "data-actiontype" private let datalinkButtonSelector = "[data-link]" private let anchorlinkButtonSelector = "a[href]" @@ -132,16 +133,31 @@ public class HtmlNormalizer { private func ensureActionButtons() throws -> [ActionInfo] { var result: [String: ActionInfo] = [:] + guard let document = document else { Exponea.logger.log(.warning, message: "[HTML] Document has not been initialized, no Action buttons") return [] } // collect 'data-link' first as it may update href try collectDataLinkButtons(document).forEach { action in - result[action.actionUrl] = action + var existingResult = result[action.actionUrl] + if existingResult == nil { + result[action.actionUrl] = ActionInfo(buttonText: action.buttonText, actionUrl: action.actionUrl, actionType: action.actionType) + } else { + existingResult?.actionUrl = action.actionUrl + existingResult?.buttonText = action.buttonText + result[action.actionUrl] = existingResult + } } try collectAnchorLinkButtons(document).forEach { action in - result[action.actionUrl] = action + var existingResult = result[action.actionUrl] + if existingResult == nil { + result[action.actionUrl] = ActionInfo(buttonText: action.buttonText, actionUrl: action.actionUrl, actionType: action.actionType) + } else { + existingResult?.actionUrl = action.actionUrl + existingResult?.buttonText = action.buttonText + result[action.actionUrl] = existingResult + } } return Array(result.values) } @@ -151,11 +167,12 @@ public class HtmlNormalizer { let anchorlinkButtons = try document.select(anchorlinkButtonSelector) for actionButton in anchorlinkButtons.array() { let targetAction = try actionButton.attr(hrefAttr) + let actionType: ActionType = .init(try actionButton.attr(dataLinkTypeAttr)) if targetAction.isEmpty { Exponea.logger.log(.error, message: "[HTML] Action button found but with empty action") continue } - result.append(ActionInfo(buttonText: try actionButton.text(), actionUrl: targetAction)) + result.append(ActionInfo(buttonText: try actionButton.text(), actionUrl: targetAction, actionType: actionType)) } return result } @@ -165,6 +182,7 @@ public class HtmlNormalizer { let datalinkButtons = try document.select(datalinkButtonSelector) for actionButton in datalinkButtons.array() { let targetAction = try actionButton.attr(actionButtonAttr) + let dataLinkType: ActionType = .init(try actionButton.attr(dataLinkTypeAttr)) if targetAction.isEmpty { Exponea.logger.log(.error, message: "[HTML] Action button found but with empty action") continue @@ -184,7 +202,7 @@ public class HtmlNormalizer { """) try actionButton.wrap("") } - result.append(ActionInfo(buttonText: try actionButton.text(), actionUrl: targetAction)) + result.append(ActionInfo(buttonText: try actionButton.text(), actionUrl: targetAction, actionType: dataLinkType)) } return result } @@ -555,6 +573,23 @@ public struct NormalizedResult { public struct ActionInfo { public var buttonText: String public var actionUrl: String + public var actionType: ActionType + + public init(buttonText: String, actionUrl: String, actionType: ActionType = .unknown) { + self.buttonText = buttonText + self.actionUrl = actionUrl + self.actionType = actionType + } +} + +public enum ActionType: String { + case unknown + case deeplink = "deep-link" + case browser + + init(_ type: String) { + self = .init(rawValue: type) ?? .unknown + } } public struct HtmlNormalizerConfig { diff --git a/ExponeaSDK/ExponeaSDK/Classes/Others/WebActionsManager.swift b/ExponeaSDK/ExponeaSDK/Classes/Others/WebActionsManager.swift index dee47ae8..a091c012 100644 --- a/ExponeaSDK/ExponeaSDK/Classes/Others/WebActionsManager.swift +++ b/ExponeaSDK/ExponeaSDK/Classes/Others/WebActionsManager.swift @@ -10,17 +10,18 @@ import Foundation import WebKit final class WebActionManager: NSObject, WKNavigationDelegate { - + // MARK: - Properties - + var htmlPayload: NormalizedResult? - + private var onCloseCallback: (() -> Void)? private var onActionCallback: ((ActionInfo) -> Void)? private var onErrorCallback: ((ExponeaError) -> Void)? - + private var onActionTypeCallback: ((String) -> Void)? + // MARK: - Init - + init( onCloseCallback: (() -> Void)? = nil, onActionCallback: ((ActionInfo) -> Void)? = nil, @@ -30,9 +31,9 @@ final class WebActionManager: NSObject, WKNavigationDelegate { self.onActionCallback = onActionCallback self.onErrorCallback = onErrorCallback } - + // MARK: - Methods - + func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, @@ -55,7 +56,7 @@ final class WebActionManager: NSObject, WKNavigationDelegate { return true } else if isActionUrl(url) { guard let url = url, - let action = findActionByUrl(url) else { + var action = findActionByUrl(url) else { Exponea.logger.log(.error, message: "[HTML] Action URL \(url?.absoluteString ?? "") cannot be found as action") onErrorCallback?(ExponeaError.unknownError("Invalid Action URL - not found")) // anyway we define it as Action, so URL opening has to be prevented @@ -75,7 +76,7 @@ final class WebActionManager: NSObject, WKNavigationDelegate { } } - private extension WebActionManager { +private extension WebActionManager { func isBlankNav(_ url: URL?) -> Bool { url?.absoluteString == "about:blank" } @@ -86,14 +87,14 @@ final class WebActionManager: NSObject, WKNavigationDelegate { } return !isCloseAction(url) && findActionByUrl(url) != nil } - + func isCloseAction(_ url: URL?) -> Bool { guard let htmlPayload = htmlPayload else { return false } return url?.absoluteString == htmlPayload.closeActionUrl } - + func findActionByUrl(_ url: URL?) -> ActionInfo? { guard let url = url, let htmlPayload = htmlPayload else { @@ -103,7 +104,7 @@ final class WebActionManager: NSObject, WKNavigationDelegate { areEqualAsURLs(action.actionUrl, url.absoluteString) }) } - + /** Put URL().absoluteString here. WKWebView is returning a slash at the end of URL, so we need to compare it properly @@ -120,11 +121,11 @@ final class WebActionManager: NSObject, WKNavigationDelegate { let path2 = url2?.path == "/" ? "" : url2?.path let query2 = url2?.query return ( - scheme1 == scheme2 - && host1 == host2 - && path1 == path2 - && query1 == query2 + scheme1 == scheme2 + && host1 == host2 + && path1 == path2 + && query1 == query2 ) } - + } diff --git a/ExponeaSDK/ExponeaSDKTests/Specs/Other/HtmlNormalizerSpec.swift b/ExponeaSDK/ExponeaSDKTests/Specs/Other/HtmlNormalizerSpec.swift index 25667a47..07da7955 100644 --- a/ExponeaSDK/ExponeaSDKTests/Specs/Other/HtmlNormalizerSpec.swift +++ b/ExponeaSDK/ExponeaSDKTests/Specs/Other/HtmlNormalizerSpec.swift @@ -15,6 +15,48 @@ import ExponeaSDKShared final class HtmlNormalizerSpec: QuickSpec { override func spec() { + it("should find data link type - deeplink") { + let rawHtml = "" + + "
Action 1
" + + "" + let result = HtmlNormalizer(rawHtml).normalize() + expect(result.actions.contains(where: { $0.actionType == .deeplink })).to(beTrue()) + } + + it("should find data link type - browser") { + let rawHtml = "" + + "
Action 1
" + + "" + let result = HtmlNormalizer(rawHtml).normalize() + expect(result.actions.contains(where: { $0.actionType == .browser })).to(beTrue()) + } + + it("should find data link type - unknown") { + let rawHtml = "" + + "
Action 1
" + + "" + let result = HtmlNormalizer(rawHtml).normalize() + expect(result.actions.contains(where: { $0.actionType == .unknown })).to(beTrue()) + } + + it("should find data link type - unknown") { + let rawHtml = "" + + "
Action 1
" + + "Action 1" + + "" + let result = HtmlNormalizer(rawHtml).normalize() + expect(result.actions.contains(where: { $0.actionType == .deeplink })).to(beTrue()) + } + + it("should find data link type - browser and deep-link") { + let rawHtml = "" + + "
Action 1
" + + "
Action 1" + + "" + let result = HtmlNormalizer(rawHtml).normalize() + expect(result.actions.filter({ $0.actionType == .deeplink || $0.actionType == .browser }).count).to(equal(2)) + } + it("should find Close and Action url") { let rawHtml = "" + "
Close
" +