From 19e5f982af27b9f72c59a05fcf1f39b75082a217 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 5 May 2023 14:22:00 +0100 Subject: [PATCH 01/16] Remove unneeded ttl property from JITM --- Fakes/Fakes/Networking.generated.swift | 1 - .../Model/Copiable/Models+Copiable.generated.swift | 3 --- Networking/Networking/Model/JustInTimeMessage.swift | 9 --------- .../Mapper/JustInTimeMessageListMapperTests.swift | 2 -- 4 files changed, 15 deletions(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 38e248c6b1e..a092d83f51b 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -339,7 +339,6 @@ extension Networking.JustInTimeMessage { siteID: .fake(), messageID: .fake(), featureClass: .fake(), - ttl: .fake(), content: .fake(), cta: .fake(), assets: .fake() diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 47022c1cc42..7df8a0838fd 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -396,7 +396,6 @@ extension Networking.JustInTimeMessage { siteID: CopiableProp = .copy, messageID: CopiableProp = .copy, featureClass: CopiableProp = .copy, - ttl: CopiableProp = .copy, content: CopiableProp = .copy, cta: CopiableProp = .copy, assets: CopiableProp<[String: URL]> = .copy @@ -404,7 +403,6 @@ extension Networking.JustInTimeMessage { let siteID = siteID ?? self.siteID let messageID = messageID ?? self.messageID let featureClass = featureClass ?? self.featureClass - let ttl = ttl ?? self.ttl let content = content ?? self.content let cta = cta ?? self.cta let assets = assets ?? self.assets @@ -413,7 +411,6 @@ extension Networking.JustInTimeMessage { siteID: siteID, messageID: messageID, featureClass: featureClass, - ttl: ttl, content: content, cta: cta, assets: assets diff --git a/Networking/Networking/Model/JustInTimeMessage.swift b/Networking/Networking/Model/JustInTimeMessage.swift index f728d3ead3e..258d9fea63b 100644 --- a/Networking/Networking/Model/JustInTimeMessage.swift +++ b/Networking/Networking/Model/JustInTimeMessage.swift @@ -18,10 +18,6 @@ public struct JustInTimeMessage: GeneratedCopiable, GeneratedFakeable, Equatable /// public let featureClass: String - /// TTL, or Time To Live: validity of the JITM's client-side dismissal in seconds, only relevant after dismissal. - /// - public let ttl: Int64 - /// Content of the JITM: in particular, the title and description of the message /// public let content: Content @@ -37,14 +33,12 @@ public struct JustInTimeMessage: GeneratedCopiable, GeneratedFakeable, Equatable public init(siteID: Int64, messageID: String, featureClass: String, - ttl: Int64, content: JustInTimeMessage.Content, cta: JustInTimeMessage.CTA, assets: [String: URL]) { self.siteID = siteID self.messageID = messageID self.featureClass = featureClass - self.ttl = ttl self.content = content self.cta = cta self.assets = assets @@ -55,7 +49,6 @@ extension JustInTimeMessage: Codable { enum CodingKeys: String, CodingKey { case messageID = "id" case featureClass = "feature_class" - case ttl case content case cta = "CTA" case assets @@ -71,7 +64,6 @@ extension JustInTimeMessage: Codable { self.siteID = siteID self.messageID = try container.decode(String.self, forKey: JustInTimeMessage.CodingKeys.messageID) self.featureClass = try container.decode(String.self, forKey: JustInTimeMessage.CodingKeys.featureClass) - self.ttl = try container.decode(Int64.self, forKey: JustInTimeMessage.CodingKeys.ttl) self.content = try container.decode(JustInTimeMessage.Content.self, forKey: JustInTimeMessage.CodingKeys.content) self.cta = try container.decode(JustInTimeMessage.CTA.self, forKey: JustInTimeMessage.CodingKeys.cta) self.assets = try container.decodeIfPresent([String: URL].self, forKey: .assets) ?? [:] @@ -82,7 +74,6 @@ extension JustInTimeMessage: Codable { try container.encode(self.messageID, forKey: JustInTimeMessage.CodingKeys.messageID) try container.encode(self.featureClass, forKey: JustInTimeMessage.CodingKeys.featureClass) - try container.encode(self.ttl, forKey: JustInTimeMessage.CodingKeys.ttl) try container.encode(self.content, forKey: JustInTimeMessage.CodingKeys.content) try container.encode(self.cta, forKey: JustInTimeMessage.CodingKeys.cta) try container.encode(self.assets, forKey: .assets) diff --git a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift index d82c4e6821d..6a8133c3136 100644 --- a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift @@ -34,7 +34,6 @@ final class JustInTimeMessageListMapperTests: XCTestCase { siteID: dummySiteID, messageID: "woomobile_ipp_barcode_users", featureClass: "woomobile_ipp", - ttl: 300, content: JustInTimeMessage.Content( message: "In-person card payments", description: "Sell anywhere, and take card payments using a mobile card reader."), @@ -55,7 +54,6 @@ final class JustInTimeMessageListMapperTests: XCTestCase { let expectedJustInTimeMessage = JustInTimeMessage(siteID: dummySiteID, messageID: "woomobile_ipp_barcode_users", featureClass: "woomobile_ipp", - ttl: 300, content: JustInTimeMessage.Content( message: "In-person card payments", description: "Sell anywhere, and take card payments using a mobile card reader."), From 17ae3b2b263223968b79be827c74bf175fd687e3 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 5 May 2023 14:42:54 +0100 Subject: [PATCH 02/16] Parse `template` from JITM API response --- Fakes/Fakes/Networking.generated.swift | 3 ++- .../Model/Copiable/Models+Copiable.generated.swift | 7 +++++-- Networking/Networking/Model/JustInTimeMessage.swift | 9 ++++++++- .../Mapper/JustInTimeMessageListMapperTests.swift | 6 ++++-- .../Responses/just-in-time-message-list.json | 2 +- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index a092d83f51b..943c9933d49 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -341,7 +341,8 @@ extension Networking.JustInTimeMessage { featureClass: .fake(), content: .fake(), cta: .fake(), - assets: .fake() + assets: .fake(), + template: .fake() ) } } diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 7df8a0838fd..c151fe91153 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -398,7 +398,8 @@ extension Networking.JustInTimeMessage { featureClass: CopiableProp = .copy, content: CopiableProp = .copy, cta: CopiableProp = .copy, - assets: CopiableProp<[String: URL]> = .copy + assets: CopiableProp<[String: URL]> = .copy, + template: CopiableProp = .copy ) -> Networking.JustInTimeMessage { let siteID = siteID ?? self.siteID let messageID = messageID ?? self.messageID @@ -406,6 +407,7 @@ extension Networking.JustInTimeMessage { let content = content ?? self.content let cta = cta ?? self.cta let assets = assets ?? self.assets + let template = template ?? self.template return Networking.JustInTimeMessage( siteID: siteID, @@ -413,7 +415,8 @@ extension Networking.JustInTimeMessage { featureClass: featureClass, content: content, cta: cta, - assets: assets + assets: assets, + template: template ) } } diff --git a/Networking/Networking/Model/JustInTimeMessage.swift b/Networking/Networking/Model/JustInTimeMessage.swift index 258d9fea63b..2a75375ef8a 100644 --- a/Networking/Networking/Model/JustInTimeMessage.swift +++ b/Networking/Networking/Model/JustInTimeMessage.swift @@ -30,18 +30,22 @@ public struct JustInTimeMessage: GeneratedCopiable, GeneratedFakeable, Equatable /// public let assets: [String: URL] + public let template: String + public init(siteID: Int64, messageID: String, featureClass: String, content: JustInTimeMessage.Content, cta: JustInTimeMessage.CTA, - assets: [String: URL]) { + assets: [String: URL], + template: String) { self.siteID = siteID self.messageID = messageID self.featureClass = featureClass self.content = content self.cta = cta self.assets = assets + self.template = template } } @@ -52,6 +56,7 @@ extension JustInTimeMessage: Codable { case content case cta = "CTA" case assets + case template } public init(from decoder: Decoder) throws { @@ -67,6 +72,7 @@ extension JustInTimeMessage: Codable { self.content = try container.decode(JustInTimeMessage.Content.self, forKey: JustInTimeMessage.CodingKeys.content) self.cta = try container.decode(JustInTimeMessage.CTA.self, forKey: JustInTimeMessage.CodingKeys.cta) self.assets = try container.decodeIfPresent([String: URL].self, forKey: .assets) ?? [:] + self.template = try container.decodeIfPresent(String.self, forKey: .template) ?? "default" } public func encode(to encoder: Encoder) throws { @@ -77,6 +83,7 @@ extension JustInTimeMessage: Codable { try container.encode(self.content, forKey: JustInTimeMessage.CodingKeys.content) try container.encode(self.cta, forKey: JustInTimeMessage.CodingKeys.cta) try container.encode(self.assets, forKey: .assets) + try container.encode(self.template, forKey: .template) } } diff --git a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift index 6a8133c3136..ef5cd005a4a 100644 --- a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift @@ -40,7 +40,8 @@ final class JustInTimeMessageListMapperTests: XCTestCase { cta: JustInTimeMessage.CTA( message: "Purchase Card Reader", link: "https://woocommerce.com/products/hardware/US"), - assets: ["background_image_url": URL(string: "https://example.net/images/background-light@2x.png")!]) + assets: ["background_image_url": URL(string: "https://example.net/images/background-light@2x.png")!], + template: "modal") assertEqual(expectedJustInTimeMessage, justInTimeMessage) } @@ -60,7 +61,8 @@ final class JustInTimeMessageListMapperTests: XCTestCase { cta: JustInTimeMessage.CTA( message: "Purchase Card Reader", link: "https://woocommerce.com/products/hardware/US"), - assets: [:]) + assets: [:], + template: "default") assertEqual(expectedJustInTimeMessage, justInTimeMessage) } } diff --git a/Networking/NetworkingTests/Responses/just-in-time-message-list.json b/Networking/NetworkingTests/Responses/just-in-time-message-list.json index 02b0b45a803..72f9d87ca24 100644 --- a/Networking/NetworkingTests/Responses/just-in-time-message-list.json +++ b/Networking/NetworkingTests/Responses/just-in-time-message-list.json @@ -18,7 +18,7 @@ "primary": true, "link": "https:\/\/woocommerce.com\/products\/hardware\/US" }, - "template": "default", + "template": "modal", "ttl": 300, "id": "woomobile_ipp_barcode_users", "feature_class": "woomobile_ipp", From 3470b31ae68d986dde3401630c074aefcaf87903 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Tue, 9 May 2023 13:41:47 +0100 Subject: [PATCH 03/16] 9679 Add `template` to JITM display model Also prep JITM view model for multiple-display use --- Fakes/Fakes/Yosemite.generated.swift | 10 ++- .../JustInTimeMessagesProvider.swift | 4 +- ...swift => JustInTimeMessageViewModel.swift} | 81 ++++++++++++------- .../WooCommerce.xcodeproj/project.pbxproj | 16 ++-- ... => JustInTimeMessageViewModelTests.swift} | 6 +- .../Dashboard/DashboardViewModelTests.swift | 2 +- .../Copiable/Models+Copiable.generated.swift | 7 +- .../Yosemite/Model/JustInTimeMessage.swift | 14 +++- .../Stores/JustInTimeMessageStoreTests.swift | 3 +- 9 files changed, 94 insertions(+), 49 deletions(-) rename WooCommerce/Classes/ViewModels/Feature Announcement Cards/{JustInTimeMessageAnnouncementCardViewModel.swift => JustInTimeMessageViewModel.swift} (87%) rename WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/{JustInTimeMessageAnnouncementCardViewModelTests.swift => JustInTimeMessageViewModelTests.swift} (97%) diff --git a/Fakes/Fakes/Yosemite.generated.swift b/Fakes/Fakes/Yosemite.generated.swift index 6385f55ea45..46f4f0daed0 100644 --- a/Fakes/Fakes/Yosemite.generated.swift +++ b/Fakes/Fakes/Yosemite.generated.swift @@ -20,10 +20,18 @@ extension Yosemite.JustInTimeMessage { backgroundImageUrl: .fake(), backgroundImageDarkUrl: .fake(), badgeImageUrl: .fake(), - badgeImageDarkUrl: .fake() + badgeImageDarkUrl: .fake(), + template: .fake() ) } } +extension JustInTimeMessageTemplate { + /// Returns a "ready to use" type filled with fake values. + /// + public static func fake() -> JustInTimeMessageTemplate { + .banner + } +} extension Yosemite.ProductReviewFromNoteParcel { /// Returns a "ready to use" type filled with fake values. /// diff --git a/WooCommerce/Classes/JustInTimeMessages/JustInTimeMessagesProvider.swift b/WooCommerce/Classes/JustInTimeMessages/JustInTimeMessagesProvider.swift index 83ad8d72900..bd77b15d4e7 100644 --- a/WooCommerce/Classes/JustInTimeMessages/JustInTimeMessagesProvider.swift +++ b/WooCommerce/Classes/JustInTimeMessages/JustInTimeMessagesProvider.swift @@ -18,7 +18,7 @@ final class JustInTimeMessagesProvider { self.analytics = analytics } - func loadMessage(for screen: JustInTimeMessagesSourceScreen, siteID: Int64) async throws -> JustInTimeMessageAnnouncementCardViewModel? { + func loadMessage(for screen: JustInTimeMessagesSourceScreen, siteID: Int64) async throws -> JustInTimeMessageViewModel? { guard let source = appScreenJitmSourceMapping[screen] else { DDLogInfo("Could not load JITM for \(screen) because there is no mapping for the given screen") return nil @@ -39,7 +39,7 @@ final class JustInTimeMessagesProvider { .JustInTimeMessage.fetchSuccess(source: source, messageID: message.messageID, count: Int64(messages.count))) - let viewModel = JustInTimeMessageAnnouncementCardViewModel( + let viewModel = JustInTimeMessageViewModel( justInTimeMessage: message, screenName: source, siteID: siteID) diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift similarity index 87% rename from WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift rename to WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift index 02652a7e308..a817ccb1dab 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift @@ -4,7 +4,7 @@ import UIKit import Yosemite import Combine -final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProtocol { +final class JustInTimeMessageViewModel { private let siteID: Int64 private let analytics: Analytics @@ -21,6 +21,8 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode private let justInTimeMessage: JustInTimeMessage // MARK: - Message properties + let template: JustInTimeMessageTemplate + let title: String let message: String @@ -31,6 +33,8 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode let imageDarkUrl: URL? + let badgeType: BadgeView.BadgeType? + private let url: URL? private let messageID: String @@ -59,6 +63,7 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode self.featureClass = justInTimeMessage.featureClass self.justInTimeMessage = justInTimeMessage self.screenName = screenName + self.template = justInTimeMessage.template self.title = justInTimeMessage.title self.message = justInTimeMessage.detail self.buttonTitle = justInTimeMessage.buttonTitle @@ -83,31 +88,9 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode }.store(in: &cancellables) } - // MARK: - default AnnouncementCardViewModelProtocol conformance - let showDividers: Bool = false - - let badgeType: BadgeView.BadgeType? - - let image: UIImage = .paymentsFeatureBannerImage - - var showDismissConfirmation: Bool = false - - let dismissAlertTitle: String = "" - - let dismissAlertMessage: String = "" - - // MARK: - AnnouncementCardViewModelProtocol methods - func onAppear() { - analytics.track(event: .JustInTimeMessage.messageDisplayed(source: screenName, - messageID: messageID, - featureClass: featureClass)) - } - + // MARK: - Actions func ctaTapped() { - analytics.track(event: .JustInTimeMessage.callToActionTapped(source: screenName, - messageID: messageID, - featureClass: featureClass)) - + trackCtaTapped() guard let url = url else { return } @@ -115,10 +98,8 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode urlRouter.handle(url: url) } - func dontShowAgainTapped() { - analytics.track(event: .JustInTimeMessage.dismissTapped(source: screenName, - messageID: messageID, - featureClass: featureClass)) + func dismissTapped() { + trackDismissTapped() let action = JustInTimeMessageAction.dismissMessage(justInTimeMessage, siteID: siteID, completion: { result in @@ -142,6 +123,48 @@ final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewMode stores.dispatch(action) } + // MARK: - Analytics + private func trackMessageDisplayed() { + analytics.track(event: .JustInTimeMessage.messageDisplayed(source: screenName, + messageID: messageID, + featureClass: featureClass)) + } + + private func trackCtaTapped() { + analytics.track(event: .JustInTimeMessage.callToActionTapped(source: screenName, + messageID: messageID, + featureClass: featureClass)) + } + + private func trackDismissTapped() { + analytics.track(event: .JustInTimeMessage.dismissTapped(source: screenName, + messageID: messageID, + featureClass: featureClass)) + } +} + + +extension JustInTimeMessageViewModel: AnnouncementCardViewModelProtocol { + // MARK: - default AnnouncementCardViewModelProtocol conformance + var showDividers: Bool { false } + + var image: UIImage { .paymentsFeatureBannerImage } + + var showDismissConfirmation: Bool { false } + + var dismissAlertTitle: String { "" } + + var dismissAlertMessage: String { "" } + + // MARK: - AnnouncementCardViewModelProtocol methods + func onAppear() { + trackMessageDisplayed() + } + + func dontShowAgainTapped() { + dismissTapped() + } + func remindLaterTapped() { // No-op } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index ad055f5bc40..b27ff4b4405 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -540,7 +540,7 @@ 031B10E3274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */; }; 032E481D2982996E00469D92 /* CardPresentModalBuiltInConnectingFailedNonRetryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E481C2982996E00469D92 /* CardPresentModalBuiltInConnectingFailedNonRetryable.swift */; }; 03582BE2299A9CC8007B7AA3 /* CollectOrderPaymentAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03582BE1299A9CC8007B7AA3 /* CollectOrderPaymentAnalytics.swift */; }; - 035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */; }; + 035BA3A8291000E90056F0AD /* JustInTimeMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageViewModelTests.swift */; }; 035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */; }; 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA44292D0163003E5125 /* CollectOrderPaymentUseCase.swift */; }; 035DBA47292D0995003E5125 /* CardPresentPaymentPreflightController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA46292D0994003E5125 /* CardPresentPaymentPreflightController.swift */; }; @@ -550,7 +550,7 @@ 0365986729AFAEFC00F297D3 /* SetUpTapToPayViewModelsOrderedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0365986629AFAEFC00F297D3 /* SetUpTapToPayViewModelsOrderedList.swift */; }; 0365986929AFB0C100F297D3 /* SetUpTapToPayInformationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0365986829AFB0C100F297D3 /* SetUpTapToPayInformationViewModel.swift */; }; 0365986B29AFB11E00F297D3 /* SetUpTapToPayInformationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0365986A29AFB11E00F297D3 /* SetUpTapToPayInformationViewController.swift */; }; - 0366EAE12909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */; }; + 0366EAE12909A37800B51755 /* JustInTimeMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0366EAE02909A37800B51755 /* JustInTimeMessageViewModel.swift */; }; 036CA6B9291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CA6B8291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift */; }; 036CA6F129229C9E00E4DF4F /* IndefiniteCircularProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036CA6F029229C9E00E4DF4F /* IndefiniteCircularProgressViewStyle.swift */; }; 036F6EA6281847D5006D84F8 /* LegacyPaymentCaptureOrchestratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036F6EA5281847D5006D84F8 /* LegacyPaymentCaptureOrchestratorTests.swift */; }; @@ -2807,7 +2807,7 @@ 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectionFailedUpdateAddress.swift; sourceTree = ""; }; 032E481C2982996E00469D92 /* CardPresentModalBuiltInConnectingFailedNonRetryable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentModalBuiltInConnectingFailedNonRetryable.swift; sourceTree = ""; }; 03582BE1299A9CC8007B7AA3 /* CollectOrderPaymentAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectOrderPaymentAnalytics.swift; sourceTree = ""; }; - 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModelTests.swift; sourceTree = ""; }; + 035BA3A7291000E90056F0AD /* JustInTimeMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageViewModelTests.swift; sourceTree = ""; }; 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateTypeProperty.swift; sourceTree = ""; }; 035DBA44292D0163003E5125 /* CollectOrderPaymentUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectOrderPaymentUseCase.swift; sourceTree = ""; }; 035DBA46292D0994003E5125 /* CardPresentPaymentPreflightController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentPreflightController.swift; sourceTree = ""; }; @@ -2817,7 +2817,7 @@ 0365986629AFAEFC00F297D3 /* SetUpTapToPayViewModelsOrderedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpTapToPayViewModelsOrderedList.swift; sourceTree = ""; }; 0365986829AFB0C100F297D3 /* SetUpTapToPayInformationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpTapToPayInformationViewModel.swift; sourceTree = ""; }; 0365986A29AFB11E00F297D3 /* SetUpTapToPayInformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetUpTapToPayInformationViewController.swift; sourceTree = ""; }; - 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModel.swift; sourceTree = ""; }; + 0366EAE02909A37800B51755 /* JustInTimeMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageViewModel.swift; sourceTree = ""; }; 036CA6B8291E8D4B00E4DF4F /* CardPresentModalPreparingReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalPreparingReader.swift; sourceTree = ""; }; 036CA6F029229C9E00E4DF4F /* IndefiniteCircularProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndefiniteCircularProgressViewStyle.swift; sourceTree = ""; }; 036F6EA5281847D5006D84F8 /* LegacyPaymentCaptureOrchestratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPaymentCaptureOrchestratorTests.swift; sourceTree = ""; }; @@ -5800,7 +5800,7 @@ isa = PBXGroup; children = ( 0371C3672875E47B00277E2C /* FeatureAnnouncementCardViewModel.swift */, - 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */, + 0366EAE02909A37800B51755 /* JustInTimeMessageViewModel.swift */, CCF4346D290AE1F900B4475A /* ProductsOnboardingAnnouncementCardViewModel.swift */, 0371C36D2876E92D00277E2C /* UpsellCardReadersCampaign.swift */, AEE085B42897C871007ACE20 /* LinkedProductsPromoCampaign.swift */, @@ -5812,7 +5812,7 @@ isa = PBXGroup; children = ( 0371C3692876DBCA00277E2C /* FeatureAnnouncementCardViewModelTests.swift */, - 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */, + 035BA3A7291000E90056F0AD /* JustInTimeMessageViewModelTests.swift */, ); path = "Feature Announcement Cards"; sourceTree = ""; @@ -11804,7 +11804,7 @@ 021DD44D286A3A8D004F0468 /* UIViewController+Navigation.swift in Sources */, B958A7CB28B3D4A100823EEF /* RouteMatcher.swift in Sources */, 0279F0E4252DC9670098D7DE /* ProductVariationLoadUseCase.swift in Sources */, - 0366EAE12909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift in Sources */, + 0366EAE12909A37800B51755 /* JustInTimeMessageViewModel.swift in Sources */, CCF87BC02790582500461C43 /* ProductVariationSelector.swift in Sources */, 02CA63DC23D1ADD100BBF148 /* DeviceMediaLibraryPicker.swift in Sources */, 021A84E0257DFC2A00BC71D1 /* RefundShippingLabelViewController.swift in Sources */, @@ -12334,7 +12334,7 @@ B958A7D328B52A2300823EEF /* MockRoute.swift in Sources */, 02153211242376B5003F2BBD /* ProductPriceSettingsViewModelTests.swift in Sources */, 45C8B25D231529410002FA77 /* CustomerInfoTableViewCellTests.swift in Sources */, - 035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */, + 035BA3A8291000E90056F0AD /* JustInTimeMessageViewModelTests.swift in Sources */, 023EC2E624DAB1270021DA91 /* EditableProductVariationModelTests.swift in Sources */, 09BE3A9127C921A70070B69D /* BulkUpdatePriceSettingsViewModelTests.swift in Sources */, 095A077E27CF486C007A61D2 /* ValueOneTableViewCellTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModelTests.swift similarity index 97% rename from WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModelTests.swift rename to WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModelTests.swift index 34c7ac9872e..99f8bbc66fb 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModelTests.swift @@ -7,13 +7,13 @@ import Networking @testable import WooCommerce -final class JustInTimeMessageAnnouncementCardViewModelTests: XCTestCase { +final class JustInTimeMessageViewModelTests: XCTestCase { private var subscriptions = Set() private var webviewPublishes: [WebViewSheetViewModel]! private var analyticsProvider: MockAnalyticsProvider! private var analytics: Analytics! private var stores: MockStoresManager! - private var sut: JustInTimeMessageAnnouncementCardViewModel! + private var sut: JustInTimeMessageViewModel! override func setUp() { subscriptions = Set() @@ -24,7 +24,7 @@ final class JustInTimeMessageAnnouncementCardViewModelTests: XCTestCase { } func setUp(with message: Yosemite.JustInTimeMessage) { - sut = JustInTimeMessageAnnouncementCardViewModel(justInTimeMessage: message, + sut = JustInTimeMessageViewModel(justInTimeMessage: message, screenName: "my_store", siteID: 1234, stores: stores, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift index 7b90a0a256f..882d75de100 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/DashboardViewModelTests.swift @@ -288,7 +288,7 @@ final class DashboardViewModelTests: XCTestCase { prepareStoresToShowJustInTimeMessage(.success([])) let viewModel = DashboardViewModel(siteID: 0, stores: stores, analytics: analytics) - viewModel.announcementViewModel = JustInTimeMessageAnnouncementCardViewModel( + viewModel.announcementViewModel = JustInTimeMessageViewModel( justInTimeMessage: .fake(), screenName: "my_store", siteID: sampleSiteID) diff --git a/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift b/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift index b997e951c4b..0c4268c8572 100644 --- a/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift +++ b/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift @@ -17,7 +17,8 @@ extension Yosemite.JustInTimeMessage { backgroundImageUrl: NullableCopiableProp = .copy, backgroundImageDarkUrl: NullableCopiableProp = .copy, badgeImageUrl: NullableCopiableProp = .copy, - badgeImageDarkUrl: NullableCopiableProp = .copy + badgeImageDarkUrl: NullableCopiableProp = .copy, + template: CopiableProp = .copy ) -> Yosemite.JustInTimeMessage { let siteID = siteID ?? self.siteID let messageID = messageID ?? self.messageID @@ -30,6 +31,7 @@ extension Yosemite.JustInTimeMessage { let backgroundImageDarkUrl = backgroundImageDarkUrl ?? self.backgroundImageDarkUrl let badgeImageUrl = badgeImageUrl ?? self.badgeImageUrl let badgeImageDarkUrl = badgeImageDarkUrl ?? self.badgeImageDarkUrl + let template = template ?? self.template return Yosemite.JustInTimeMessage( siteID: siteID, @@ -42,7 +44,8 @@ extension Yosemite.JustInTimeMessage { backgroundImageUrl: backgroundImageUrl, backgroundImageDarkUrl: backgroundImageDarkUrl, badgeImageUrl: badgeImageUrl, - badgeImageDarkUrl: badgeImageDarkUrl + badgeImageDarkUrl: badgeImageDarkUrl, + template: template ) } } diff --git a/Yosemite/Yosemite/Model/JustInTimeMessage.swift b/Yosemite/Yosemite/Model/JustInTimeMessage.swift index f8a97c28563..0b1c9d5a706 100644 --- a/Yosemite/Yosemite/Model/JustInTimeMessage.swift +++ b/Yosemite/Yosemite/Model/JustInTimeMessage.swift @@ -39,6 +39,8 @@ public struct JustInTimeMessage: GeneratedFakeable, GeneratedCopiable, Equatable public let badgeImageDarkUrl: URL? + public let template: JustInTimeMessageTemplate + public init(siteID: Int64, messageID: String, featureClass: String, @@ -49,7 +51,8 @@ public struct JustInTimeMessage: GeneratedFakeable, GeneratedCopiable, Equatable backgroundImageUrl: URL?, backgroundImageDarkUrl: URL?, badgeImageUrl: URL?, - badgeImageDarkUrl: URL?) { + badgeImageDarkUrl: URL?, + template: JustInTimeMessageTemplate) { self.siteID = siteID self.messageID = messageID self.featureClass = featureClass @@ -61,6 +64,7 @@ public struct JustInTimeMessage: GeneratedFakeable, GeneratedCopiable, Equatable self.backgroundImageDarkUrl = backgroundImageDarkUrl self.badgeImageUrl = badgeImageUrl self.badgeImageDarkUrl = badgeImageDarkUrl + self.template = template } init(message: Networking.JustInTimeMessage) { @@ -74,10 +78,16 @@ public struct JustInTimeMessage: GeneratedFakeable, GeneratedCopiable, Equatable backgroundImageUrl: message.assets[ImageAssetKind.background.baseUrlKey], backgroundImageDarkUrl: message.assets[ImageAssetKind.background.darkUrlKey], badgeImageUrl: message.assets[ImageAssetKind.badge.baseUrlKey], - badgeImageDarkUrl: message.assets[ImageAssetKind.badge.darkUrlKey]) + badgeImageDarkUrl: message.assets[ImageAssetKind.badge.darkUrlKey], + template: JustInTimeMessageTemplate(rawValue: message.template) ?? .banner) } } +public enum JustInTimeMessageTemplate: String, GeneratedFakeable, GeneratedCopiable { + case banner + case modal +} + private extension JustInTimeMessage { enum ImageAssetKind { case background diff --git a/Yosemite/YosemiteTests/Stores/JustInTimeMessageStoreTests.swift b/Yosemite/YosemiteTests/Stores/JustInTimeMessageStoreTests.swift index 2740dc16d4e..b0f5c2ce357 100644 --- a/Yosemite/YosemiteTests/Stores/JustInTimeMessageStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/JustInTimeMessageStoreTests.swift @@ -72,7 +72,8 @@ final class JustInTimeMessageStoreTests: XCTestCase { backgroundImageUrl: URL(string: "https://example.net/images/background-light@2x.png"), backgroundImageDarkUrl: nil, badgeImageUrl: nil, - badgeImageDarkUrl: nil + badgeImageDarkUrl: nil, + template: .modal ) // Then From 2e58417e5d001cba566050a4230877f9669624e5 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 10 May 2023 10:56:45 +0100 Subject: [PATCH 04/16] 9679 Show modal JITM (temp) Temporarily use IPP modals to show a JITM --- .../Dashboard/DashboardViewController.swift | 16 ++++++++++++++++ .../Dashboard/DashboardViewModel.swift | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 451bd14ad8b..d889e1a893a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -160,6 +160,7 @@ final class DashboardViewController: UIViewController { observeBottomJetpackBenefitsBannerVisibilityUpdates() observeStatsVersionForDashboardUIUpdates() observeAnnouncements() + observeModalJustInTimeMessages() observeShowWebViewSheet() observeAddProductTrigger() observeOnboardingVisibility() @@ -527,6 +528,21 @@ private extension DashboardViewController { hostingController.view.layoutIfNeeded() } + private func observeModalJustInTimeMessages() { + viewModel.$modalJustInTimeMessageViewModel.sink { [weak self] viewModel in + guard let viewModel = viewModel else { + return + } + let modalView = CardPresentPaymentsModalViewController( + viewModel: CardPresentModalDisplayMessage(name: viewModel.title, amount: "0", message: viewModel.message)) + Task { @MainActor in + modalView.prepareForCardReaderModalFlow() + self?.present(modalView, animated: true) + } + } + .store(in: &subscriptions) + } + /// Display the error banner at the top of the dashboard content (below the site title) /// func showTopBannerView() { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index f960fe61083..b6bae90e259 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -15,6 +15,8 @@ final class DashboardViewModel { @Published var announcementViewModel: AnnouncementCardViewModelProtocol? = nil + @Published var modalJustInTimeMessageViewModel: JustInTimeMessageViewModel? = nil + let storeOnboardingViewModel: StoreOnboardingViewModel @Published private(set) var showWebViewSheet: WebViewSheetViewModel? = nil @@ -224,7 +226,15 @@ final class DashboardViewModel { let viewModel = try? await justInTimeMessagesManager.loadMessage(for: .dashboard, siteID: siteID) viewModel?.$showWebViewSheet.assign(to: &self.$showWebViewSheet) - announcementViewModel = viewModel + switch viewModel?.template { + case .some(.banner): + announcementViewModel = viewModel + case .some(.modal): + modalJustInTimeMessageViewModel = viewModel + default: + break + } + } /// Sets up observer to decide store onboarding task lists visibility From 8e0733f9996531c9656d53cdd3d610fbf5eb5d88 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 10 May 2023 21:50:21 +0100 Subject: [PATCH 05/16] Move AnimatedPlaceholder to a file --- .../AnimatedPlaceholder.swift | 29 +++++++++++++++++++ .../FeatureAnnouncementCardView.swift | 22 -------------- .../WooCommerce.xcodeproj/project.pbxproj | 4 +++ 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/AnimatedPlaceholder.swift diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/AnimatedPlaceholder.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/AnimatedPlaceholder.swift new file mode 100644 index 00000000000..f1d5ab7fa3e --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/AnimatedPlaceholder.swift @@ -0,0 +1,29 @@ +import SwiftUI +import WordPressUI // For GhostStyle defaults + +struct AnimatedPlaceholder: View { + @State var animate: Bool = false + + var body: some View { + Rectangle() + .fill(animate ? Color(.listForeground(modal: false)) : Color(.ghostCellAnimationEndColor)) + .aspectRatio(Constants.landscapeFourThirds, contentMode: .fit) + .animation(.easeInOut(duration: GhostStyle.Defaults.beatDuration) + .repeatForever(autoreverses: true), value: animate) + .onAppear { + animate.toggle() + } + .padding(Constants.placeholderPadding) + } + + private enum Constants { + static var landscapeFourThirds: CGFloat = 4/3 + static var placeholderPadding: CGFloat = 8 + } +} + +struct AnimatedPlaceholder_Previews: PreviewProvider { + static var previews: some View { + AnimatedPlaceholder(animate: true) + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift index 932578d3c69..01a9b3d286f 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/FeatureAnnouncementCardView.swift @@ -1,5 +1,4 @@ import SwiftUI -import WordPressUI // For GhostStyle defaults struct FeatureAnnouncementCardView: View { private let viewModel: AnnouncementCardViewModelProtocol @@ -141,24 +140,3 @@ extension FeatureAnnouncementCardView { comment: "Alert button text on a feature announcement which prevents the banner being shown again") } } - -fileprivate struct AnimatedPlaceholder: View { - @State var animate: Bool = false - - var body: some View { - Rectangle() - .fill(animate ? Color(.listForeground(modal: false)) : Color(.ghostCellAnimationEndColor)) - .aspectRatio(Constants.landscapeFourThirds, contentMode: .fit) - .animation(.easeInOut(duration: GhostStyle.Defaults.beatDuration) - .repeatForever(autoreverses: true), value: animate) - .onAppear { - animate.toggle() - } - .padding(Constants.placeholderPadding) - } - - private enum Constants { - static var landscapeFourThirds: CGFloat = 4/3 - static var placeholderPadding: CGFloat = 8 - } -} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b27ff4b4405..3987d9c0cd0 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -610,6 +610,7 @@ 03EF250228C615A5006A033E /* InPersonPaymentsMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF250128C615A5006A033E /* InPersonPaymentsMenuViewModel.swift */; }; 03EF250428C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF250328C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift */; }; 03EF250628C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF250528C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift */; }; + 03F5CB832A0C3A1A0026877A /* AnimatedPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */; }; 03FBDA9D263AD49200ACE257 /* CouponListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FBDA9C263AD49100ACE257 /* CouponListViewController.swift */; }; 03FBDAA3263AED2F00ACE257 /* CouponListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03FBDAA2263AED2F00ACE257 /* CouponListViewController.xib */; }; 03FBDAF2263EE47C00ACE257 /* CouponListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FBDAF1263EE47C00ACE257 /* CouponListViewModel.swift */; }; @@ -2877,6 +2878,7 @@ 03EF250128C615A5006A033E /* InPersonPaymentsMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModel.swift; sourceTree = ""; }; 03EF250328C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModelTests.swift; sourceTree = ""; }; 03EF250528C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCardReaderWebViewViewModel.swift; sourceTree = ""; }; + 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedPlaceholder.swift; sourceTree = ""; }; 03FBDA9C263AD49100ACE257 /* CouponListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponListViewController.swift; sourceTree = ""; }; 03FBDAA2263AED2F00ACE257 /* CouponListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CouponListViewController.xib; sourceTree = ""; }; 03FBDAF1263EE47C00ACE257 /* CouponListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponListViewModel.swift; sourceTree = ""; }; @@ -6957,6 +6959,7 @@ 02B2829127C4808D004A332A /* InfiniteScrollIndicator.swift */, DE34771227F174C8009CA300 /* StatusView.swift */, B9E4364B287587D300883CFA /* FeatureAnnouncementCardView.swift */, + 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */, B9E4364D287589E200883CFA /* BadgeView.swift */, 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */, 03076D39290C22BE008EE839 /* WebView.swift */, @@ -11598,6 +11601,7 @@ 0379C51727BFCE9800A7E284 /* WCPayCardBrand+icons.swift in Sources */, B958A7C728B3D44A00823EEF /* UniversalLinkRouter.swift in Sources */, 02E8B17C23E2C78A00A43403 /* ProductImageStatus.swift in Sources */, + 03F5CB832A0C3A1A0026877A /* AnimatedPlaceholder.swift in Sources */, 0259D5FF2581F3FA003B1CD6 /* ShippingLabelPaperSizeOptionsViewController.swift in Sources */, CCEC256A27B581E800EF9FA3 /* ProductVariationFormatter.swift in Sources */, 02EA6BFA2435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift in Sources */, From 033c98cbba1b5bfb07cdbad69a0b538281c2554b Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 10 May 2023 18:12:56 +0100 Subject: [PATCH 06/16] 9679 Add ModalOverlay presentation style --- .../JustInTimeMessageViewModel.swift | 18 +++- .../Dashboard/DashboardViewController.swift | 21 +++- .../JustInTimeMessageModal.swift | 100 ++++++++++++++++++ .../SwiftUI Components/ModalOverlay.swift | 100 ++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 8 ++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift index a817ccb1dab..7490a4cd24a 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift @@ -152,7 +152,14 @@ extension JustInTimeMessageViewModel: AnnouncementCardViewModelProtocol { var showDismissConfirmation: Bool { false } - var dismissAlertTitle: String { "" } + var dismissAlertTitle: String { + switch template { + case .modal: + return Localization.maybeLaterButton + default: + return "" + } + } var dismissAlertMessage: String { "" } @@ -169,3 +176,12 @@ extension JustInTimeMessageViewModel: AnnouncementCardViewModelProtocol { // No-op } } + +private extension JustInTimeMessageViewModel { + enum Localization { + static let maybeLaterButton = NSLocalizedString( + "Maybe Later", + comment: "Dismiss button title for modally presented Just in Time Messages" + ) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index d889e1a893a..12c5bfe1fae 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -104,6 +104,8 @@ final class DashboardViewController: UIViewController { private var announcementView: UIView? + private var modalJustInTimeMessageHostingController: ConstraintsUpdatingHostingController? + /// Onboarding card. private var onboardingHostingController: StoreOnboardingViewHostingController? private var onboardingView: UIView? @@ -533,11 +535,20 @@ private extension DashboardViewController { guard let viewModel = viewModel else { return } - let modalView = CardPresentPaymentsModalViewController( - viewModel: CardPresentModalDisplayMessage(name: viewModel.title, amount: "0", message: viewModel.message)) - Task { @MainActor in - modalView.prepareForCardReaderModalFlow() - self?.present(modalView, animated: true) + + Task { @MainActor [weak self] in + guard let self = self else { return } + let modalController = ConstraintsUpdatingHostingController( + rootView: JustInTimeMessageModal_UIKit( + onDismiss: { + self.dismiss(animated: true) + }, + viewModel: viewModel)) + + self.modalJustInTimeMessageHostingController = modalController + modalController.view.backgroundColor = .clear + modalController.modalPresentationStyle = .overFullScreen + self.present(modalController, animated: true) } } .store(in: &subscriptions) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift new file mode 100644 index 00000000000..6a8aa102249 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift @@ -0,0 +1,100 @@ +import SwiftUI + +struct JustInTimeMessageModal: View { + let viewModel: JustInTimeMessageViewModel + @Binding var isPresented: Bool + + var body: some View { + VStack(spacing: Layout.spacing) { + if let imageUrl = viewModel.imageUrl { + AdaptiveAsyncImage(lightUrl: imageUrl, darkUrl: viewModel.imageDarkUrl, scale: 3) { imagePhase in + switch imagePhase { + case .failure: + Image(uiImage: viewModel.image) + .accessibilityHidden(true) + case .success(let image): + image.resizable() + .scaledToFit() + .accessibilityHidden(true) + case .empty: + AnimatedPlaceholder() + @unknown default: + EmptyView() + } + } + } else { + Image(uiImage: viewModel.image) + .accessibilityHidden(true) + } + + Text(viewModel.title) + .headlineStyle() + + Text(viewModel.message) + .bodyStyle() + .fixedSize(horizontal: false, vertical: true) + + if let buttonTitle = viewModel.buttonTitle { + Button(buttonTitle) { + viewModel.ctaTapped() + } + .buttonStyle(PrimaryButtonStyle()) + .foregroundColor(Color(uiColor: .primary)) + } + + Button(viewModel.dismissAlertTitle) { + viewModel.dismissTapped() + isPresented = false + } + .padding(.bottom, Layout.padding) + } + } + + enum Layout { + static let padding: CGFloat = 16 + static let spacing: CGFloat = 16 + } +} + +#if DEBUG // to avoid importing things we don't need in the implementation +import Yosemite +import Fakes + +struct JustInTimeMessageModal_Previews: PreviewProvider { + static let viewModel: JustInTimeMessageViewModel = .init( + justInTimeMessage: Yosemite.JustInTimeMessage.fake().copy( + title: "Hello merchants!", + detail: "Take a look at this!", + template: .modal), + screenName: "preview", + siteID: 0) + + static var previews: some View { + ZStack { + Text("Modal test") + } + .modalOverlay(isPresented: .constant(true)) { + JustInTimeMessageModal(viewModel: viewModel, isPresented: .constant(true)) + } + } +} +#endif + +/// This wrapper exists to avoid the need to init a Binding in UIKit (which we can't) but +/// retain the presentation/dismiss behaviour +struct JustInTimeMessageModal_UIKit: View { + @State var isPresented: Bool = true + let viewModel: JustInTimeMessageViewModel + let onDismiss: (() -> Void)? + + init(onDismiss: (() -> Void)?, viewModel: JustInTimeMessageViewModel) { + self.viewModel = viewModel + self.onDismiss = onDismiss + } + + var body: some View { + ModalOverlay(isPresented: $isPresented, onDismiss: onDismiss) { + JustInTimeMessageModal(viewModel: viewModel, isPresented: $isPresented) + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift new file mode 100644 index 00000000000..1b2f0b8ad4e --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift @@ -0,0 +1,100 @@ +import SwiftUI + +struct ModalOverlay: View { + @Binding var isPresented: Bool + @ViewBuilder let content: () -> OverlayContent + let onDismiss: (() -> Void)? + + /// We use an internal copy of the `isPresented` state so that we can detect changes, and wrap them in a `withAnimation` call. + /// Without this, the fade and slide animations do not work. + @State private var internalIsPresented: Bool + + init(isPresented: Binding, onDismiss: (() -> Void)? = nil, content: @escaping () -> OverlayContent) { + self.content = content + self._isPresented = isPresented + self.onDismiss = onDismiss + self.internalIsPresented = isPresented.wrappedValue + } + + var body: some View { + ZStack { + if internalIsPresented { + Color.black.opacity(0.5) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + dismiss() + } + .transition(.opacity) + .animation(.easeInOut(duration: 0.1), value: internalIsPresented) + + GeometryReader { geometry in + VStack { + content() + .padding(16) + .frame(width: geometry.size.width * 0.75) + .frame(maxHeight: geometry.size.height * 0.8) + .fixedSize(horizontal: false, vertical: true) // these three modifiers define the container size + .background(Color(.basicBackground)) + .cornerRadius(10) + .shadow(radius: 10) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) // Ensures the container is centred + } + .transition(.move(edge: .bottom)) + .animation(.easeInOut(duration: 0.25), value: internalIsPresented) + } + } + .onChange(of: isPresented) { newValue in + withAnimation { + internalIsPresented = newValue + } + + if newValue == false { + onDismiss?() + } + } + } + + private func dismiss() { + isPresented = false + } +} + +extension View { + func modalOverlay(isPresented: Binding, @ViewBuilder overlayContent: @escaping () -> OverlayContent) -> some View { + self.modifier(ModalOverlayModifier(isPresented: isPresented, overlayContent: overlayContent)) + } +} + +struct ModalOverlayModifier: ViewModifier { + @Binding var isPresented: Bool + @ViewBuilder let overlayContent: () -> OverlayContent + + func body(content: Content) -> some View { + ZStack { + // Underlying content + content + // Modal overlay + ModalOverlay(isPresented: $isPresented, content: overlayContent) + } + } +} + +/// This wrapper exists to avoid the need to init a Binding in UIKit (which we can't) but +/// retain the presentation/dismiss behaviour +struct ModalOverlay_UIKit: View { + @State var isPresented: Bool = true + @ViewBuilder let content: () -> OverlayContent + let onDismiss: (() -> Void)? + + init(onDismiss: (() -> Void)? = nil, content: @escaping () -> OverlayContent) { + self.content = content + self.onDismiss = onDismiss + } + + var body: some View { + ModalOverlay(isPresented: $isPresented, + onDismiss: onDismiss, + content: content) + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 3987d9c0cd0..ab3b4c05a66 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -611,6 +611,8 @@ 03EF250428C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF250328C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift */; }; 03EF250628C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF250528C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift */; }; 03F5CB832A0C3A1A0026877A /* AnimatedPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */; }; + 03F5CAFF2A0BA37C0026877A /* JustInTimeMessageModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5CAFE2A0BA37C0026877A /* JustInTimeMessageModal.swift */; }; + 03F5CB012A0BA3D40026877A /* ModalOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5CB002A0BA3D40026877A /* ModalOverlay.swift */; }; 03FBDA9D263AD49200ACE257 /* CouponListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FBDA9C263AD49100ACE257 /* CouponListViewController.swift */; }; 03FBDAA3263AED2F00ACE257 /* CouponListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03FBDAA2263AED2F00ACE257 /* CouponListViewController.xib */; }; 03FBDAF2263EE47C00ACE257 /* CouponListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FBDAF1263EE47C00ACE257 /* CouponListViewModel.swift */; }; @@ -2879,6 +2881,8 @@ 03EF250328C6283B006A033E /* InPersonPaymentsMenuViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModelTests.swift; sourceTree = ""; }; 03EF250528C75838006A033E /* PurchaseCardReaderWebViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCardReaderWebViewViewModel.swift; sourceTree = ""; }; 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedPlaceholder.swift; sourceTree = ""; }; + 03F5CAFE2A0BA37C0026877A /* JustInTimeMessageModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageModal.swift; sourceTree = ""; }; + 03F5CB002A0BA3D40026877A /* ModalOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalOverlay.swift; sourceTree = ""; }; 03FBDA9C263AD49100ACE257 /* CouponListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponListViewController.swift; sourceTree = ""; }; 03FBDAA2263AED2F00ACE257 /* CouponListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CouponListViewController.xib; sourceTree = ""; }; 03FBDAF1263EE47C00ACE257 /* CouponListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponListViewModel.swift; sourceTree = ""; }; @@ -6975,6 +6979,8 @@ CC857C7629B25FAF00E19D1E /* FooterNotice.swift */, 02BE9CBF29C05CFD00292333 /* SitePreviewView.swift */, 03A9F3B12A03E70700385673 /* AdaptiveAsyncImage.swift */, + 03F5CAFE2A0BA37C0026877A /* JustInTimeMessageModal.swift */, + 03F5CB002A0BA3D40026877A /* ModalOverlay.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -11372,6 +11378,7 @@ B57B678A2107546E00AF8905 /* Address+Woo.swift in Sources */, 035F2308275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift in Sources */, EEB4E2DC29B600B800371C3C /* CouponLineDetails.swift in Sources */, + 03F5CB012A0BA3D40026877A /* ModalOverlay.swift in Sources */, 02C37B7D2967B72A00F0CF9E /* FreeStagingDomainView.swift in Sources */, 457509E4267B9E91005FA2EA /* AggregatedProductListViewController.swift in Sources */, D8815B0D263861A400EDAD62 /* CardPresentModalSuccess.swift in Sources */, @@ -11520,6 +11527,7 @@ 02E8B17723E2C49000A43403 /* InProgressProductImageCollectionViewCell.swift in Sources */, 0365986929AFB0C100F297D3 /* SetUpTapToPayInformationViewModel.swift in Sources */, CE5F462C23AACBC4006B1A5C /* RefundDetailsResultController.swift in Sources */, + 03F5CAFF2A0BA37C0026877A /* JustInTimeMessageModal.swift in Sources */, 261AA309275178FA009530FE /* PaymentMethodsView.swift in Sources */, DE6906E527D7439C00735E3B /* OrdersSplitViewWrapperController.swift in Sources */, AEC12B7A2758D55900845F97 /* OrderStatusList.swift in Sources */, From 1e34a9df583713bf52f70d26bcc58a9d9b53e3f6 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 11 May 2023 10:59:12 +0100 Subject: [PATCH 07/16] Improve contrast of overlay modal in dark mode --- .../ReusableViews/SwiftUI Components/ModalOverlay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift index 1b2f0b8ad4e..d625f96917b 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ModalOverlay.swift @@ -34,7 +34,7 @@ struct ModalOverlay: View { .frame(width: geometry.size.width * 0.75) .frame(maxHeight: geometry.size.height * 0.8) .fixedSize(horizontal: false, vertical: true) // these three modifiers define the container size - .background(Color(.basicBackground)) + .background(Color(.tertiarySystemBackground)) .cornerRadius(10) .shadow(radius: 10) } From 86d115f899ee56d4c32e75e21a13107f6afab8d4 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 11 May 2023 10:59:47 +0100 Subject: [PATCH 08/16] Remove Fakes in preview as it broke device builds --- .../JustInTimeMessageModal.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift index 6a8aa102249..7073385bde9 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift @@ -58,14 +58,24 @@ struct JustInTimeMessageModal: View { #if DEBUG // to avoid importing things we don't need in the implementation import Yosemite -import Fakes struct JustInTimeMessageModal_Previews: PreviewProvider { - static let viewModel: JustInTimeMessageViewModel = .init( - justInTimeMessage: Yosemite.JustInTimeMessage.fake().copy( - title: "Hello merchants!", - detail: "Take a look at this!", - template: .modal), + private static let message = Yosemite.JustInTimeMessage( + siteID: 0, + messageID: "messageID", + featureClass: "new–feature", + title: "Hey merchants!", + detail: "Take a look at this new feature...", + buttonTitle: "Try it out", + url: "https://woocommerce.com/mobile/payments", + backgroundImageUrl: nil, + backgroundImageDarkUrl: nil, + badgeImageUrl: nil, + badgeImageDarkUrl: nil, + template: .modal) + + private static let viewModel: JustInTimeMessageViewModel = .init( + justInTimeMessage: message, screenName: "preview", siteID: 0) From 5fb31d19bebe0d2c704dbebb4277019e7e46ce92 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 11 May 2023 11:07:27 +0100 Subject: [PATCH 09/16] Fix opening universal links from modal JITMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because of the additional delay in dismissing the modal JITM, universal links to Set up Tap to Pay on iPhone only got as far as the payments menu. This was due to using a short async delay to trigger the navigation to the Set up TTPoI flow – which works with a banner JITM, but triggers before the screen is presented when called from a modal JITM, so `navigationController` was still nil. That meant there was nothing to show the Set up TTPoI flow modal from. I’ve added a callback in the `viewDidLoad` for the view controller which allows us to trigger the navigation step at the right time, i.e. when the menu screen is fully loaded. --- .../InPersonPaymentsMenuViewController.swift | 13 +++++++++++-- .../ViewRelated/Hub Menu/HubMenuCoordinator.swift | 10 ++++------ .../Hub Menu/HubMenuViewController.swift | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index cd4ac2e082d..4d9d734c589 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -59,14 +59,22 @@ final class InPersonPaymentsMenuViewController: UIViewController { return button } + private let viewDidLoadAction: ((InPersonPaymentsMenuViewController) -> Void)? + + /// In Person Payments Menu View Controller contains the menu for managing and accepting payments with a card reader or Tap to Pay + /// - Parameters: + /// - stores: stores manager – for handling actions + /// - featureFlagService: feature flags which affect the view controller's behaviour will be loaded from here + /// - viewDidLoadAction: Provided as a one-time callback on viewDidLoad, originally to handle universal link navigation correctly. init(stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService - ) { + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, + viewDidLoadAction: ((InPersonPaymentsMenuViewController) -> Void)? = nil) { self.stores = stores self.featureFlagService = featureFlagService self.cardPresentPaymentsOnboardingUseCase = CardPresentPaymentsOnboardingUseCase() self.cashOnDeliveryToggleRowViewModel = InPersonPaymentsCashOnDeliveryToggleRowViewModel() self.setUpFlowOnlyEnabledAfterOnboardingComplete = !featureFlagService.isFeatureFlagEnabled(.tapToPayOnIPhoneMilestone2) + self.viewDidLoadAction = viewDidLoadAction super.init(nibName: nil, bundle: nil) } @@ -88,6 +96,7 @@ final class InPersonPaymentsMenuViewController: UIViewController { configureTableReload() runCardPresentPaymentsOnboardingIfPossible() configureWebViewPresentation() + viewDidLoadAction?(self) } } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift index 7d82c5d6376..bf9f5eae3d7 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift @@ -127,14 +127,12 @@ extension HubMenuCoordinator { case .paymentsMenu: _ = hubMenuController.showPaymentsMenu() case .simplePayments: - let viewController = hubMenuController.showPaymentsMenu() - DispatchQueue.main.asyncAfter(deadline: .now() + Constants.screenTransitionsDelay) { - viewController.openSimplePaymentsAmountFlow() + _ = hubMenuController.showPaymentsMenu { paymentsMenu in + paymentsMenu.openSimplePaymentsAmountFlow() } case .tapToPayOnIPhone: - let viewController = hubMenuController.showPaymentsMenu() - DispatchQueue.main.asyncAfter(deadline: .now() + Constants.screenTransitionsDelay) { - viewController.presentSetUpTapToPayOnIPhoneViewController() + _ = hubMenuController.showPaymentsMenu { paymentsMenu in + paymentsMenu.presentSetUpTapToPayOnIPhoneViewController() } } } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift index 509e84a0c4a..f3181631713 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift @@ -28,8 +28,8 @@ final class HubMenuViewController: UIHostingController { viewModel.showReviewDetails(using: parcel) } - func showPaymentsMenu() -> InPersonPaymentsMenuViewController { - let inPersonPaymentsMenuViewController = InPersonPaymentsMenuViewController() + func showPaymentsMenu(onCompletion: ((InPersonPaymentsMenuViewController) -> Void)? = nil) -> InPersonPaymentsMenuViewController { + let inPersonPaymentsMenuViewController = InPersonPaymentsMenuViewController(viewDidLoadAction: onCompletion) show(inPersonPaymentsMenuViewController, sender: self) return inPersonPaymentsMenuViewController From e7d2e13f2046f021565ce5b06dbf905b12b7fcd0 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 11 May 2023 11:43:59 +0100 Subject: [PATCH 10/16] 9679 Fix webview presentation from modal JITM CTA --- .../SwiftUI Components/JustInTimeMessageModal.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift index 7073385bde9..cffc313cc85 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift @@ -36,7 +36,11 @@ struct JustInTimeMessageModal: View { if let buttonTitle = viewModel.buttonTitle { Button(buttonTitle) { - viewModel.ctaTapped() + // dismissal and async call to the viewModel are required for the webview presentation to work. + isPresented = false + Task { + viewModel.ctaTapped() + } } .buttonStyle(PrimaryButtonStyle()) .foregroundColor(Color(uiColor: .primary)) From f18623a82b0bf066bc77b504c4542fac94a4079a Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 11 May 2023 11:55:43 +0100 Subject: [PATCH 11/16] 9679 Add modal JITMs to 13.6 release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 75cd2ff39a6..3d6a2309f75 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ 13.6 ----- - [*] JITMs: Added customization to Just in Time Message banner background and badges [https://github.com/woocommerce/woocommerce-ios/pull/9633] +- [*] JITMs: Added modal-style Just in Time Message support on the dashboard [https://github.com/woocommerce/woocommerce-ios/pull/9694] - [*] Product form > description editor: fix the extra bottom inset after hiding the keyboard either manually (available on a tablet) or applying an AI-generated product description. [https://github.com/woocommerce/woocommerce-ios/pull/9638] 13.5 From 30d3fa471ce2aafcc0d3efb56220fcbc036ceb29 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 12 May 2023 11:06:19 +0100 Subject: [PATCH 12/16] 9679 Bump modal JITMs to 13.7 --- RELEASE-NOTES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 3d6a2309f75..088271b464a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,9 +1,12 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] +13.7 +----- +- [*] JITMs: Added modal-style Just in Time Message support on the dashboard [https://github.com/woocommerce/woocommerce-ios/pull/9694] + 13.6 ----- - [*] JITMs: Added customization to Just in Time Message banner background and badges [https://github.com/woocommerce/woocommerce-ios/pull/9633] -- [*] JITMs: Added modal-style Just in Time Message support on the dashboard [https://github.com/woocommerce/woocommerce-ios/pull/9694] - [*] Product form > description editor: fix the extra bottom inset after hiding the keyboard either manually (available on a tablet) or applying an AI-generated product description. [https://github.com/woocommerce/woocommerce-ios/pull/9638] 13.5 From 75236963e7e7a31883505ccd918fb4479181cbd5 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 12 May 2023 11:30:54 +0100 Subject: [PATCH 13/16] 9679 Dismiss JITMs when empty response recieved --- .../Dashboard/DashboardViewController.swift | 10 ++++++++++ .../ViewRelated/Dashboard/DashboardViewModel.swift | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 12c5bfe1fae..67a4219306c 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -538,6 +538,7 @@ private extension DashboardViewController { Task { @MainActor [weak self] in guard let self = self else { return } + self.dismissModalJustInTimeMessage() let modalController = ConstraintsUpdatingHostingController( rootView: JustInTimeMessageModal_UIKit( onDismiss: { @@ -554,6 +555,15 @@ private extension DashboardViewController { .store(in: &subscriptions) } + private func dismissModalJustInTimeMessage() { + guard let modalJustInTimeMessageHostingController = modalJustInTimeMessageHostingController, + modalJustInTimeMessageHostingController.isBeingPresented + else { + return + } + dismiss(animated: true) + } + /// Display the error banner at the top of the dashboard content (below the site title) /// func showTopBannerView() { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index b6bae90e259..bcce746414b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -232,7 +232,8 @@ final class DashboardViewModel { case .some(.modal): modalJustInTimeMessageViewModel = viewModel default: - break + announcementViewModel = nil + modalJustInTimeMessageViewModel = nil } } From 9061b61a66342addede443505f6fc113909678dc Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 15 May 2023 17:40:10 +0100 Subject: [PATCH 14/16] 9679 Dismiss modal JITMs before showing a webview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a modal JITM is showing when we open the webview, we’ll get an error and the webview won’t open. This shouldn’t really happen, because the JITM modal already dismisses itself when we tap its CTA, but just in case we can do a pre-emptive dismissal before showing the webview. --- .../Classes/ViewRelated/Dashboard/DashboardViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 67a4219306c..9351fa3613d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -423,6 +423,7 @@ private extension DashboardViewController { viewModel.$showWebViewSheet.sink { [weak self] viewModel in guard let self = self else { return } guard let viewModel = viewModel else { return } + dismissModalJustInTimeMessage() self.openWebView(viewModel: viewModel) } .store(in: &subscriptions) @@ -562,6 +563,7 @@ private extension DashboardViewController { return } dismiss(animated: true) + self.modalJustInTimeMessageHostingController = nil } /// Display the error banner at the top of the dashboard content (below the site title) From ac17696db4477894442971d149a0bfa5c6cbc86d Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 15 May 2023 18:09:52 +0100 Subject: [PATCH 15/16] Prevent multiple re-display of JITM after webview --- .../Dashboard/DashboardViewController.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 9351fa3613d..9b5b036c50b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -433,15 +433,23 @@ private extension DashboardViewController { let webViewSheet = WebViewSheet(viewModel: viewModel) { [weak self] in guard let self = self else { return } self.dismiss(animated: true) - Task { - await self.viewModel.syncAnnouncements(for: self.siteID) - } + maybeSyncAnnouncementsAfterWebViewDismissed() } let hostingController = UIHostingController(rootView: webViewSheet) hostingController.presentationController?.delegate = self present(hostingController, animated: true, completion: nil) } + private func maybeSyncAnnouncementsAfterWebViewDismissed() { + // If the web view was opened from a modal JITM, it was dismissed before the webview + // was presented. Syncing in that situation would result in it showing again. + if self.viewModel.modalJustInTimeMessageViewModel == nil { + Task { + await self.viewModel.syncAnnouncements(for: self.siteID) + } + } + } + /// Subscribes to the trigger to start the Add Product flow for products onboarding /// private func observeAddProductTrigger() { @@ -657,9 +665,7 @@ private extension DashboardViewController { extension DashboardViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if presentationController.presentedViewController is UIHostingController { - Task { - await viewModel.syncAnnouncements(for: siteID) - } + maybeSyncAnnouncementsAfterWebViewDismissed() } } } From 8a13be05d60bab1c904b833bba4497b044b94674 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 15 May 2023 20:41:36 +0100 Subject: [PATCH 16/16] Fix pre-webview dismissal from modal JITMs --- .../ViewRelated/Dashboard/DashboardViewController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 9b5b036c50b..7a753467364 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -423,7 +423,7 @@ private extension DashboardViewController { viewModel.$showWebViewSheet.sink { [weak self] viewModel in guard let self = self else { return } guard let viewModel = viewModel else { return } - dismissModalJustInTimeMessage() + self.dismissModalJustInTimeMessage() self.openWebView(viewModel: viewModel) } .store(in: &subscriptions) @@ -433,7 +433,7 @@ private extension DashboardViewController { let webViewSheet = WebViewSheet(viewModel: viewModel) { [weak self] in guard let self = self else { return } self.dismiss(animated: true) - maybeSyncAnnouncementsAfterWebViewDismissed() + self.maybeSyncAnnouncementsAfterWebViewDismissed() } let hostingController = UIHostingController(rootView: webViewSheet) hostingController.presentationController?.delegate = self @@ -565,8 +565,7 @@ private extension DashboardViewController { } private func dismissModalJustInTimeMessage() { - guard let modalJustInTimeMessageHostingController = modalJustInTimeMessageHostingController, - modalJustInTimeMessageHostingController.isBeingPresented + guard let modalJustInTimeMessageHostingController = modalJustInTimeMessageHostingController else { return }