Skip to content

Commit

Permalink
In-app Content Block feature to invoke action programatically
Browse files Browse the repository at this point in the history
  • Loading branch information
adam1929 authored Jan 11, 2024
1 parent 46038b0 commit 4fe2ae4
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 45 deletions.
98 changes: 97 additions & 1 deletion Documentation/IN_APP_CONTENT_BLOCKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,100 @@ class CustomInAppContentBlockCallback: InAppContentBlockCallbackType {
handleUrlByYourApp(action.url)
}
}
```
```

### Custom presentation of In-app content block

In case that UI presentation of StaticInAppContentBlockView does not fit UX design of your application (for example customized animations) you may create own UIView element that wraps existing StaticInAppContentBlockView instance.
Setup could differ from your use case but you should keep these 4 principles:

1. Prepare StaticInAppContentBlockView instance with deferred load:
```swift
class CustomView: UIViewController {

lazy var placeholder = StaticInAppContentBlockView(placeholder: "placeholder_1", deferredLoad: true)

override func viewDidLoad() {
super.viewDidLoad()
placeholder.behaviourCallback = CustomBehaviourCallback(
placeholder.behaviourCallback,
placeholder,
self
)
placeholderView.reload()
}
}
```
2. Hook your CustomView to listen on In-app Content Block message arrival with customized behaviourCallback:
```swift
class CustomBehaviourCallback: InAppContentBlockCallbackType {

private let originalBehaviour: InAppContentBlockCallbackType
private let ownerView: StaticInAppContentBlockView
private let viewDelegate: InAppCbViewDelegate

init(
_ originalBehaviour: InAppContentBlockCallbackType,
_ ownerView: StaticInAppContentBlockView,
_ viewDelegate: InAppCbViewDelegate
) {
self.originalBehaviour = originalBehaviour
self.ownerView = ownerView
self.viewDelegate = viewDelegate
}

func onMessageShown(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse) {
// Calling originalBehavior tracks 'show' event and opens URL
originalBehaviour.onMessageShown(placeholderId: placeholderId, contentBlock: contentBlock)
viewDelegate.showMessage(contentBlock)
}

func onNoMessageFound(placeholderId: String) {
viewDelegate.showNoMessage()
}

func onError(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse?, errorMessage: String) {
// Calling originalBehavior tracks 'error' event
originalBehaviour.onError(placeholderId: placeholderId, contentBlock: contentBlock, errorMessage: errorMessage)
viewDelegate.showError()
}

func onCloseClicked(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse) {
// Calling originalBehavior tracks 'close' event
originalBehaviour.onCloseClicked(placeholderId: placeholderId, contentBlock: contentBlock)
viewDelegate.hideMe()
}

func onActionClicked(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse, action: ExponeaSDK.InAppContentBlockAction) {
// Calling originalBehavior tracks 'click' event
originalBehaviour.onActionClicked(placeholderId: placeholderId, contentBlock: contentBlock, action: action)
}
}
```
3. Show retrieved message in your customized UIView:
```swift
protocol InAppCbViewDelegate {
func showMessage(_ contentBlock: ExponeaSDK.InAppContentBlockResponse)
func showNoMessage()
func showError()
func hideMe()
}

class CustomView: UIViewController, InAppCbViewDelegate {
/// Update your customized content.
/// This method could be called multiple times for every content block update, especially in case that multiple messages are assigned to given "placeholder_1" ID
func showMessage(_ contentBlock: ExponeaSDK.InAppContentBlockResponse) {
// ...
}
}
```
4. Invoke clicked action manually. For example if your CustomView contains UIButton that is registered with `addTarget` for action URL and is calling `onMyActionClick` method:
```swift
@objc func onMyActionClick(sender: UIButton) {
// retrieve `actionUrl` from extended UIButton or stored in some private field, it is up to you
let actionUrl = getActionUrl(sender)
placeholder.invokeActionClick(actionUrl: actionUrl)
}
```

