From 276de2d336d8c68973bfc884f18e8a7c20f3ac21 Mon Sep 17 00:00:00 2001 From: xyzcancer Date: Thu, 23 Oct 2025 20:22:20 +0400 Subject: [PATCH] added delegate for passing parentViewController and enableAutoPopupPresentation flag --- .../view/NotificationWidget.swift | 14 +- .../component/dialog/base/BaseDialog.swift | 8 +- .../dialog/base/DialogViewModel.swift | 5 +- .../component/dialog/types/TopDialog.swift | 4 +- .../Classes/Presentation/PopupPresenter.swift | 142 ++++++++++++++++++ .../Delegates/PopupPresentationDelegate.swift | 17 +++ .../Sdk/impl/SimplePersonalizationSDK.swift | 27 ++-- .../PersonalizationSDK.protocol.swift | 10 ++ .../Service/impl/TrackServiceImpl.swift | 16 +- 9 files changed, 216 insertions(+), 27 deletions(-) create mode 100644 REES46/Classes/Presentation/PopupPresenter.swift create mode 100644 REES46/Classes/Sdk/Delegates/PopupPresentationDelegate.swift diff --git a/REES46/Classes/InAppNotification/view/NotificationWidget.swift b/REES46/Classes/InAppNotification/view/NotificationWidget.swift index d07ee6b0..4fd1c1ce 100644 --- a/REES46/Classes/InAppNotification/view/NotificationWidget.swift +++ b/REES46/Classes/InAppNotification/view/NotificationWidget.swift @@ -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) @@ -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 @@ -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) @@ -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 diff --git a/REES46/Classes/InAppNotification/view/component/dialog/base/BaseDialog.swift b/REES46/Classes/InAppNotification/view/component/dialog/base/BaseDialog.swift index 5e9677d5..d2201e07 100644 --- a/REES46/Classes/InAppNotification/view/component/dialog/base/BaseDialog.swift +++ b/REES46/Classes/InAppNotification/view/component/dialog/base/BaseDialog.swift @@ -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() { diff --git a/REES46/Classes/InAppNotification/view/component/dialog/base/DialogViewModel.swift b/REES46/Classes/InAppNotification/view/component/dialog/base/DialogViewModel.swift index be33359a..5c1129c6 100644 --- a/REES46/Classes/InAppNotification/view/component/dialog/base/DialogViewModel.swift +++ b/REES46/Classes/InAppNotification/view/component/dialog/base/DialogViewModel.swift @@ -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 } @@ -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 @@ -35,6 +37,7 @@ public class DialogViewModel { self.dismissButtonText = dismissButtonText self.isImageContainerHidden = imageUrl.isEmpty self.onConfirmButtonClick = onConfirmButtonClick + self.onDismiss = onDismiss } func determineButtonState() -> ButtonState { diff --git a/REES46/Classes/InAppNotification/view/component/dialog/types/TopDialog.swift b/REES46/Classes/InAppNotification/view/component/dialog/types/TopDialog.swift index f4e65cdc..6e597fe6 100644 --- a/REES46/Classes/InAppNotification/view/component/dialog/types/TopDialog.swift +++ b/REES46/Classes/InAppNotification/view/component/dialog/types/TopDialog.swift @@ -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?() + }) }) } } diff --git a/REES46/Classes/Presentation/PopupPresenter.swift b/REES46/Classes/Presentation/PopupPresenter.swift new file mode 100644 index 00000000..c8870d5e --- /dev/null +++ b/REES46/Classes/Presentation/PopupPresenter.swift @@ -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 + } +} diff --git a/REES46/Classes/Sdk/Delegates/PopupPresentationDelegate.swift b/REES46/Classes/Sdk/Delegates/PopupPresentationDelegate.swift new file mode 100644 index 00000000..e2693fea --- /dev/null +++ b/REES46/Classes/Sdk/Delegates/PopupPresentationDelegate.swift @@ -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? +} diff --git a/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift b/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift index a91e4834..9f7318ae 100644 --- a/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift +++ b/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift @@ -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 @@ -74,6 +79,10 @@ class SimplePersonalizationSDK: PersonalizationSDK { ) }() + lazy var popupPresenter: PopupPresenter = { + return PopupPresenter(sdk: self) + }() + init( shopId: String, userEmail: String? = nil, @@ -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 + "/" @@ -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 @@ -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? { diff --git a/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift b/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift index 953d1edf..b1ddf7f7 100644 --- a/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift +++ b/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift @@ -9,6 +9,8 @@ import Foundation import UIKit +// MARK: - Main SDK Protocol + public protocol PersonalizationSDK { var shopId: String { get } var deviceId: String { get } @@ -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) @@ -34,6 +41,7 @@ public protocol PersonalizationSDK { func getProductsFromCart(completion: @escaping(Result<[CartItem], SdkError>) -> Void) func getProductInfo(id: String, completion: @escaping(Result) -> 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 @@ -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 { @@ -334,6 +343,7 @@ public func createPersonalizationSDK( autoSendPushToken: autoSendPushToken, sendAdvertisingId: sendAdvertisingId, parentViewController: parentViewController, + enableAutoPopupPresentation: enableAutoPopupPresentation, needReInitialization: needReInitialization, completion: completion ) diff --git a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift index c75261c5..3e8b2827 100644 --- a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift +++ b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift @@ -284,16 +284,12 @@ class TrackEventServiceImpl: TrackEventServiceProtocol { } private func showPopup(jsonResult: [String: Any]) { - let popup = Popup(json: jsonResult["popup"] as? [String: Any] ?? [:]) - if let parentViewController = sdk?.parentViewController { - DispatchQueue.main.async { - self.notificationWidget = NotificationWidget( - parentViewController: parentViewController, - popup: popup - ) - } - } - } + guard let popupData = jsonResult["popup"] as? [String: Any], !popupData.isEmpty else { + return + } + let popup = Popup(json: popupData) + sdk?.popupPresenter.presentPopup(popup) + } } class TrackSourceServiceImpl: TrackSourceServiceProtocol {