Skip to content
Merged
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
14 changes: 10 additions & 4 deletions REES46/Classes/InAppNotification/view/NotificationWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ public class NotificationWidget: InAppNotificationProtocol {
private var parentViewController: UIViewController
private var snackbar: UIView?
private var popup: Popup?
private var onDismiss: (() -> Void)?

public init(
parentViewController: UIViewController,
popup: Popup? = nil
popup: Popup? = nil,
onDismiss: (() -> Void)? = nil
) {
self.parentViewController = parentViewController
self.popup = popup
self.onDismiss = onDismiss

if let popup = popup {
showPopup(popup)
Expand Down Expand Up @@ -103,7 +106,8 @@ public class NotificationWidget: InAppNotificationProtocol {
imageUrl: imageUrl,
confirmButtonText: confirmButtonText,
dismissButtonText: dismissButtonText,
onConfirmButtonClick: onConfirmButtonClick
onConfirmButtonClick: onConfirmButtonClick,
onDismiss: onDismiss
)
let dialog = AlertDialog(viewModel: viewModel)
dialog.modalPresentationStyle = .overFullScreen
Expand All @@ -124,7 +128,8 @@ public class NotificationWidget: InAppNotificationProtocol {
imageUrl: imageUrl,
confirmButtonText: confirmButtonText,
dismissButtonText: dismissButtonText,
onConfirmButtonClick: onConfirmButtonClick
onConfirmButtonClick: onConfirmButtonClick,
onDismiss: onDismiss
)
let dialog = BottomDialog(viewModel: viewModel)
parentViewController.present(dialog, animated: true, completion: nil)
Expand All @@ -144,7 +149,8 @@ public class NotificationWidget: InAppNotificationProtocol {
imageUrl: imageUrl,
confirmButtonText: confirmButtonText,
dismissButtonText: dismissButtonText,
onConfirmButtonClick: onConfirmButtonClick
onConfirmButtonClick: onConfirmButtonClick,
onDismiss: onDismiss
)
let dialog = TopDialog(viewModel: viewModel)
dialog.modalPresentationStyle = .overFullScreen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,16 @@ class BaseDialog: UIViewController {
}

@objc internal func dismissDialog() {
dismiss(animated: true, completion: nil)
dismiss(animated: true, completion: { [weak self] in
self?.viewModel.onDismiss?()
})
}

@objc func onConfirmButtonTapped() {
viewModel.onConfirmButtonClick?()
dismiss(animated: true, completion: nil)
dismiss(animated: true, completion: { [weak self] in
self?.viewModel.onDismiss?()
})
}

@objc func onDismissButtonTapped() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class DialogViewModel {
buttonState == .noButtons
}
var onConfirmButtonClick: (() -> Void)?
var onDismiss: (() -> Void)?

private var hasConfirmButton: Bool { confirmButtonText != nil }
private var hasDismissButton: Bool { dismissButtonText != nil }
Expand All @@ -26,7 +27,8 @@ public class DialogViewModel {
imageUrl: String,
confirmButtonText: String?,
dismissButtonText: String?,
onConfirmButtonClick: (() -> Void)?
onConfirmButtonClick: (() -> Void)?,
onDismiss: (() -> Void)? = nil
) {
self.titleText = titleText
self.messageText = messageText
Expand All @@ -35,6 +37,7 @@ public class DialogViewModel {
self.dismissButtonText = dismissButtonText
self.isImageContainerHidden = imageUrl.isEmpty
self.onConfirmButtonClick = onConfirmButtonClick
self.onDismiss = onDismiss
}

func determineButtonState() -> ButtonState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class TopDialog: BaseDialog {
UIView.animate(withDuration: AppDimensions.Animation.duration, animations: {
self.view.frame.origin.y = -self.view.frame.height
}, completion: { _ in
self.dismiss(animated: false, completion: nil)
self.dismiss(animated: false, completion: { [weak self] in
self?.viewModel.onDismiss?()
})
})
}
}
142 changes: 142 additions & 0 deletions REES46/Classes/Presentation/PopupPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//
// PopupPresenter.swift
// REES46
//
// Created by REES46
// Copyright (c) 2023. All rights reserved.
//

import UIKit

// MARK: - Popup Presenter Service

public class PopupPresenter {
private weak var sdk: AnyObject? // Use AnyObject to avoid circular dependency
private var currentPopup: NotificationWidget?
private var popupQueue: [Popup] = []
private let serialQueue = DispatchQueue(label: "com.rees46.popup.presenter")

public init(sdk: AnyObject) {
self.sdk = sdk
}

// MARK: - Public Interface

/// Main entry point - call this to present any popup
public func presentPopup(_ popup: Popup) {
serialQueue.async { [weak self] in
guard let self = self else { return }

if self.currentPopup != nil {
// Queue popup if one is already showing
self.popupQueue.append(popup)
} else {
self.showPopupNow(popup)
}
}
}

/// Dismiss the current popup and show the next one in queue
public func dismissCurrentPopup() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.currentPopup = nil

// Present next queued popup if any
self.serialQueue.async {
if let nextPopup = self.popupQueue.first {
self.popupQueue.removeFirst()
self.showPopupNow(nextPopup)
}
}
}
}

// MARK: - Private Methods

private func showPopupNow(_ popup: Popup) {
guard let presentingVC = getPresentingViewController(for: popup) else {
return // No VC available or delegate prevented presentation
}

DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.currentPopup = NotificationWidget(
parentViewController: presentingVC,
popup: popup,
onDismiss: { [weak self] in
self?.dismissCurrentPopup()
}
)
}
}

private func getPresentingViewController(for popup: Popup) -> UIViewController? {
// Fallback chain:

// 1. Check delegate first (if set, delegate has full control)
if let sdk = sdk as? PersonalizationSDK,
let delegate = sdk.popupPresentationDelegate {
return delegate.sdk(sdk, shouldPresentPopup: popup)
}

// 2. Check if auto-presentation is enabled
guard let sdk = sdk as? PersonalizationSDK,
sdk.enableAutoPopupPresentation == true else {
return nil // Auto-presentation disabled, no delegate = no presentation
}

// 3. Check parentViewController (backward compatibility)
if let parentVC = sdk.parentViewController {
return parentVC
}

// 4. Auto-discover from window hierarchy (must be on main thread)
var topVC: UIViewController?
DispatchQueue.main.sync {
topVC = getTopViewController()
}
return topVC
}

private func getTopViewController() -> UIViewController? {
// Get key window's root view controller
if #available(iOS 13.0, *) {
// iOS 13+ approach using UIWindowScene
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
let rootViewController = window.rootViewController else {
return nil
}
return findTopViewController(from: rootViewController)
} else {
// iOS 12 and earlier approach using UIApplication.shared.keyWindow
guard let window = UIApplication.shared.keyWindow,
let rootViewController = window.rootViewController else {
return nil
}
return findTopViewController(from: rootViewController)
}
}

