Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
33 changes: 33 additions & 0 deletions swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,39 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure)
}

/// Register a Live Activity push token
/// This sends the token as a tracked event so it can be associated with the user profile
@available(iOS 16.1, *)
func registerLiveActivityToken(_ token: Data,
activityId: String,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) {
guard isEitherUserIdOrEmailSet() else {
let errorMessage = "Either userId or email must be set to register Live Activity token"
ITBError(errorMessage)
onFailure?(errorMessage, nil)
return
}

let tokenString = token.hexString()

// Track the Live Activity token registration as a custom event
// This allows the backend to associate the token with the user
let dataFields: [String: Any] = [
"liveActivityToken": tokenString,
"activityId": activityId,
"platform": "iOS",
"registeredAt": ISO8601DateFormatter().string(from: Date())
]

ITBInfo("Registering Live Activity token for activity: \(activityId)")

track("liveActivityTokenRegistered",
dataFields: dataFields,
onSuccess: onSuccess,
onFailure: onFailure)
}

@discardableResult
func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) -> Pending<SendRequestValue, SendRequestError> {
Expand Down
14 changes: 13 additions & 1 deletion swift-sdk/Internal/InternalIterableAppIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ struct InternalIterableAppIntegration {
func application(_ application: ApplicationStateProviderProtocol, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)?) {
ITBInfo()

if case let NotificationInfo.silentPush(silentPush) = NotificationHelper.inspect(notification: userInfo) {
let notificationInfo = NotificationHelper.inspect(notification: userInfo)

switch notificationInfo {
case .silentPush(let silentPush):
switch silentPush.notificationType {
case .update:
_ = inAppNotifiable?.scheduleSync()
Expand All @@ -160,6 +163,15 @@ struct InternalIterableAppIntegration {
ITBError("messageId not found in 'remove' silent push")
}
}
case .liveActivity(let metadata):
#if canImport(ActivityKit)
if #available(iOS 16.2, *) {
IterableLiveActivityManager.shared
.startRunComparison(against: .alex, at: .normal)
}
#endif
case .iterable, .other:
break
}

completionHandler?(.noData)
Expand Down
90 changes: 73 additions & 17 deletions swift-sdk/Internal/IterableHtmlMessageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,20 @@ protocol MessageViewControllerDelegate: AnyObject {
func messageDeinitialized()
}

class IterableHtmlMessageViewController: UIViewController {
/// Weak wrapper to avoid retain cycle with WKUserContentController
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: WKScriptMessageHandler?

init(delegate: WKScriptMessageHandler) {
self.delegate = delegate
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
delegate?.userContentController(userContentController, didReceive: message)
}
}

class IterableHtmlMessageViewController: UIViewController, WKScriptMessageHandler {
struct Parameters {
let html: String
let padding: Padding
Expand Down Expand Up @@ -98,11 +111,9 @@ class IterableHtmlMessageViewController: UIViewController {
private init(parameters: Parameters,
eventTrackerProvider: @escaping @autoclosure () -> MessageViewControllerEventTrackerProtocol?,
onClickCallback: ((URL) -> Void)?,
webViewProvider: @escaping @autoclosure () -> WebViewProtocol = IterableHtmlMessageViewController.createWebView(),
delegate: MessageViewControllerDelegate?) {
ITBInfo()
self.eventTrackerProvider = eventTrackerProvider
self.webViewProvider = webViewProvider
self.parameters = parameters
self.onClickCallback = onClickCallback
self.delegate = delegate
Expand All @@ -128,10 +139,15 @@ class IterableHtmlMessageViewController: UIViewController {
view.backgroundColor = InAppCalculations.initialViewBackgroundColor(isModal: parameters.isModal)

webView.set(position: ViewPosition(width: view.frame.width, height: view.frame.height, center: view.center))
webView.loadHTMLString(parameters.html, baseURL: URL(string: ""))

var html = parameters.html
if let jsString = parameters.messageMetadata?.message.customPayload?["js"] as? String {
html += "<script>\(jsString)</script>"
}
webView.loadHTMLString(html, baseURL: URL(string: ""))
webView.set(navigationDelegate: self)

view.addSubview(webView.view)
view.addSubview(webView)
}

override func viewDidLoad() {
Expand Down Expand Up @@ -197,29 +213,37 @@ class IterableHtmlMessageViewController: UIViewController {

deinit {
ITBInfo()
webView.configuration.userContentController.removeScriptMessageHandler(forName: "textHandler")
delegate?.messageDeinitialized()
}

private var eventTrackerProvider: () -> MessageViewControllerEventTrackerProtocol?
private var webViewProvider: () -> WebViewProtocol
private var parameters: Parameters
private var onClickCallback: ((URL) -> Void)?
private var delegate: MessageViewControllerDelegate?
private var location: IterableMessageLocation = .full
private var linkClicked = false
private var clickedLink: String?

private lazy var webView = webViewProvider()
private var eventTracker: MessageViewControllerEventTrackerProtocol? {
eventTrackerProvider()
}
private lazy var scriptMessageHandler = WeakScriptMessageHandler(delegate: self)

private static func createWebView() -> WebViewProtocol {
let webView = WKWebView(frame: .zero)
private lazy var webView: WKWebView = {
let contentController = WKUserContentController()
contentController.add(scriptMessageHandler, name: "textHandler")

let config = WKWebViewConfiguration()
config.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: config)
webView.scrollView.bounces = false
webView.scrollView.delaysContentTouches = false
webView.isUserInteractionEnabled = true
webView.isOpaque = false
webView.backgroundColor = UIColor.clear
return webView as WebViewProtocol
return webView
}()

private var eventTracker: MessageViewControllerEventTrackerProtocol? {
eventTrackerProvider()
}

/// Resizes the webview based upon the insetPadding, height etc
Expand Down Expand Up @@ -268,11 +292,11 @@ class IterableHtmlMessageViewController: UIViewController {
private func applyAnimation(animationDetail: InAppCalculations.AnimationDetail, completion: (() -> Void)? = nil) {
Self.animate(duration: parameters.animationDuration) { [weak self] in
self?.webView.set(position: animationDetail.initial.position)
self?.webView.view.alpha = animationDetail.initial.alpha
self?.webView.alpha = animationDetail.initial.alpha
self?.view.backgroundColor = animationDetail.initial.bgColor
} finalValues: { [weak self] in
self?.webView.set(position: animationDetail.final.position)
self?.webView.view.alpha = animationDetail.final.alpha
self?.webView.alpha = animationDetail.final.alpha
self?.view.backgroundColor = animationDetail.final.bgColor
} completion: {
completion?()
Expand All @@ -292,7 +316,7 @@ class IterableHtmlMessageViewController: UIViewController {
}
}

static func calculateWebViewPosition(webView: WebViewProtocol,
static func calculateWebViewPosition(webView: WKWebView,
safeAreaInsets: UIEdgeInsets,
parentPosition: ViewPosition,
paddingLeft: CGFloat,
Expand Down Expand Up @@ -356,7 +380,7 @@ extension IterableHtmlMessageViewController: WKNavigationDelegate {
@available(iOS 13.0, *)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
if #available(iOS 14.0, *) {
preferences.allowsContentJavaScript = false
preferences.allowsContentJavaScript = true
}
guard navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url else {
decisionHandler(.allow, preferences)
Expand Down Expand Up @@ -405,3 +429,35 @@ extension IterableHtmlMessageViewController: WKNavigationDelegate {
}

}

extension IterableHtmlMessageViewController {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageString = message.body as? String else { return }

let components = messageString
.lowercased()
.trimmingCharacters(in: .whitespaces)
.split(separator: "|")
.map { $0.trimmingCharacters(in: .whitespaces) }

guard components.count == 2,
let runner = RunnerName.allCases.first(where: { $0.rawValue.lowercased() == components[0] }),
let pace = PaceLevel.allCases.first(where: { $0.rawValue.lowercased() == components[1] }) else {
ITBError("Failed to parse challenge message: \(messageString)")
return
}

ITBInfo("Starting Live Activity: \(runner.rawValue) at \(pace.rawValue)")

#if canImport(ActivityKit)
if #available(iOS 16.2, *) {
if let activityId = IterableLiveActivityManager.shared.startRunComparison(against: runner, at: pace) {
IterableLiveActivityManager.shared.startMockUpdates(activityId: activityId, updateInterval: 3.0)
}
}
#endif

// Dismiss the in-app view
animateWhileLeaving(webView.position)
}
}
22 changes: 22 additions & 0 deletions swift-sdk/Internal/Utilities/NotificationHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,25 @@ import Foundation
enum NotificationInfo {
case silentPush(ITBLSilentPushNotificationInfo)
case iterable(IterablePushNotificationMetadata)
case liveActivity(IterableLiveActivityMetadata)
case other
}

struct IterableLiveActivityMetadata {
let vital: String
let duration: TimeInterval
let title: String

static func parse(info: [String: Any]) -> IterableLiveActivityMetadata? {
guard let vital = info["vital"] as? String,
let duration = info["duration"] as? TimeInterval,
let title = info["title"] as? String else {
return nil
}
return IterableLiveActivityMetadata(vital: vital, duration: duration, title: title)
}
}

struct ITBLSilentPushNotificationInfo {
let notificationType: ITBLSilentPushNotificationType
let messageId: String?
Expand Down Expand Up @@ -44,6 +60,11 @@ struct NotificationHelper {
return .other
}

if let liveActivityInfo = itblElement[Keys.liveActivity.rawValue] as? [String: Any],
let metadata = IterableLiveActivityMetadata.parse(info: liveActivityInfo) {
return .liveActivity(metadata)
}

if isGhostPush {
if let silentPush = ITBLSilentPushNotificationInfo.parse(notification: notification) {
return .silentPush(silentPush)
Expand All @@ -66,6 +87,7 @@ struct NotificationHelper {
private enum Keys: String {
case itbl
case isGhostPush
case liveActivity
}
}

Expand Down
16 changes: 1 addition & 15 deletions swift-sdk/Internal/Utilities/WebViewProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,7 @@ struct ViewPosition: Equatable {
var center: CGPoint = CGPoint.zero
}

protocol WebViewProtocol {
var view: UIView { get }
var position: ViewPosition { get }
@discardableResult func loadHTMLString(_ string: String, baseURL: URL?) -> WKNavigation?
func set(position: ViewPosition)
func set(navigationDelegate: WKNavigationDelegate?)
func layoutSubviews()
func calculateHeight() -> Pending<CGFloat, IterableError>
}

extension WKWebView: WebViewProtocol {
var view: UIView {
self
}

extension WKWebView {
var position: ViewPosition {
ViewPosition(width: frame.size.width, height: frame.size.height, center: center)
}
Expand Down
23 changes: 23 additions & 0 deletions swift-sdk/SDK/IterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,29 @@ import UIKit
implementation.register(token: token, onSuccess: onSuccess, onFailure: onFailure)
}

// MARK: - Live Activity Token Registration

/// Register a Live Activity push token with Iterable
///
/// - Parameters:
/// - token: The push token for the Live Activity
/// - activityId: The unique identifier for the Live Activity
/// - onSuccess: `OnSuccessHandler` to invoke if registration is successful
/// - onFailure: `OnFailureHandler` to invoke if registration fails
@available(iOS 16.1, *)
public static func registerLiveActivityToken(
_ token: Data,
activityId: String,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil
) {
guard let implementation, implementation.isSDKInitialized() else {
onFailure?("SDK not initialized", nil)
return
}
implementation.registerLiveActivityToken(token, activityId: activityId, onSuccess: onSuccess, onFailure: onFailure)
}

@objc(pauseAuthRetries:)
public static func pauseAuthRetries(_ pauseRetry: Bool) {
implementation?.authManager.pauseAuthRetries(pauseRetry)
Expand Down
Loading
Loading