-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: get WebView for inline message and add to Inline View to display #717
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Foundation | ||
|
||
// Callbacks specific to inline message events. | ||
protocol InlineMessageManagerDelegate: AnyObject { | ||
func sizeChanged(width: CGFloat, height: CGFloat) | ||
} | ||
|
||
/** | ||
Class that implements the business logic for a inline message being displayed. Handle when action buttons are clicked, render the HTML message, and get callbacks for inline message events. | ||
|
||
Usage: | ||
``` | ||
let inlineMessageManager = InlineMessageManager(siteId: "", message: message) | ||
inlineMessageManager.inlineMessageDelegate = self // Get callbacks for inline message events. | ||
inlineMessageManager.inlineMessageView // View that displays the in-app web message | ||
``` | ||
*/ | ||
class InlineMessageManager: MessageManager { | ||
var inlineMessageView: GistView? { | ||
let view = super.gistView | ||
view?.delegate = self | ||
return view | ||
} | ||
|
||
weak var inlineMessageDelegate: InlineMessageManagerDelegate? | ||
|
||
override func onReplaceMessage(newMessageToShow: Message) { | ||
// Not yet implemented. Planned in future update. | ||
} | ||
|
||
override func onDoneLoadingMessage(routeLoaded: String, onComplete: @escaping () -> Void) { | ||
// The Inline View is responsible for making the in-app message visible in the UI. No logic needed in the manager. | ||
onComplete() | ||
} | ||
|
||
override func onDeepLinkOpened() { | ||
// Do not do anything. Continue showing the in-app message. | ||
} | ||
|
||
override func onCloseAction() { | ||
// Not yet implemented. Planned in future update. | ||
} | ||
} | ||
|
||
extension InlineMessageManager: GistViewDelegate { | ||
func sizeChanged(message: Message, width: CGFloat, height: CGFloat) { | ||
inlineMessageDelegate?.sizeChanged(width: width, height: height) | ||
} | ||
|
||
func action(message: Message, currentRoute: String, action: String, name: String) { | ||
// Action button handling is processed by the superclass. Ignore this callback and instead use one of the superclass event callback functions. | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,13 +5,18 @@ | |
case close = "gist://close" | ||
} | ||
|
||
class MessageManager: EngineWebDelegate { | ||
/** | ||
Handles business logic for in-app message events such as loading messages and handling when action buttons are clicked. | ||
|
||
This class is meant to be extended and not constructed directly. It holds the common logic between all in-app message types. | ||
|
||
Usage: | ||
* Extend class. | ||
* Override any of the abstract functions in class to implement custom logic for when certain events happen. Depending on the type of message you are displaying, you may want to handle events differently. | ||
*/ | ||
class MessageManager { | ||
private var engine: EngineWeb? | ||
private let siteId: String | ||
private var messagePosition: MessagePosition = .top | ||
private var messageLoaded = false | ||
private var modalViewManager: ModalViewManager? | ||
var isMessageEmbed = false | ||
let currentMessage: Message | ||
var gistView: GistView! | ||
private var currentRoute: String | ||
|
@@ -32,51 +37,49 @@ | |
properties: message.toEngineRoute().properties | ||
) | ||
|
||
// When EngineWeb instance is constructed, it will begin the rendering process for the in-app message. | ||
// This means that the message begins the process of loading. | ||
// Start a timer that helps us determine how long a message took to load/render. | ||
elapsedTimer.start(title: "Loading message with id: \(currentMessage.messageId)") | ||
self.engine = EngineWeb(configuration: engineWebConfiguration) | ||
|
||
if let engine = engine { | ||
engine.delegate = self | ||
self.gistView = GistView(message: currentMessage, engineView: engine.view) | ||
} | ||
} | ||
|
||
func showMessage(position: MessagePosition) { | ||
elapsedTimer.start(title: "Displaying modal for message: \(currentMessage.messageId)") | ||
messagePosition = position | ||
deinit { | ||
engine?.cleanEngineWeb() | ||
engine = nil | ||
} | ||
|
||
func getMessageView() -> GistView { | ||
isMessageEmbed = true | ||
return gistView | ||
// MARK: event listeners that subclasses override to handle events. | ||
|
||
// Called when close action button pressed. | ||
func onCloseAction() { | ||
// Expect subclass implements this. | ||
} | ||
|
||
private func loadModalMessage() { | ||
if messageLoaded { | ||
modalViewManager = ModalViewManager(gistView: gistView, position: messagePosition) | ||
modalViewManager?.showModalView { [weak self] in | ||
guard let self = self else { return } | ||
self.delegate?.messageShown(message: self.currentMessage) | ||
self.elapsedTimer.end() | ||
} | ||
} | ||
// Called when a deep link action button was clicked in a message and the SDK opened the deep link. | ||
func onDeepLinkOpened() { | ||
// expect subclass implements this. | ||
} | ||
|
||
func dismissMessage(completionHandler: (() -> Void)? = nil) { | ||
if let modalViewManager = modalViewManager { | ||
modalViewManager.dismissModalView { [weak self] in | ||
guard let self = self else { return } | ||
self.delegate?.messageDismissed(message: self.currentMessage) | ||
completionHandler?() | ||
} | ||
} | ||
Comment on lines
-64
to
-70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is code that should only be called for modal messages. So I moved this block into the Modal manager subclass. |
||
// Called when the message has finished loading and the WebView is ready to display the message. | ||
func onDoneLoadingMessage(routeLoaded: String, onComplete: @escaping () -> Void) { | ||
// expect subclass implements this. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am honestly not big fan of subclassing when there are different approaches that makes the code easier to follow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my latest update. |
||
} | ||
|
||
func removePersistentMessage() { | ||
if currentMessage.gistProperties.persistent == true { | ||
Logger.instance.debug(message: "Persistent message dismissed, logging view") | ||
Gist.shared.logMessageView(message: currentMessage) | ||
} | ||
// Called when an action button is clicked and the action is to show a different in-app message. | ||
func onReplaceMessage(newMessageToShow: Message) { | ||
// subclass should implement | ||
} | ||
} | ||
|
||
// The main logic of this class is being the delegate for the EngineWeb instance. | ||
// This class's delegate responsibilities are to run the logic that's common to all types of in-app messages and call the event listeners that subclasses override. | ||
extension MessageManager: EngineWebDelegate { | ||
func bootstrapped() { | ||
Logger.instance.debug(message: "Bourbon Engine bootstrapped") | ||
|
||
|
@@ -86,7 +89,6 @@ | |
} | ||
} | ||
|
||
// swiftlint:disable cyclomatic_complexity | ||
func tap(name: String, action: String, system: Bool) { | ||
Logger.instance.info(message: "Action triggered: \(action) with name: \(name)") | ||
delegate?.action(message: currentMessage, currentRoute: currentRoute, action: action, name: name) | ||
|
@@ -96,22 +98,15 @@ | |
switch url.host { | ||
case "close": | ||
Logger.instance.info(message: "Dismissing from action: \(action)") | ||
removePersistentMessage() | ||
dismissMessage() | ||
onCloseAction() | ||
case "loadPage": | ||
if let page = url.queryParameters?["url"], | ||
let pageUrl = URL(string: page), | ||
UIApplication.shared.canOpenURL(pageUrl) { | ||
UIApplication.shared.open(pageUrl) | ||
} | ||
case "showMessage": | ||
if currentMessage.isEmbedded { | ||
showNewMessage(url: url) | ||
} else { | ||
dismissMessage { | ||
self.showNewMessage(url: url) | ||
} | ||
} | ||
showNewMessage(url: url) | ||
default: break | ||
} | ||
} else { | ||
|
@@ -140,24 +135,22 @@ | |
UIApplication.shared.open(url) { handled in | ||
if handled { | ||
Logger.instance.info(message: "Dismissing from system action: \(action)") | ||
self.dismissMessage() | ||
self.onDeepLinkOpened() | ||
} else { | ||
Logger.instance.info(message: "System action not handled") | ||
} | ||
} | ||
} else { | ||
Logger.instance.info(message: "Handled by NSUserActivity") | ||
dismissMessage() | ||
onDeepLinkOpened() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// swiftlint:enable cyclomatic_complexity | ||
|
||
// Check if | ||
func continueNSUserActivity(webpageURL: URL) -> Bool { | ||
// Check if deep link can be handled in the host app. By using NSUserActivity, our SDK can handle Universal Links. | ||
private func continueNSUserActivity(webpageURL: URL) -> Bool { | ||
guard #available(iOS 10.0, *) else { | ||
return false | ||
} | ||
|
@@ -174,7 +167,7 @@ | |
} | ||
|
||
// The NSUserActivity.webpageURL property permits only specific URL schemes. This function exists to validate the scheme and prevent potential exceptions due to incompatible URL formats. | ||
func isLinkValidNSUserActivityLink(_ url: URL) -> Bool { | ||
private func isLinkValidNSUserActivityLink(_ url: URL) -> Bool { | ||
guard let schemeOfUrl = url.scheme else { | ||
return false | ||
} | ||
|
@@ -206,25 +199,12 @@ | |
|
||
func routeLoaded(route: String) { | ||
Logger.instance.info(message: "Message loaded with route: \(route)") | ||
|
||
currentRoute = route | ||
if route == currentMessage.messageId, !messageLoaded { | ||
messageLoaded = true | ||
if isMessageEmbed { | ||
delegate?.messageShown(message: currentMessage) | ||
} else { | ||
if UIApplication.shared.applicationState == .active { | ||
loadModalMessage() | ||
} else { | ||
Gist.shared.removeMessageManager(instanceId: currentMessage.instanceId) | ||
} | ||
} | ||
} | ||
Comment on lines
-211
to
-222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is code that should only be called for modal messages. So I moved this block into the Modal manager subclass. |
||
} | ||
|
||
deinit { | ||
engine?.cleanEngineWeb() | ||
engine = nil | ||
onDoneLoadingMessage(routeLoaded: currentRoute) { | ||
self.delegate?.messageShown(message: self.currentMessage) | ||
self.elapsedTimer.end() | ||
} | ||
} | ||
|
||
private func showNewMessage(url: URL) { | ||
|
@@ -236,9 +216,8 @@ | |
let convertedProps = convertToDictionary(text: decodedString) { | ||
properties = convertedProps | ||
} | ||
|
||
if let messageId = url.queryParameters?["messageId"] { | ||
_ = Gist.shared.showMessage(Message(messageId: messageId, properties: properties)) | ||
onReplaceMessage(newMessageToShow: Message(messageId: messageId, properties: properties)) | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import Foundation | ||
import UIKit | ||
|
||
/** | ||
Class that implements the business logic for a modal message being displayed. Handle when action buttons are clicked, render the HTML message, and get callbacks for modal message events. | ||
*/ | ||
class ModalMessageManager: MessageManager { | ||
private var messageLoaded = false | ||
private var modalViewManager: ModalViewManager? | ||
var messagePosition: MessagePosition = .top | ||
|
||
func showMessage(position: MessagePosition) { | ||
messagePosition = position | ||
} | ||
|
||
override func onDoneLoadingMessage(routeLoaded: String, onComplete: @escaping () -> Void) { | ||
if routeLoaded == currentMessage.messageId, !messageLoaded { | ||
messageLoaded = true | ||
|
||
if UIApplication.shared.applicationState == .active { | ||
modalViewManager = ModalViewManager(gistView: gistView, position: messagePosition) | ||
modalViewManager?.showModalView { | ||
onComplete() | ||
} | ||
} else { | ||
Gist.shared.removeMessageManager(instanceId: currentMessage.instanceId) | ||
} | ||
} | ||
} | ||
|
||
override func onDeepLinkOpened() { | ||
dismissMessage() | ||
} | ||
|
||
override func onCloseAction() { | ||
removePersistentMessage() | ||
dismissMessage() | ||
} | ||
|
||
func removePersistentMessage() { | ||
if currentMessage.gistProperties.persistent == true { | ||
Logger.instance.debug(message: "Persistent message dismissed, logging view") | ||
Gist.shared.logMessageView(message: currentMessage) | ||
} | ||
} | ||
|
||
func dismissMessage(completionHandler: (() -> Void)? = nil) { | ||
if let modalViewManager = modalViewManager { | ||
modalViewManager.dismissModalView { [weak self] in | ||
guard let self = self else { return } | ||
self.delegate?.messageDismissed(message: self.currentMessage) | ||
completionHandler?() | ||
} | ||
} | ||
} | ||
|
||
override func onReplaceMessage(newMessageToShow: Message) { | ||
dismissMessage { | ||
_ = Gist.shared.showMessage(newMessageToShow) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the main logic of this PR.
MessageManager
contains a lot of code that's common between inline and modal messages.ModalMessageManager
contains code I pulled out ofMessageManager
that's only used for modal messages.