private func findTopViewController(from viewController: UIViewController) -> UIViewController {
// Traverse to find the topmost presented view controller
if let presented = viewController.presentedViewController {
return findTopViewController(from: presented)
}

if let navigationController = viewController as? UINavigationController {
if let visible = navigationController.visibleViewController {
return findTopViewController(from: visible)
}
}

if let tabBarController = viewController as? UITabBarController {
if let selected = tabBarController.selectedViewController {
return findTopViewController(from: selected)
}
}

return viewController
}
}
17 changes: 17 additions & 0 deletions REES46/Classes/Sdk/Delegates/PopupPresentationDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// PopupPresentationDelegate.swift
// REES46
//
// Created by REES46
// Copyright (c) 2023. All rights reserved.
//

import UIKit

// MARK: - Popup Presentation Delegate Protocol

public protocol PopupPresentationDelegate: AnyObject {
/// Called when SDK has a popup to present
/// Return presenting UIViewController to allow SDK to present, or nil to prevent presentation
func sdk(_ sdk: PersonalizationSDK, shouldPresentPopup popup: Popup) -> UIViewController?
}
27 changes: 18 additions & 9 deletions REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import AppTrackingTransparency

public var global_EL: Bool = true

// MARK: - SimplePersonalizationSDK

class SimplePersonalizationSDK: PersonalizationSDK {
private var global_EL: Bool = false

var parentViewController: UIViewController?
var notificationWidget: NotificationWidget?

weak var popupPresentationDelegate: PopupPresentationDelegate?
var enableAutoPopupPresentation: Bool

var storiesCode: String?
var shopId: String
var deviceId: String
Expand Down Expand Up @@ -74,6 +79,10 @@ class SimplePersonalizationSDK: PersonalizationSDK {
)
}()

