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
+
+```
+
+> 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
+
+```
+
+> 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
+
+```
+
+> 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
" +