Skip to content
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

feat: support for large in-app messages #831

Merged
merged 4 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 78 additions & 46 deletions Sources/MessagingInApp/Gist/EngineWeb/EngineWeb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,55 +34,64 @@ public class EngineWeb: NSObject, EngineWebInstance {
webView
}

private var currentConfiguration: EngineWebConfiguration?
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved

public private(set) var currentRoute: String {
get {
_currentRoute
}
set {
_currentRoute = newValue
}
get { _currentRoute }
set { _currentRoute = newValue }
}

/// Initializes the EngineWeb instance with the given configuration, state, and message.
init(configuration: EngineWebConfiguration, state: InAppMessageState, message: Message) {
self.currentMessage = message
self.currentConfiguration = configuration

super.init()

_elapsedTimer.start(title: "Engine render for message: \(configuration.messageId)")
setupWebView()
injectJavaScriptListener()
loadMessage(with: state)
}

/// Sets up the properties and appearance of the WKWebView.
private func setupWebView() {
_elapsedTimer.start(title: "Engine render for message: \(currentConfiguration?.messageId ?? "")")

webView.translatesAutoresizingMaskIntoConstraints = false
webView.navigationDelegate = self
webView.isOpaque = false
webView.backgroundColor = UIColor.clear
webView.scrollView.backgroundColor = UIColor.clear
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear

let js = "window.parent.postMessage = function(message) {webkit.messageHandlers.gist.postMessage(message)}"
if #available(iOS 11.0, *) {
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
webView.scrollView.contentInsetAdjustmentBehavior = .never
}
}

/// Injects a JavaScript listener to handle messages from the web content.
private func injectJavaScriptListener() {
let js = """
window.addEventListener('message', function(event) {
webkit.messageHandlers.gist.postMessage(event.data);
});
"""
let messageHandlerScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)

webView.configuration.userContentController.add(self, name: "gist")
webView.configuration.userContentController.addUserScript(messageHandlerScript)
}

if #available(iOS 11.0, *) {
webView.scrollView.contentInsetAdjustmentBehavior = .never
}
private func loadMessage(with state: InAppMessageState) {
let messageUrl = "\(state.environment.networkSettings.renderer)/index.html"
logger.debug("Rendering message with URL: \(messageUrl)")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved

if let jsonData = try? JSONEncoder().encode(configuration),
let jsonString = String(data: jsonData, encoding: .utf8),
let options = jsonString.data(using: .utf8)?.base64EncodedString()
.addingPercentEncoding(withAllowedCharacters: .alphanumerics) {
let url = "\(state.environment.networkSettings.renderer)/index.html?options=\(options)"
logger.logWithModuleTag("Loading URL: \(url)", level: .info)
if let link = URL(string: url) {
self._timeoutTimer = Timer.scheduledTimer(
timeInterval: 5.0,
target: self,
selector: #selector(forcedTimeout),
userInfo: nil,
repeats: false
)
let request = URLRequest(url: link)
webView.load(request)
}
if let url = URL(string: messageUrl) {
_timeoutTimer?.invalidate()
_timeoutTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(forcedTimeout), userInfo: nil, repeats: false)
webView.load(URLRequest(url: url))
} else {
logger.error("Invalid URL: \(messageUrl)")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
delegate?.error()
}
}

Expand All @@ -104,10 +113,7 @@ public class EngineWeb: NSObject, EngineWebInstance {

// swiftlint:disable cyclomatic_complexity
extension EngineWeb: WKScriptMessageHandler {
public func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let dict = message.body as? [String: AnyObject],
let eventProperties = dict["gist"] as? [String: AnyObject],
let method = eventProperties["method"] as? String,
Expand All @@ -116,6 +122,10 @@ extension EngineWeb: WKScriptMessageHandler {
return
}

handleEngineEvent(engineEventMethod, eventProperties: eventProperties)
}

private func handleEngineEvent(_ engineEventMethod: EngineEvent, eventProperties: [String: AnyObject]) {
switch engineEventMethod {
case .bootstrapped:
_timeoutTimer?.invalidate()
Expand Down Expand Up @@ -150,27 +160,49 @@ extension EngineWeb: WKScriptMessageHandler {
}

// swiftlint:enable cyclomatic_complexity

extension EngineWeb: WKNavigationDelegate {
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let configuration = currentConfiguration else {
logger.error("Configuration not available")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
delegate?.error()
return
}

injectConfiguration(configuration)
}

private func injectConfiguration(_ configuration: EngineWebConfiguration) {
do {
let jsonData = try JSONEncoder().encode(["options": configuration])
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
throw NSError(domain: "EngineWeb", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create JSON string"])
}

let js = "window.postMessage(\(jsonString), '*');"

webView.evaluateJavaScript(js) { [weak self] _, error in
if let error = error {
self?.logger.error("JavaScript execution error: \(error)")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
self?.delegate?.error()
} else {
self?.logger.debug("Configuration injected successfully")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
}
}
} catch {
logger.error("Failed to encode configuration: \(error)")
mrehan27 marked this conversation as resolved.
Show resolved Hide resolved
delegate?.error()
}
}

public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
delegate?.error()
}

public func webView(
_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error
) {
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
delegate?.error()
}

public func webView(
_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error
) {
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
delegate?.error()
}
}
4 changes: 2 additions & 2 deletions Sources/MessagingInApp/Gist/Network/NetworkSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ protocol NetworkSettings {
struct NetworkSettingsProduction: NetworkSettings {
let queueAPI = "https://gist-queue-consumer-api.cloud.gist.build"
let engineAPI = "https://engine.api.gist.build"
let renderer = "https://renderer.gist.build/2.0"
let renderer = "https://renderer.gist.build/3.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure about the impact here, but my assumption is that posting JSON might only work with this version of renderer and not the old one. And I believe it should still handle messages from both legacy editor and new editor without issues. However, just to be safe, can you please confirm if this change was intentional? It would also be helpful to mention this on linked ticket or in Slack so the squad is aligned and aware of the update.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure i get it? 3.0 handles the new way of rendering and 2.0 still handles the old way. So the folks who had older SDK version would keep on using 2.0 until they use 3.0?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was exactly my question, whether 3.0 enforces any new requirements when composing in-app messages or if it's fully compatible with existing ones. From your response, it sounds like it is fully compatible. My only request here is to explicitly mention this update on ticket for reference, as I don't see it mentioned in ticket description or comments. I think calling this out clearly would be good for transparency.

}

struct NetworkSettingsDevelopment: NetworkSettings {
let queueAPI = "https://gist-queue-consumer-api.cloud.dev.gist.build"
let engineAPI = "https://engine.api.dev.gist.build"
let renderer = "https://renderer.gist.build/2.0"
let renderer = "https://renderer.gist.build/3.0"
}

struct NetworkSettingsLocal: NetworkSettings {
Expand Down
Loading