Skip to content

Commit

Permalink
adds widget callback promise (#24)
Browse files Browse the repository at this point in the history
* adds widget callback promise

* tighten up sdk code for pr
  • Loading branch information
scrummitch authored Sep 20, 2024
1 parent d65d627 commit ab53dad
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 34 deletions.
143 changes: 116 additions & 27 deletions Sources/InAppNotifications/OrttoCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import Foundation
import OrttoSDKCore
import SwiftUI
import UIKit

public protocol Capture {
func showWidget(_ id: String)
func showWidget(_ id: String) -> Promise<Result<Void, Error>>
func queueWidget(_ id: String)
static func getKeyWindow() -> UIWindow?
}
Expand Down Expand Up @@ -53,6 +54,16 @@ public class OrttoCapture: ObservableObject, Capture {
return _widgetView!
}

private var retainedWebViewController: UIViewController?

private func retainWebViewController(_ viewController: UIViewController) {
retainedWebViewController = viewController
}

private func releaseWebViewController() {
retainedWebViewController = nil
}

public private(set) static var shared: OrttoCapture!

init(dataSourceKey: String, captureJSURL: URL?, apiHost: URL?) {
Expand Down Expand Up @@ -112,31 +123,47 @@ public class OrttoCapture: ObservableObject, Capture {
_queue.queue(id)
}

public func showWidget(_ id: String) {
let canShowWidget = {
os_unfair_lock_lock(&lock)

var canShowWidget = false

if !isWidgetActive {
isWidgetActive = true
canShowWidget = true
public func showWidget(_ id: String) -> Promise<Result<Void, Error>> {
return Promise { resolver in
// Check if we're on the main thread
guard Thread.isMainThread else {
DispatchQueue.main.async {
self.showWidget(id).then { result in
resolver(result)
}
}
return
}

os_unfair_lock_unlock(&lock)
// Check if widget can be shown
let canShowWidget = {
os_unfair_lock_lock(&self.lock)
defer { os_unfair_lock_unlock(&self.lock) }

return canShowWidget
}()
if !self.isWidgetActive {
self.isWidgetActive = true
return true
}
return false
}()

if !canShowWidget {
return
}
if !canShowWidget {
resolver(.failure(WidgetError.alreadyActive))
return
}

DispatchQueue.main.async {
self._queue.remove(id)

// Check for keyWindow and rootViewController
guard let keyWindow = self.keyWindow,
let rootViewController = keyWindow.rootViewController else {
resolver(.failure(WidgetError.noKeyWindowOrRootViewController))
return
}

self.widgetView.setWidgetId(id)
self.widgetView.load { webView in

let webViewController = UIViewController()
webViewController.edgesForExtendedLayout = .all
webViewController.extendedLayoutIncludesOpaqueBars = true
Expand All @@ -146,22 +173,39 @@ public class OrttoCapture: ObservableObject, Capture {
webViewController.view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: webViewController.view.topAnchor),
webView.bottomAnchor.constraint(equalTo: webViewController.view.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: webViewController.view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: webViewController.view.trailingAnchor),
])
do {
try NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: webViewController.view.topAnchor),
webView.bottomAnchor.constraint(equalTo: webViewController.view.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: webViewController.view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: webViewController.view.trailingAnchor),
])
} catch {
resolver(.failure(WidgetError.constraintActivationFailed(error)))
return
}

webViewController.modalPresentationStyle = .overFullScreen
webViewController.modalTransitionStyle = .crossDissolve

let rootViewController = self.keyWindow?.rootViewController
// Hide keyboard if it's open
rootViewController.view.endEditing(true)

// Retain webViewController to prevent deallocation
self.retainWebViewController(webViewController)

// this is to hide the keyboard in the case that it is currently open
rootViewController?.view.endEditing(true)
// Set a timeout for presentation
let timeout = DispatchWorkItem {
resolver(.failure(WidgetError.presentationTimeout))
self.releaseWebViewController()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: timeout)

rootViewController?.present(webViewController, animated: true)
rootViewController.present(webViewController, animated: true) {
timeout.cancel()
self.releaseWebViewController()
resolver(.success(()))
}
}
}
}
Expand Down Expand Up @@ -277,3 +321,48 @@ public class OrttoCapture: ObservableObject, Capture {
}
}
}

public enum WidgetError: LocalizedError {
case alreadyActive
case noKeyWindowOrRootViewController
case webViewLoadFailed
case constraintActivationFailed(Error)
case presentationTimeout

public var errorDescription: String? {
switch self {
case .alreadyActive:
return "A widget is already active and cannot be shown at this time."
case .noKeyWindowOrRootViewController:
return "Unable to find the key window or root view controller."
case .webViewLoadFailed:
return "Failed to load the web view for the widget."
case .constraintActivationFailed(let error):
return "Failed to activate layout constraints: \(error.localizedDescription)"
case .presentationTimeout:
return "Widget presentation timed out."
}
}
}

// Simple Promise implementation
public class Promise<T> {
private var result: T?
private var completionHandlers: [(T) -> Void] = []

public init(_ closure: (@escaping (T) -> Void) -> Void) {
closure { result in
self.result = result
self.completionHandlers.forEach { $0(result) }
self.completionHandlers.removeAll()
}
}

public func then(_ handler: @escaping (T) -> Void) {
if let result = result {
handler(result)
} else {
completionHandlers.append(handler)
}
}
}
25 changes: 18 additions & 7 deletions Sources/InAppNotifications/WidgetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,29 @@ class WidgetViewNavigationDelegate: NSObject, WKNavigationDelegate {
return .allow
}

if await UIApplication.shared.canOpenURL(url) {
DispatchQueue.main.async {
UIApplication.shared.open(url)
}

return .cancel
// Use a more generic approach to handle URL opening
DispatchQueue.main.async {
self.openURL(url)
}

return .cancel
}

return .allow
}

// Add this helper method to handle URL opening
private func openURL(_ url: URL) {
// Use a completion handler to open the URL
let completionHandler: (Bool) -> Void = { success in
if success {
Ortto.log().debug("URL opened successfully: \(url)")
} else {
Ortto.log().error("Failed to open URL: \(url)")
}
}
Ortto.shared.openURL(url, completionHandler: completionHandler)
}
}

extension WKWebView {
Expand All @@ -252,7 +264,6 @@ extension WKWebView {
}

evaluateJavaScript("ap3cWebView.setConfig(\(json)); ap3cWebView.hasConfig()") { result, error in

if let intResult = result as? Int {
completionHandler?(Bool(intResult > 0), error)
} else {
Expand Down
14 changes: 14 additions & 0 deletions Sources/SDKCore/Ortto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,18 @@ public class Ortto: OrttoInterface {
public func screen(_ screenName: String) {
self.screenName = screenName
}

@available(iOSApplicationExtension, unavailable)
public func openURL(_ url: URL, completionHandler: @escaping (Bool) -> Void) {
#if os(iOS)
DispatchQueue.main.async {
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: completionHandler)
} else {
let success = UIApplication.shared.openURL(url)
completionHandler(success)
}
}
#endif
}
}

0 comments on commit ab53dad

Please sign in to comment.