diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 38e248c6b1e..943c9933d49 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -339,10 +339,10 @@ extension Networking.JustInTimeMessage { siteID: .fake(), messageID: .fake(), featureClass: .fake(), - ttl: .fake(), content: .fake(), cta: .fake(), - assets: .fake() + assets: .fake(), + template: .fake() ) } } 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/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 47022c1cc42..c151fe91153 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -396,27 +396,27 @@ 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 + assets: CopiableProp<[String: URL]> = .copy, + template: CopiableProp = .copy ) -> 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 + let template = template ?? self.template return Networking.JustInTimeMessage( siteID: siteID, messageID: messageID, featureClass: featureClass, - ttl: ttl, 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 f728d3ead3e..2a75375ef8a 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 @@ -34,20 +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, - ttl: Int64, content: JustInTimeMessage.Content, cta: JustInTimeMessage.CTA, - assets: [String: URL]) { + assets: [String: URL], + template: String) { self.siteID = siteID self.messageID = messageID self.featureClass = featureClass - self.ttl = ttl self.content = content self.cta = cta self.assets = assets + self.template = template } } @@ -55,10 +53,10 @@ extension JustInTimeMessage: Codable { enum CodingKeys: String, CodingKey { case messageID = "id" case featureClass = "feature_class" - case ttl case content case cta = "CTA" case assets + case template } public init(from decoder: Decoder) throws { @@ -71,10 +69,10 @@ 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) ?? [:] + self.template = try container.decodeIfPresent(String.self, forKey: .template) ?? "default" } public func encode(to encoder: Encoder) throws { @@ -82,10 +80,10 @@ 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) + try container.encode(self.template, forKey: .template) } } diff --git a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift index d82c4e6821d..ef5cd005a4a 100644 --- a/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/JustInTimeMessageListMapperTests.swift @@ -34,14 +34,14 @@ 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."), 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) } @@ -55,14 +55,14 @@ 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."), 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", diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 188e139be92..fd82ad853e1 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,6 +2,7 @@ 13.7 ----- +- [*] JITMs: Added modal-style Just in Time Message support on the dashboard [https://github.com/woocommerce/woocommerce-ios/pull/9694] - [*] Order Creation: Products can be searched by SKU when adding products to an order. [https://github.com/woocommerce/woocommerce-ios/pull/9711] - [*] Orders: Fixes order details so separate order items are not combined just because they are the same product or variation. [https://github.com/woocommerce/woocommerce-ios/pull/9710] 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 81% rename from WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift rename to WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageViewModel.swift index 02652a7e308..7490a4cd24a 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,7 +123,65 @@ 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 { + switch template { + case .modal: + return Localization.maybeLaterButton + default: + return "" + } + } + + var dismissAlertMessage: String { "" } + + // MARK: - AnnouncementCardViewModelProtocol methods + func onAppear() { + trackMessageDisplayed() + } + + func dontShowAgainTapped() { + dismissTapped() + } + func remindLaterTapped() { // 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 451bd14ad8b..7a753467364 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? @@ -160,6 +162,7 @@ final class DashboardViewController: UIViewController { observeBottomJetpackBenefitsBannerVisibilityUpdates() observeStatsVersionForDashboardUIUpdates() observeAnnouncements() + observeModalJustInTimeMessages() observeShowWebViewSheet() observeAddProductTrigger() observeOnboardingVisibility() @@ -420,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 } + self.dismissModalJustInTimeMessage() self.openWebView(viewModel: viewModel) } .store(in: &subscriptions) @@ -429,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) - } + self.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() { @@ -527,6 +539,40 @@ private extension DashboardViewController { hostingController.view.layoutIfNeeded() } + private func observeModalJustInTimeMessages() { + viewModel.$modalJustInTimeMessageViewModel.sink { [weak self] viewModel in + guard let viewModel = viewModel else { + return + } + + Task { @MainActor [weak self] in + guard let self = self else { return } + self.dismissModalJustInTimeMessage() + 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) + } + + private func dismissModalJustInTimeMessage() { + guard let modalJustInTimeMessageHostingController = modalJustInTimeMessageHostingController + else { + return + } + dismiss(animated: true) + self.modalJustInTimeMessageHostingController = nil + } + /// Display the error banner at the top of the dashboard content (below the site title) /// func showTopBannerView() { @@ -618,9 +664,7 @@ private extension DashboardViewController { extension DashboardViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if presentationController.presentedViewController is UIHostingController { - Task { - await viewModel.syncAnnouncements(for: siteID) - } + maybeSyncAnnouncementsAfterWebViewDismissed() } } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index f960fe61083..bcce746414b 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,16 @@ 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: + announcementViewModel = nil + modalJustInTimeMessageViewModel = nil + } + } /// Sets up observer to decide store onboarding task lists visibility 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 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/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift new file mode 100644 index 00000000000..cffc313cc85 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/JustInTimeMessageModal.swift @@ -0,0 +1,114 @@ +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) { + // 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)) + } + + 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 + +struct JustInTimeMessageModal_Previews: PreviewProvider { + 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) + + 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..d625f96917b --- /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(.tertiarySystemBackground)) + .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 b9911efec02..eebb7d0af7f 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 */; }; @@ -610,6 +610,9 @@ 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 */; }; + 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 */; }; @@ -2811,7 +2814,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 = ""; }; @@ -2821,7 +2824,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 = ""; }; @@ -2881,6 +2884,9 @@ 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 = ""; }; + 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 = ""; }; @@ -5810,7 +5816,7 @@ isa = PBXGroup; children = ( 0371C3672875E47B00277E2C /* FeatureAnnouncementCardViewModel.swift */, - 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */, + 0366EAE02909A37800B51755 /* JustInTimeMessageViewModel.swift */, CCF4346D290AE1F900B4475A /* ProductsOnboardingAnnouncementCardViewModel.swift */, 0371C36D2876E92D00277E2C /* UpsellCardReadersCampaign.swift */, AEE085B42897C871007ACE20 /* LinkedProductsPromoCampaign.swift */, @@ -5822,7 +5828,7 @@ isa = PBXGroup; children = ( 0371C3692876DBCA00277E2C /* FeatureAnnouncementCardViewModelTests.swift */, - 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */, + 035BA3A7291000E90056F0AD /* JustInTimeMessageViewModelTests.swift */, ); path = "Feature Announcement Cards"; sourceTree = ""; @@ -6967,6 +6973,7 @@ 02B2829127C4808D004A332A /* InfiniteScrollIndicator.swift */, DE34771227F174C8009CA300 /* StatusView.swift */, B9E4364B287587D300883CFA /* FeatureAnnouncementCardView.swift */, + 03F5CB822A0C3A1A0026877A /* AnimatedPlaceholder.swift */, B9E4364D287589E200883CFA /* BadgeView.swift */, 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */, 03076D39290C22BE008EE839 /* WebView.swift */, @@ -6982,6 +6989,8 @@ CC857C7629B25FAF00E19D1E /* FooterNotice.swift */, 02BE9CBF29C05CFD00292333 /* SitePreviewView.swift */, 03A9F3B12A03E70700385673 /* AdaptiveAsyncImage.swift */, + 03F5CAFE2A0BA37C0026877A /* JustInTimeMessageModal.swift */, + 03F5CB002A0BA3D40026877A /* ModalOverlay.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -11381,6 +11390,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 */, @@ -11529,6 +11539,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 */, @@ -11611,6 +11622,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 */, @@ -11817,7 +11829,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 */, @@ -12349,7 +12361,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