diff --git a/DemoApp/REES46Demo/MainViewController.swift b/DemoApp/REES46Demo/MainViewController.swift index 76adb536..e5f76b16 100644 --- a/DemoApp/REES46Demo/MainViewController.swift +++ b/DemoApp/REES46Demo/MainViewController.swift @@ -25,6 +25,7 @@ class MainViewController: UIViewController, UIScrollViewDelegate { @IBOutlet private weak var resetDidButton: UIButton! @IBOutlet private weak var showStoriesButton: UIButton! @IBOutlet private weak var showSnackBarButton: UIButton! + private var showTestPopupButton: UIButton! public var waitIndicator: SdkActivityIndicator! @@ -188,6 +189,48 @@ class MainViewController: UIViewController, UIScrollViewDelegate { storiesCollectionView.showStories() } + @objc + private func showTestPopup() { + guard let sdk = globalSDK else { + return + } + + let componentsDict: [String: Any] = [ + "header": "Test Popup", + "text": "This is a test popup for iOS SDK" + ] + + let componentsJSON: String + if let componentsData = try? JSONSerialization.data(withJSONObject: componentsDict), + let componentsString = String(data: componentsData, encoding: .utf8) { + componentsJSON = componentsString + } else { + componentsJSON = "{}" + } + + let testPopupData: [String: Any] = [ + "id": 999, + "channels": ["email"], + "position": "centered", + "delay": 0, + "html": """ + + + """, + "components": componentsJSON, + "web_push_system": false, + "popup_actions": "{}" + ] + + let testPopup = Popup(json: testPopupData) + + sdk.popupPresenter.dismissCurrentPopup() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + sdk.popupPresenter.presentPopup(testPopup) + } + } + func setupSdkDemoAppViews() { navigationController?.navigationBar.isHidden = true scrollView.contentSize = CGSize(width: UIScreen.main.bounds.size.width, height: 2000) @@ -199,6 +242,8 @@ class MainViewController: UIViewController, UIScrollViewDelegate { resetDidButton.addTarget(self, action: #selector(didTapReset), for: .touchUpInside) showStoriesButton.addTarget(self, action: #selector(showStories), for: .touchUpInside) + setupTestPopupButton() + fontInterPreload() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.setupSdkLabels() @@ -231,6 +276,24 @@ class MainViewController: UIViewController, UIScrollViewDelegate { self.waitIndicator.hideIndicatorWhenStopped = true } + func setupTestPopupButton() { + showTestPopupButton = DemoShopButton(type: .system) + showTestPopupButton.setTitle("Show Test Popup", for: .normal) + showTestPopupButton.setTitleColor(.white, for: .normal) + showTestPopupButton.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(showTestPopupButton) + + // Place button next to other test buttons + NSLayoutConstraint.activate([ + showTestPopupButton.topAnchor.constraint(equalTo: showStoriesButton.bottomAnchor, constant: 10), + showTestPopupButton.leadingAnchor.constraint(equalTo: showStoriesButton.leadingAnchor), + showTestPopupButton.widthAnchor.constraint(equalTo: showStoriesButton.widthAnchor), + showTestPopupButton.heightAnchor.constraint(equalTo: showStoriesButton.heightAnchor) + ]) + + showTestPopupButton.addTarget(self, action: #selector(showTestPopup), for: .touchUpInside) + } + func fontInterPreload() { fcmTokenLabel.font = SdkDynamicFont.dynamicFont(textStyle: .headline, weight: .bold) pushTokenLabel .font = SdkDynamicFont.dynamicFont(textStyle: .headline, weight: .bold) diff --git a/DemoApp/Rees46DemoTests/SearchServiceTests.swift b/DemoApp/Rees46DemoTests/SearchServiceTests.swift index 2662c1d3..370a1108 100644 --- a/DemoApp/Rees46DemoTests/SearchServiceTests.swift +++ b/DemoApp/Rees46DemoTests/SearchServiceTests.swift @@ -6,17 +6,11 @@ class SearchServiceImplTests: XCTestCase { var sdk: PersonalizationSDK! var shopId: String { - guard let value = ProcessInfo.processInfo.environment[Constants.testShopIdKey] else { - fatalError("Environment variable TEST_SHOP_ID is not set") - } - return value + return ProcessInfo.processInfo.environment[Constants.testShopIdKey] ?? Constants.testShopId } var apiDomain: String { - guard let value = ProcessInfo.processInfo.environment[Constants.testApiUrlKey] else { - fatalError("Environment variable TEST_API_URL is not set") - } - return value + return ProcessInfo.processInfo.environment[Constants.testApiUrlKey] ?? Constants.testApiDomain } override func setUp() { diff --git a/REES46/Classes/Presentation/PopupPresenter.swift b/REES46/Classes/Presentation/PopupPresenter.swift index c8870d5e..1a52602f 100644 --- a/REES46/Classes/Presentation/PopupPresenter.swift +++ b/REES46/Classes/Presentation/PopupPresenter.swift @@ -15,6 +15,7 @@ public class PopupPresenter { private var currentPopup: NotificationWidget? private var popupQueue: [Popup] = [] private let serialQueue = DispatchQueue(label: "com.rees46.popup.presenter") + private var popupShownFlags: [Int: Date] = [:] public init(sdk: AnyObject) { self.sdk = sdk @@ -55,6 +56,14 @@ public class PopupPresenter { // MARK: - Private Methods private func showPopupNow(_ popup: Popup) { + // Check if popup was shown in the last 60 seconds + if let shownDate = popupShownFlags[popup.id] { + let timeSinceShown = Date().timeIntervalSince(shownDate) + if timeSinceShown < 60 { + return // Popup was already shown, skip + } + } + guard let presentingVC = getPresentingViewController(for: popup) else { return // No VC available or delegate prevented presentation } @@ -68,6 +77,21 @@ public class PopupPresenter { self?.dismissCurrentPopup() } ) + + // Store popup shown flag in memory for 60 seconds + self.popupShownFlags[popup.id] = Date() + + // Remove flag after 60 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self] in + self?.popupShownFlags.removeValue(forKey: popup.id) + } + + // Send popup shown event to server + if let sdk = self.sdk as? PersonalizationSDK { + sdk.trackPopupShown(popupId: popup.id) { _ in + // Handle result (log if needed) + } + } } } diff --git a/REES46/Classes/Sdk/Extensions/Popup.extension.swift b/REES46/Classes/Sdk/Extensions/Popup.extension.swift index ee517ff2..3d771fad 100644 --- a/REES46/Classes/Sdk/Extensions/Popup.extension.swift +++ b/REES46/Classes/Sdk/Extensions/Popup.extension.swift @@ -1,6 +1,6 @@ import Foundation extension Popup { - init(json: [String: Any]) { + public init(json: [String: Any]) { self.id = json["id"] as? Int ?? 0 self.channels = json["channels"] as? [String] ?? [] self.position = json["position"] as? String ?? "" @@ -17,7 +17,7 @@ extension Popup { } } - func extractTitleAndSubtitle() -> (title: String?, subTitle: String?) { + public func extractTitleAndSubtitle() -> (title: String?, subTitle: String?) { let title = RegexHelper.extract( using: RegexPattern.title, from: html diff --git a/REES46/Classes/Sdk/Model/Popup.struct.swift b/REES46/Classes/Sdk/Model/Popup.struct.swift index 699d804f..19a2ab34 100644 --- a/REES46/Classes/Sdk/Model/Popup.struct.swift +++ b/REES46/Classes/Sdk/Model/Popup.struct.swift @@ -1,22 +1,22 @@ import Foundation public struct Popup: Codable { - enum Position: String { + public enum Position: String { case centered = "centered" case bottom = "fixed_bottom" case top = "top" } - let id: Int - let channels: [String] - let position: String - let delay: Int - let html: String - let components: PopupComponents? - let web_push_system: Bool - let popup_actions: String + public let id: Int + public let channels: [String] + public let position: String + public let delay: Int + public let html: String + public let components: PopupComponents? + public let web_push_system: Bool + public let popup_actions: String - func getParsedPopupActions() -> PopupActions? { + public func getParsedPopupActions() -> PopupActions? { guard let data = popup_actions.data(using: .utf8) else { return nil } let decoder = JSONDecoder() do { diff --git a/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift b/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift index 9f7318ae..67c6c21d 100644 --- a/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift +++ b/REES46/Classes/Sdk/impl/SimplePersonalizationSDK.swift @@ -408,6 +408,10 @@ class SimplePersonalizationSDK: PersonalizationSDK { trackEventService.trackEvent(event: event, category: category, label: label, value: value, completion: completion) } + func trackPopupShown(popupId: Int, completion: @escaping (Result) -> Void) { + trackEventService.trackPopupShown(popupId: popupId, completion: completion) + } + func trackSource(source: RecommendedByCase, code: String) { trackSourceService.trackSource(source: source, code: code) } @@ -1196,7 +1200,7 @@ class SimplePersonalizationSDK: PersonalizationSDK { } do { if data.isEmpty { - if path.contains("clicked") || path.contains("closed") || path.contains("received") { + if path.contains("clicked") || path.contains("closed") || path.contains("received") || path.contains("showed") { completion(.success([:])) return } diff --git a/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift b/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift index b1ddf7f7..bd3f9dc1 100644 --- a/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift +++ b/REES46/Classes/Sdk/protocol/PersonalizationSDK.protocol.swift @@ -35,6 +35,7 @@ public protocol PersonalizationSDK { func track(event: Event, recommendedBy: RecomendedBy?, completion: @escaping (Result) -> Void) func trackSource(source: RecommendedByCase, code: String) func trackEvent(event: String, category: String?, label: String?, value: Int?, completion: @escaping (Result) -> Void) + func trackPopupShown(popupId: Int, completion: @escaping (Result) -> Void) func recommend(blockId: String, currentProductId: String?, currentCategoryId: String?, locations: String?, imageSize: String?,timeOut: Double?, withLocations: Bool, extended: Bool, completion: @escaping (Result) -> Void) func suggest(query: String, locations: String?, excludedMerchants: [String]?, excludedBrands: [String]?, timeOut: Double?, extended: String?, completion: @escaping (Result) -> Void) func getProductsList(brands: String?, merchants: String?, categories: String?, locations: String?, limit: Int?, page: Int?, filters: [String: Any]?, completion: @escaping(Result) -> Void) diff --git a/REES46/Classes/Subscription/Services/impl/SubscriptionServiceImpl.swift b/REES46/Classes/Subscription/Services/impl/SubscriptionServiceImpl.swift index 705be1b2..dd028920 100644 --- a/REES46/Classes/Subscription/Services/impl/SubscriptionServiceImpl.swift +++ b/REES46/Classes/Subscription/Services/impl/SubscriptionServiceImpl.swift @@ -65,7 +65,12 @@ class SubscriptionServiceImpl: SubscriptionServiceProtocol { sdk.postRequest(path: path, params: params) { result in switch result { - case .success: + case let .success(successResult): + // Check if response contains popup and show it + if let popupData = successResult["popup"] as? [String: Any], !popupData.isEmpty { + let popup = Popup(json: popupData) + sdk.popupPresenter.presentPopup(popup) + } completion(.success(Void())) case let .failure(error): completion(.failure(error)) diff --git a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift index 3e8b2827..35885f8f 100644 --- a/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift +++ b/REES46/Classes/Tracking/Service/impl/TrackServiceImpl.swift @@ -15,6 +15,7 @@ class TrackEventServiceImpl: TrackEventServiceProtocol { private struct Constants { static let trackStoriesPath = "track/stories" static let trackCustomEventPath = "push/custom" + static let popupShownPath = "popup/showed" static let id = "id" static let amount = "amount" @@ -283,6 +284,31 @@ class TrackEventServiceImpl: TrackEventServiceProtocol { } } + func trackPopupShown(popupId: Int, completion: @escaping (Result) -> Void) { + guard let sdk = sdk else { + completion(.failure(.custom(error: "trackPopupShown: SDK is not initialized"))) + return + } + + sessionQueue.addOperation { + let params: [String: Any] = [ + Constants.shopId: sdk.shopId, + Constants.did: sdk.deviceId, + Constants.sid: sdk.userSeance, + "popup": popupId + ] + + sdk.postRequest(path: Constants.popupShownPath, params: params) { result in + switch result { + case .success: + completion(.success(Void())) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + private func showPopup(jsonResult: [String: Any]) { guard let popupData = jsonResult["popup"] as? [String: Any], !popupData.isEmpty else { return diff --git a/REES46/Classes/Tracking/Service/protocol/TrackEventServiceProtocol.swift b/REES46/Classes/Tracking/Service/protocol/TrackEventServiceProtocol.swift index e4d34501..58ed9c4c 100644 --- a/REES46/Classes/Tracking/Service/protocol/TrackEventServiceProtocol.swift +++ b/REES46/Classes/Tracking/Service/protocol/TrackEventServiceProtocol.swift @@ -4,4 +4,5 @@ import Foundation protocol TrackEventServiceProtocol { func track(event: Event, recommendedBy: RecomendedBy?, completion: @escaping (Result) -> Void) func trackEvent(event: String, category: String?, label: String?, value: Int?, completion: @escaping (Result) -> Void) + func trackPopupShown(popupId: Int, completion: @escaping (Result) -> Void) }