diff --git a/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard b/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard index 3d95f3091..f0ae261fa 100644 --- a/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard +++ b/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -153,161 +153,206 @@ - - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + - - - - - - - + - - - - - - - - - - - - + + + + + - - - + + + - - - - - - - - - - - - - - - - - + + + + + + diff --git a/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift b/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift index 90f0f5f5b..26875e078 100644 --- a/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift +++ b/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift @@ -1,4 +1,5 @@ import CioDataPipelines +import CioMessagingInApp import UIKit class DashboardViewController: BaseViewController { @@ -15,6 +16,9 @@ class DashboardViewController: BaseViewController { @IBOutlet var versionsLabel: UILabel! @IBOutlet var userInfoLabel: UILabel! @IBOutlet var settings: UIImageView! + @IBOutlet var inlineInAppViewCreatedInStoryboard: InAppMessageView! + @IBOutlet var buttonStackView: UIStackView! + var dashboardRouter: DashboardRouting? var notificationUtil = DIGraphShared.shared.notificationUtil var storage = DIGraphShared.shared.storage @@ -28,6 +32,23 @@ class DashboardViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + + // In a future PR, we will remove the asyncAfter(). this is only for testing in sample apps because when app opens, the local queue is empty. so wait to check messages until first fetch is done. + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [self] in + inlineInAppViewCreatedInStoryboard.elementId = "dashboard-announcement" + + // We want to test that Inline Views can be used by customers who prefer to use code to make the UI. + // Construct a new instance of the View, add it to the ViewController, then set constraints to make it visible. + let newInlineViewUsingUIAsCode = InAppMessageView(elementId: "dashboard-announcement-code") + // Add the View to the screen. + // It's important that we test inline Views that are nested in a UIStackView. See comments in inline View code to learn more. + buttonStackView.addArrangedSubview(newInlineViewUsingUIAsCode) + + // Customers are responsible for setting the width of the View. + newInlineViewUsingUIAsCode.translatesAutoresizingMaskIntoConstraints = false + newInlineViewUsingUIAsCode.widthAnchor.constraint(equalTo: buttonStackView.widthAnchor).isActive = true + } + configureDashboardRouter() addNotifierObserver() addUserInteractionToImageViews() diff --git a/Sources/MessagingInApp/Gist/Gist.swift b/Sources/MessagingInApp/Gist/Gist.swift index ada5053af..bc9294fbb 100644 --- a/Sources/MessagingInApp/Gist/Gist.swift +++ b/Sources/MessagingInApp/Gist/Gist.swift @@ -8,8 +8,11 @@ protocol GistInstance: AutoMockable { } public class Gist: GistInstance, GistDelegate { - var messageQueueManager: MessageQueueManager = DIGraphShared.shared.messageQueueManager var shownModalMessageQueueIds: Set = [] // all modal messages that have been shown in the app already. + var messageQueueManager: MessageQueueManager { + DIGraphShared.shared.messageQueueManager + } + private var messageManagers: [ModalMessageManager] = [] public var siteId: String = "" public var dataCenter: String = "" diff --git a/Sources/MessagingInApp/Gist/Managers/MessageQueueManager.swift b/Sources/MessagingInApp/Gist/Managers/MessageQueueManager.swift index 385972611..4fa4c68b7 100644 --- a/Sources/MessagingInApp/Gist/Managers/MessageQueueManager.swift +++ b/Sources/MessagingInApp/Gist/Managers/MessageQueueManager.swift @@ -3,7 +3,8 @@ import Foundation import UIKit protocol MessageQueueManager: AutoMockable { - var interval: Double { get set } + func getInterval() -> Double + func setInterval(_ newInterval: Double) func setup(skipQueueCheck: Bool) func fetchUserMessagesFromLocalStore() func removeMessageFromLocalStore(message: Message) @@ -12,8 +13,9 @@ protocol MessageQueueManager: AutoMockable { } // sourcery: InjectRegisterShared = "MessageQueueManager" +// sourcery: InjectSingleton class MessageQueueManagerImpl: MessageQueueManager { - @Atomic var interval: Double = 600 + @Atomic private var interval: Double = 600 private var queueTimer: Timer? // The local message store is used to keep messages that can't be displayed because the route rule doesnt match and inline messages. @@ -23,6 +25,14 @@ class MessageQueueManagerImpl: MessageQueueManager { DIGraphShared.shared.gist } + func getInterval() -> Double { + interval + } + + func setInterval(_ newInterval: Double) { + interval = newInterval + } + func setup(skipQueueCheck: Bool) { queueTimer?.invalidate() queueTimer = nil diff --git a/Sources/MessagingInApp/Gist/Managers/QueueManager.swift b/Sources/MessagingInApp/Gist/Managers/QueueManager.swift index 69be30d94..452ff69e7 100644 --- a/Sources/MessagingInApp/Gist/Managers/QueueManager.swift +++ b/Sources/MessagingInApp/Gist/Managers/QueueManager.swift @@ -1,3 +1,4 @@ +import CioInternalCommon import Foundation class QueueManager { @@ -55,10 +56,10 @@ class QueueManager { private func updatePollingInterval(headers: [AnyHashable: Any]) { if let newPollingIntervalString = headers["x-gist-queue-polling-interval"] as? String, let newPollingInterval = Double(newPollingIntervalString), - newPollingInterval != Gist.shared.messageQueueManager.interval { + newPollingInterval != DIGraphShared.shared.messageQueueManager.getInterval() { DispatchQueue.main.async { - Gist.shared.messageQueueManager.interval = newPollingInterval - Gist.shared.messageQueueManager.setup(skipQueueCheck: true) + DIGraphShared.shared.messageQueueManager.setInterval(newPollingInterval) + DIGraphShared.shared.messageQueueManager.setup(skipQueueCheck: true) Logger.instance.info(message: "Polling interval changed to: \(newPollingInterval) seconds") } } diff --git a/Sources/MessagingInApp/Views/InAppMessageView.swift b/Sources/MessagingInApp/Views/InAppMessageView.swift index 324702b85..468acd401 100644 --- a/Sources/MessagingInApp/Views/InAppMessageView.swift +++ b/Sources/MessagingInApp/Views/InAppMessageView.swift @@ -34,6 +34,19 @@ public class InAppMessageView: UIView { } } + var heightConstraint: NSLayoutConstraint! + + // Get the View's current height or change the height by setting a new value. + private var viewHeight: CGFloat { + get { + heightConstraint.constant + } + set { + heightConstraint.constant = newValue + layoutIfNeeded() + } + } + private var inlineMessageManager: InlineMessageManager? public init(elementId: String) { @@ -54,12 +67,24 @@ public class InAppMessageView: UIView { } private func setupView() { - // next, configure the View such as setting the position and size. This will come in a future change. + // Remove any existing height constraints added by customer. + // This is required as only 1 height constraint can be active at a time. Our height constraint will be ignored + // if we do not do this. + for existingViewConstraint in constraints where existingViewConstraint.firstAnchor == heightAnchor { + existingViewConstraint.isActive = false + } + + // Create a view constraint for the height of the View. + // This allows us to dynamically update the height at a later time. + // + // Set the initial height of the view to 0 so it's not visible. + heightConstraint = heightAnchor.constraint(equalToConstant: 0) + heightConstraint.priority = .required + heightConstraint.isActive = true + layoutIfNeeded() } private func checkIfMessageAvailableToDisplay() { - // In a future PR, we will remove the asyncAfter(). this is only for testing in sample apps because when app opens, the local queue is empty. so wait to check messages until first fetch is done. -// DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [self] in guard let elementId = elementId else { return } @@ -88,6 +113,15 @@ public class InAppMessageView: UIView { } addSubview(inlineView) + // Setup the WebView to be the same size as this View. When this View changes size, the WebView will change, too. + inlineView.translatesAutoresizingMaskIntoConstraints = false // Required in order for this inline View to have full control over the AutoLayout constraints for the WebView. + NSLayoutConstraint.activate([ + inlineView.topAnchor.constraint(equalTo: topAnchor), + inlineView.leadingAnchor.constraint(equalTo: leadingAnchor), + inlineView.trailingAnchor.constraint(equalTo: trailingAnchor), + inlineView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + inlineMessageManager = newInlineMessageManager } } @@ -95,6 +129,8 @@ public class InAppMessageView: UIView { extension InAppMessageView: InlineMessageManagerDelegate { // This function is called by WebView when the content's size changes. public func sizeChanged(width: CGFloat, height: CGFloat) { - // In a future commit, we will change the height of the View to display the web content. + // We keep the width the same to what the customer set it as. + // Update the height to match the aspect ratio of the web content. + viewHeight = height } } diff --git a/Sources/MessagingInApp/autogenerated/AutoDependencyInjection.generated.swift b/Sources/MessagingInApp/autogenerated/AutoDependencyInjection.generated.swift index b97712750..41c7d91e6 100644 --- a/Sources/MessagingInApp/autogenerated/AutoDependencyInjection.generated.swift +++ b/Sources/MessagingInApp/autogenerated/AutoDependencyInjection.generated.swift @@ -74,13 +74,27 @@ extension DIGraphShared { GistInAppProvider() } - // MessageQueueManager + // MessageQueueManager (singleton) var messageQueueManager: MessageQueueManager { getOverriddenInstance() ?? - newMessageQueueManager + sharedMessageQueueManager } - private var newMessageQueueManager: MessageQueueManager { + var sharedMessageQueueManager: MessageQueueManager { + // Use a DispatchQueue to make singleton thread safe. You must create unique dispatchqueues instead of using 1 shared one or you will get a crash when trying + // to call DispatchQueue.sync{} while already inside another DispatchQueue.sync{} call. + DispatchQueue(label: "DIGraphShared_MessageQueueManager_singleton_access").sync { + if let overridenDep: MessageQueueManager = getOverriddenInstance() { + return overridenDep + } + let existingSingletonInstance = self.singletons[String(describing: MessageQueueManager.self)] as? MessageQueueManager + let instance = existingSingletonInstance ?? _get_messageQueueManager() + self.singletons[String(describing: MessageQueueManager.self)] = instance + return instance + } + } + + private func _get_messageQueueManager() -> MessageQueueManager { MessageQueueManagerImpl() } } diff --git a/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift b/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift index 4b68305a6..7b3d50cea 100644 --- a/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift @@ -498,45 +498,15 @@ class MessageQueueManagerMock: MessageQueueManager, Mock { Mocks.shared.add(mock: self) } - /** - When setter of the property called, the value given to setter is set here. - When the getter of the property called, the value set here will be returned. Your chance to mock the property. - */ - var underlyingInterval: Double! - /// `true` if the getter or setter of property is called at least once. - var intervalCalled: Bool { - intervalGetCalled || intervalSetCalled - } - - /// `true` if the getter called on the property at least once. - var intervalGetCalled: Bool { - intervalGetCallsCount > 0 - } - - var intervalGetCallsCount = 0 - /// `true` if the setter called on the property at least once. - var intervalSetCalled: Bool { - intervalSetCallsCount > 0 - } + public func resetMock() { + getIntervalCallsCount = 0 - var intervalSetCallsCount = 0 - /// The mocked property with a getter and setter. - var interval: Double { - get { - mockCalled = true - intervalGetCallsCount += 1 - return underlyingInterval - } - set(value) { - mockCalled = true - intervalSetCallsCount += 1 - underlyingInterval = value - } - } + mockCalled = false // do last as resetting properties above can make this true + setIntervalCallsCount = 0 + setIntervalReceivedArguments = nil + setIntervalReceivedInvocations = [] - public func resetMock() { - intervalGetCallsCount = 0 - intervalSetCallsCount = 0 + mockCalled = false // do last as resetting properties above can make this true setupCallsCount = 0 setupReceivedArguments = nil setupReceivedInvocations = [] @@ -560,6 +530,58 @@ class MessageQueueManagerMock: MessageQueueManager, Mock { mockCalled = false // do last as resetting properties above can make this true } + // MARK: - getInterval + + /// Number of times the function was called. + @Atomic private(set) var getIntervalCallsCount = 0 + /// `true` if the function was ever called. + var getIntervalCalled: Bool { + getIntervalCallsCount > 0 + } + + /// Value to return from the mocked function. + var getIntervalReturnValue: Double! + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + The closure has first priority to return a value for the mocked function. If the closure returns `nil`, + then the mock will attempt to return the value for `getIntervalReturnValue` + */ + var getIntervalClosure: (() -> Double)? + + /// Mocked function for `getInterval()`. Your opportunity to return a mocked value and check result of mock in test code. + func getInterval() -> Double { + mockCalled = true + getIntervalCallsCount += 1 + return getIntervalClosure.map { $0() } ?? getIntervalReturnValue + } + + // MARK: - setInterval + + /// Number of times the function was called. + @Atomic private(set) var setIntervalCallsCount = 0 + /// `true` if the function was ever called. + var setIntervalCalled: Bool { + setIntervalCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + @Atomic private(set) var setIntervalReceivedArguments: Double? + /// Arguments from *all* of the times that the function was called. + @Atomic private(set) var setIntervalReceivedInvocations: [Double] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + var setIntervalClosure: ((Double) -> Void)? + + /// Mocked function for `setInterval(_ newInterval: Double)`. Your opportunity to return a mocked value and check result of mock in test code. + func setInterval(_ newInterval: Double) { + mockCalled = true + setIntervalCallsCount += 1 + setIntervalReceivedArguments = newInterval + setIntervalReceivedInvocations.append(newInterval) + setIntervalClosure?(newInterval) + } + // MARK: - setup /// Number of times the function was called.