That is all, now your CustomView will receive all In-app Content Block data.
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import ExponeaSDK
class ExampleInAppContentBlockCallback: InAppContentBlockCallbackType {

private let originalBehaviour: InAppContentBlockCallbackType
private let ownerView: StaticInAppContentBlockView

init(
originalBehaviour: InAppContentBlockCallbackType
originalBehaviour: InAppContentBlockCallbackType,
ownerView: StaticInAppContentBlockView
) {
self.originalBehaviour = originalBehaviour
self.ownerView = ownerView
}

func onMessageShown(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse) {
Expand Down Expand Up @@ -52,26 +55,34 @@ class ExampleInAppContentBlockCallback: InAppContentBlockCallbackType {
)
}

private var actionClickBounce = false

func onActionClicked(placeholderId: String, contentBlock: ExponeaSDK.InAppContentBlockResponse, action: ExponeaSDK.InAppContentBlockAction) {
Exponea.shared.trackInAppContentBlockClickWithoutTrackingConsent(
placeholderId: placeholderId,
action: action,
message: contentBlock
)
Exponea.logger.log(.verbose, message: "Handling In-app content block action \(action.url ?? "none")")
guard let actionUrl = action.url,
let url = actionUrl.cleanedURL() else {
return
}
switch action.type {
case .browser:
UIApplication.shared.open(url, options: [:], completionHandler: nil)
case .deeplink:
if !openUniversalLink(url, application: UIApplication.shared) {
openURLSchemeDeeplink(url, application: UIApplication.shared)
if actionClickBounce {
actionClickBounce = false
Exponea.shared.trackInAppContentBlockClickWithoutTrackingConsent(
placeholderId: placeholderId,
action: action,
message: contentBlock
)
switch action.type {
case .browser:
UIApplication.shared.open(url, options: [:], completionHandler: nil)
case .deeplink:
if !openUniversalLink(url, application: UIApplication.shared) {
openURLSchemeDeeplink(url, application: UIApplication.shared)
}
case .close:
Exponea.logger.log(.error, message: "In-app content block close has to be handled elsewhere")
}
case .close:
Exponea.logger.log(.error, message: "In-app content block close has to be handled elsewhere")
} else {
actionClickBounce = true
ownerView.invokeActionClick(actionUrl: actionUrl)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ class InAppContentBlocksViewController: UIViewController, UITableViewDelegate, U
}
}
let origBehaviour = placeholder.behaviourCallback
placeholder.behaviourCallback = ExampleInAppContentBlockCallback(originalBehaviour: origBehaviour)
placeholder.behaviourCallback = ExampleInAppContentBlockCallback(originalBehaviour: origBehaviour, ownerView: placeholder
)
placeholder.translatesAutoresizingMaskIntoConstraints = false
placeholder.topAnchor.constraint(equalTo: view.topAnchor, constant: 80).isActive = true
placeholder.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,50 @@ public final class StaticInAppContentBlockView: UIView, WKNavigationDelegate {
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let result = inAppContentBlocksManager.inAppContentBlockMessages.first(where: { $0.tags?.contains(webView.tag) == true })
let handled = handleUrlClick(navigationAction.request.url)
decisionHandler(handled ? .cancel : .allow)
}

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://") {
return .browser
}
return .deeplink
}

// directly calls `contentReadyCompletion` with given contentReady flag
// use this method in case that no layout is going to be invoked
private func notifyContentReadyState(_ contentReady: Bool) {
onMain {
self.contentReadyCompletion?(contentReady)
}
}

// registers contentReady flag that will be used for `contentReadyCompletion` when layout/bounds will be updated
private func prepareContentReadyState(_ contentReady: Bool) {
contentReadyFlag = contentReady
}

public func invokeActionClick(actionUrl: String) {
Exponea.logger.log(.verbose, message: "InAppCB: Manual action \(actionUrl) invoked on placeholder \(placeholder)")
_ = handleUrlClick(actionUrl.cleanedURL())
}

private func handleUrlClick(_ actionUrl: URL?) -> Bool {
guard let actionUrl else {
Exponea.logger.log(.warning, message: "InAppCB: Unknown action URL: \(String(describing: actionUrl))")
return false
}
guard let message = assignedMessage else {
Exponea.logger.log(.error, message: "InAppCB: Placeholder \(placeholder) has invalid state - action or message is invalid")
behaviourCallback.onError(placeholderId: placeholder, contentBlock: nil, errorMessage: "Invalid action definition")
// webView has to stop navigation, missing message data are internal issue
return true
}
let result = inAppContentBlocksManager.inAppContentBlockMessages.first(where: { $0.tags?.contains(webview.tag) == true })
let webAction: WebActionManager = .init {
let indexOfMessage: Int = self.inAppContentBlocksManager.inAppContentBlockMessages.firstIndex(where: { $0.id == result?.id ?? "" }) ?? 0
let currentDisplay = self.inAppContentBlocksManager.inAppContentBlockMessages[indexOfMessage].displayState
Expand Down Expand Up @@ -213,36 +256,12 @@ public final class StaticInAppContentBlockView: UIView, WKNavigationDelegate {
Exponea.logger.log(.error, message: "WebActionManager error \(error.localizedDescription)")
}
webAction.htmlPayload = result?.personalizedMessage?.htmlPayload
let handled = webAction.handleActionClick(navigationAction.request.url)
let handled = webAction.handleActionClick(actionUrl)
if handled {
Exponea.logger.log(.verbose, message: "[HTML] Action \(navigationAction.request.url?.absoluteString ?? "Invalid") has been handled")
decisionHandler(.cancel)
Exponea.logger.log(.verbose, message: "[HTML] Action \(actionUrl.absoluteString) has been handled")
} else {
Exponea.logger.log(.verbose, message: "[HTML] Action \(navigationAction.request.url?.absoluteString ?? "Invalid") has not been handled, continue")
decisionHandler(.allow)
}
}

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://") {
return .browser
}
return .deeplink
}

// directly calls `contentReadyCompletion` with given contentReady flag
// use this method in case that no layout is going to be invoked
private func notifyContentReadyState(_ contentReady: Bool) {
onMain {
self.contentReadyCompletion?(contentReady)
Exponea.logger.log(.verbose, message: "[HTML] Action \(actionUrl.absoluteString) has not been handled, continue")
}
}

// registers contentReady flag that will be used for `contentReadyCompletion` when layout/bounds will be updated
private func prepareContentReadyState(_ contentReady: Bool) {
contentReadyFlag = contentReady
return handled
}
}

0 comments on commit 4fe2ae4

Please sign in to comment.