diff --git a/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard b/Apps/APN-UIKit/APN UIKit/Base.lproj/Main.storyboard index 3d95f3091..11237d8f0 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,204 @@ - - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + - - - - - - - + - - - - - - - - - - - - + + + + + - - - + + + - - - - - - - - - - - - - - - - - + + + + + diff --git a/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift b/Apps/APN-UIKit/APN UIKit/View/DashboardViewController.swift index 90f0f5f5b..7ce032606 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,8 @@ class DashboardViewController: BaseViewController { @IBOutlet var versionsLabel: UILabel! @IBOutlet var userInfoLabel: UILabel! @IBOutlet var settings: UIImageView! + @IBOutlet var inlineInAppView: InAppMessageView! + var dashboardRouter: DashboardRouting? var notificationUtil = DIGraphShared.shared.notificationUtil var storage = DIGraphShared.shared.storage @@ -28,6 +31,26 @@ 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 + inlineInAppView.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. + view.addSubview(newInlineViewUsingUIAsCode) + + // We are adding the new View between 2 Buttons in the StackView. No specific reason why, not sure where else to put it. + newInlineViewUsingUIAsCode.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true + newInlineViewUsingUIAsCode.topAnchor.constraint(equalTo: randomEventButton.bottomAnchor).isActive = true + newInlineViewUsingUIAsCode.bottomAnchor.constraint(equalTo: customEventButton.topAnchor).isActive = true + newInlineViewUsingUIAsCode.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + newInlineViewUsingUIAsCode.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + newInlineViewUsingUIAsCode.centerXAnchor.constraint(equalTo: randomEventButton.centerXAnchor).isActive = true + } + configureDashboardRouter() addNotifierObserver() addUserInteractionToImageViews() diff --git a/Sources/MessagingInApp/Gist/Gist.swift b/Sources/MessagingInApp/Gist/Gist.swift index de1818a93..1a1146504 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 187ba88a3..ff0170b78 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() // sourcery:Name=setupSkipQueueCheck // sourcery:DuplicateMethod=setup @@ -15,8 +16,9 @@ protocol MessageQueueManager: AutoMockable { } // sourcery: InjectRegisterShared = "MessageQueueManager" +// sourcery: InjectSingleton class MessageQueueManagerImpl: MessageQueueManager { - var interval: Double = 600 + 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. var localMessageStore: [String: Message] = [:] @@ -28,6 +30,14 @@ class MessageQueueManagerImpl: MessageQueueManager { setup(skipQueueCheck: false) } + 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 b601ce1ae..185c74a50 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) { @@ -53,11 +66,28 @@ public class InAppMessageView: UIView { // An element id will not be set yet. No need to check for messages to display. } - private func setupView() {} + private func setupView() { + // Enable auto layout constraints + translatesAutoresizingMaskIntoConstraints = false + + // 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 } @@ -68,7 +98,6 @@ public class InAppMessageView: UIView { } displayInAppMessage(messageToDisplay) - // } } private func displayInAppMessage(_ message: Message) { @@ -89,6 +118,15 @@ public class InAppMessageView: UIView { webView.delegate = self addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + // Setup the WebView to be the same size as this View. When this View changes size, the WebView will change, too. + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: topAnchor), + webView.leadingAnchor.constraint(equalTo: leadingAnchor), + webView.trailingAnchor.constraint(equalTo: trailingAnchor), + webView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + inlineMessageManager = newInlineMessageManager } } @@ -99,7 +137,9 @@ extension InAppMessageView: GistViewDelegate { // When this function is called, it's an indicator that the web content has been loaded into the WebView. Report to the manager that we are done loading the message. inlineMessageManager?.doneLoadingMessage() - // 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 } public func action(message: Message, currentRoute: String, action: String, name: String) {} 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 5c6ca190f..727a87067 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 mockCalled = false // do last as resetting properties above can make this true @@ -562,6 +532,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.