lazy var popupPresenter: PopupPresenter = {
return PopupPresenter(sdk: self)
}()

init(
shopId: String,
userEmail: String? = nil,
Expand All @@ -84,13 +93,15 @@ class SimplePersonalizationSDK: PersonalizationSDK {
enableLogs: Bool = false,
autoSendPushToken: Bool = true,
sendAdvertisingId: Bool = false,
parentViewController: UIViewController?,
parentViewController: UIViewController? = nil,
enableAutoPopupPresentation: Bool = true,
needReInitialization: Bool = false,
completion: ((SdkError?) -> Void)? = nil
) {
self.shopId = shopId
self.autoSendPushToken = autoSendPushToken
self.parentViewController = parentViewController
self.enableAutoPopupPresentation = enableAutoPopupPresentation

global_EL = enableLogs
self.baseURL = "https://" + apiDomain + "/"
Expand Down Expand Up @@ -118,13 +129,8 @@ class SimplePersonalizationSDK: PersonalizationSDK {
self.userSeance = response.seance
self.deviceId = response.deviceId

if let popup = response.popup, let parentViewController {
DispatchQueue.main.async {
self.notificationWidget = NotificationWidget(
parentViewController: parentViewController,
popup: popup
)
}
if let popup = response.popup {
self.popupPresenter.presentPopup(popup)
}

// Handle push token if autoSendPushToken is true
Expand Down Expand Up @@ -162,9 +168,12 @@ class SimplePersonalizationSDK: PersonalizationSDK {
return deviceId
}

@available(*, deprecated, message: "Use enableAutoPopupPresentation or popupPresentationDelegate instead")
func setParentViewController(controller: UIViewController, completion: @escaping () -> Void) {
self.parentViewController = controller
completion()
DispatchQueue.main.async {
completion()
}
}

func getNotificationWidget() -> NotificationWidget? {
Expand Down
10 changes: 10 additions & 0 deletions REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import Foundation
import UIKit

// MARK: - Main SDK Protocol

public protocol PersonalizationSDK {
var shopId: String { get }
var deviceId: String { get }
Expand All @@ -19,6 +21,11 @@ public protocol PersonalizationSDK {
var parentViewController: UIViewController? {get}
var urlSession: URLSession { get set }

// New popup presentation properties
var popupPresentationDelegate: PopupPresentationDelegate? { get set }
var enableAutoPopupPresentation: Bool { get set }
var popupPresenter: PopupPresenter { get }

func postRequest(path: String, params: [String: Any], completion: @escaping (Result<[String: Any], SdkError>) -> Void)
func getRequest(path: String, params: [String: String], _ isInit: Bool, completion: @escaping (Result<[String: Any], SdkError>) -> Void)
func configureURLSession(configuration: URLSessionConfiguration)
Expand All @@ -34,6 +41,7 @@ public protocol PersonalizationSDK {
func getProductsFromCart(completion: @escaping(Result<[CartItem], SdkError>) -> Void)
func getProductInfo(id: String, completion: @escaping(Result<ProductInfo, SdkError>) -> Void)
func getDeviceId() -> String
@available(*, deprecated, message: "Use enableAutoPopupPresentation or popupPresentationDelegate instead")
func setParentViewController(controller: UIViewController, completion: @escaping () -> Void)
func getNotificationWidget() -> NotificationWidget?
func getSession() -> String
Expand Down Expand Up @@ -320,6 +328,7 @@ public func createPersonalizationSDK(
autoSendPushToken: Bool = true,
sendAdvertisingId: Bool = false,
parentViewController: UIViewController? = nil,
enableAutoPopupPresentation: Bool = true,
needReInitialization: Bool = false,
_ completion: ((SdkError?) -> Void)? = nil
) -> PersonalizationSDK {
Expand All @@ -334,6 +343,7 @@ public func createPersonalizationSDK(
autoSendPushToken: autoSendPushToken,
sendAdvertisingId: sendAdvertisingId,
parentViewController: parentViewController,
enableAutoPopupPresentation: enableAutoPopupPresentation,
needReInitialization: needReInitialization,
completion: completion
)
Expand Down
Loading