diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift index 58a4dec0e9f..da26fc2553f 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift @@ -18,8 +18,10 @@ struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProt // No-op } + let onCTATapped: (() -> Void)? + func ctaTapped() { - // No-op + onCTATapped?() } var showDismissConfirmation: Bool = false diff --git a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/UpsellCardReadersCampaign.swift b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/UpsellCardReadersCampaign.swift index 85f8260a01d..ab5bc98d28b 100644 --- a/WooCommerce/Classes/ViewModels/Feature Announcement Cards/UpsellCardReadersCampaign.swift +++ b/WooCommerce/Classes/ViewModels/Feature Announcement Cards/UpsellCardReadersCampaign.swift @@ -55,10 +55,6 @@ extension UpsellCardReadersCampaign { static let cardReaderWebViewTitle = NSLocalizedString( "Purchase Card Reader", comment: "Title for the WebView opened to upsell card readers") - - static let cardReaderWebViewDoneButtonTitle = NSLocalizedString( - "Done", - comment: "Title for the Done button on the WebView opened to upsell card readers") } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 164c204a1c3..ee306630f8b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -122,6 +122,7 @@ final class DashboardViewController: UIViewController { observeNavigationBarHeightForStoreNameLabelVisibility() observeStatsVersionForDashboardUIUpdates() observeAnnouncements() + observeShowWebViewSheet() viewModel.syncAnnouncements(for: siteID) Task { @MainActor in await reloadDashboardUIStatsVersion(forced: true) @@ -282,6 +283,23 @@ private extension DashboardViewController { }.store(in: &subscriptions) } + func observeShowWebViewSheet() { + viewModel.$showWebViewSheet.sink { [weak self] viewModel in + guard let self = self else { return } + guard let viewModel = viewModel else { return } + self.openWebView(viewModel: viewModel) + } + .store(in: &subscriptions) + } + + private func openWebView(viewModel: WebViewSheetViewModel) { + let cardReaderWebview = WebViewSheet(viewModel: viewModel) { [weak self] in + self?.dismiss(animated: true) + } + let hostingController = UIHostingController(rootView: cardReaderWebview) + present(hostingController, animated: true, completion: nil) + } + // This is used so we have a specific type for the view while applying modifiers. struct AnnouncementCardWrapper: View { let cardView: FeatureAnnouncementCardView @@ -300,8 +318,8 @@ private extension DashboardViewController { } let cardView = FeatureAnnouncementCardView(viewModel: viewModel, - dismiss: {}, - callToAction: {}) + dismiss: {}, + callToAction: {}) self.showAnnouncement(AnnouncementCardWrapper(cardView: cardView)) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index 8108faf6359..8b1e47f2f60 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -10,6 +10,8 @@ final class DashboardViewModel { @Published private(set) var announcementViewModel: AnnouncementCardViewModelProtocol? = nil + @Published private(set) var showWebViewSheet: WebViewSheetViewModel? = nil + private let stores: StoresManager private let featureFlagService: FeatureFlagService @@ -147,18 +149,37 @@ final class DashboardViewModel { siteID: siteID, screen: Constants.dashboardScreenName, hook: .adminNotices) { [weak self] result in + guard let self = self else { return } switch result { case let .success(.some(message)): - let viewModel = JustInTimeMessageAnnouncementCardViewModel(title: message.title, - message: message.detail, - buttonTitle: message.buttonTitle) - self?.announcementViewModel = viewModel + let viewModel = JustInTimeMessageAnnouncementCardViewModel( + title: message.title, + message: message.detail, + buttonTitle: message.buttonTitle, + onCTATapped: { [weak self] in + guard let self = self, + let url = URL(string: message.url) + else { return } + let webViewModel = WebViewSheetViewModel( + url: url, + navigationTitle: message.title, + wpComAuthenticated: self.needsAuthenticatedWebView(url: url)) + self.showWebViewSheet = webViewModel + }) + self.announcementViewModel = viewModel default: break } } stores.dispatch(action) } + + private func needsAuthenticatedWebView(url: URL) -> Bool { + guard let host = url.host else { + return false + } + return Constants.trustedDomains.contains(host) + } } // MARK: - Constants @@ -167,5 +188,6 @@ private extension DashboardViewModel { enum Constants { static let topEarnerStatsLimit: Int = 5 static let dashboardScreenName = "my_store" + static let trustedDomains = ["woocommerce.com", "wordpress.com"] } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index 533e44e47d5..1ba0a596c7d 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -434,30 +434,18 @@ extension OrderListViewController: SyncingCoordinatorDelegate { private func openCardReaderProductPageInWebView() { let configuration = CardPresentConfigurationLoader().configuration let url = configuration.purchaseCardReaderUrl(utmProvider: viewModel.upsellCardReadersCampaign.utmProvider) - let cardReaderWebview = makeCardReaderProductPageWebView(url: url) + let cardReaderWebview = WebViewSheet( + viewModel: WebViewSheetViewModel( + url: url, + navigationTitle: UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle, + wpComAuthenticated: true), + done: { [weak self] in + self?.dismiss(animated: true) + }) let hostingController = UIHostingController(rootView: cardReaderWebview) present(hostingController, animated: true, completion: nil) } - private func makeCardReaderProductPageWebView(url: URL) -> some View { - return NavigationView { - AuthenticatedWebView(isPresented: .constant(true), - url: url) - .navigationTitle(UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(action: { [weak self] in - self?.dismiss(animated: true) - }, label: { - Text(UpsellCardReadersCampaign.Localization.cardReaderWebViewDoneButtonTitle) - }) - } - } - } - .wooNavigationBarStyle() - } - func updateUpsellCardReaderTopBannerVisibility(with newCollection: UITraitCollection) { guard viewModel.topBanner == .upsellCardReaders else { return diff --git a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift index 941778efa66..dda778e2752 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift @@ -104,20 +104,14 @@ struct PaymentMethodsView: View { } } .sheet(isPresented: $showingPurchaseCardReaderView) { - NavigationView { - AuthenticatedWebView(isPresented: .constant(true), - url: viewModel.purchaseCardReaderUrl) - .navigationTitle(Text(UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle)) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(UpsellCardReadersCampaign.Localization.cardReaderWebViewDoneButtonTitle) { - showingPurchaseCardReaderView = false - } - } - } - } - .wooNavigationBarStyle() + WebViewSheet( + viewModel: WebViewSheetViewModel( + url: viewModel.purchaseCardReaderUrl, + navigationTitle: UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle, + wpComAuthenticated: true), + done: { + showingPurchaseCardReaderView = false + }) .navigationViewStyle(.stack) } .shareSheet(isPresented: $sharingPaymentLink) { diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebView.swift new file mode 100644 index 00000000000..d1011cc6535 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebView.swift @@ -0,0 +1,65 @@ +import SwiftUI +import WebKit +import Alamofire +import class Networking.UserAgent + +/// Mirror of AuthenticatedWebView, for equivalent display of URLs in `WKWebView` that do not need authentication on WPCom. +struct WebView: UIViewRepresentable { + @Environment(\.presentationMode) var presentation + @Binding var isPresented: Bool { + didSet { + if !isPresented { + presentation.wrappedValue.dismiss() + } + } + } + + let url: URL + + /// Optional URL or part of URL to trigger exit + /// + var urlToTriggerExit: String? + + /// Callback that will be triggered if the destination url containts the `urlToTriggerExit` + /// + var exitTrigger: (() -> Void)? + + private let credentials = ServiceLocator.stores.sessionManager.defaultCredentials + + func makeCoordinator() -> WebViewCoordinator { + WebViewCoordinator(self) + } + + func makeUIView(context: Context) -> WKWebView { + let webview = WKWebView() + webview.customUserAgent = UserAgent.defaultUserAgent + webview.navigationDelegate = context.coordinator + + webview.load(URLRequest(url: url)) + return webview + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + + } + + class WebViewCoordinator: NSObject, WKNavigationDelegate { + private var parent: WebView + + init(_ uiWebView: WebView) { + parent = uiWebView + } + + func webView(_ webView: WKWebView, decidePolicyFor + navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = webView.url?.absoluteString, let urlTrigger = parent.urlToTriggerExit, url.contains(urlTrigger) { + parent.exitTrigger?() + decisionHandler(.cancel) + webView.navigationDelegate = nil + return + } + decisionHandler(.allow) + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebViewSheet.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebViewSheet.swift new file mode 100644 index 00000000000..4671cd63c17 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebViewSheet.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct WebViewSheetViewModel { + let url: URL + let navigationTitle: String + let wpComAuthenticated: Bool +} + +struct WebViewSheet: View { + let viewModel: WebViewSheetViewModel + + let done: () -> Void + + var body: some View { + WooNavigationSheet(viewModel: .init(navigationTitle: viewModel.navigationTitle, + done: done)) { + switch viewModel.wpComAuthenticated { + case true: + AuthenticatedWebView(isPresented: .constant(true), + url: viewModel.url) + case false: + WebView(isPresented: .constant(true), + url: viewModel.url) + } + } + } +} + +struct WebViewSheet_Previews: PreviewProvider { + static var previews: some View { + WebViewSheet( + viewModel: WebViewSheetViewModel.init( + url: URL(string: "https://woocommerce.com")!, + navigationTitle: "WooCommerce.com", + wpComAuthenticated: true), + done: { }) + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift new file mode 100644 index 00000000000..d49899eb1eb --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct WooNavigationSheetViewModel { + let navigationTitle: String + let done: () -> Void + let doneButtonTitle = NSLocalizedString( + "Done", + comment: "Title for the Done button on a WebView modal sheet") +} + +struct WooNavigationSheet: View { + let content: Content + + let viewModel: WooNavigationSheetViewModel + + init(viewModel: WooNavigationSheetViewModel, + @ViewBuilder content: () -> Content) { + self.content = content() + self.viewModel = viewModel + } + + var body: some View { + NavigationView { + content + .navigationTitle(viewModel.navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(action: viewModel.done, + label: { + Text(viewModel.doneButtonTitle) + }) + } + } + .wooNavigationBarStyle() + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 4cfbcfa8425..9d16b2ff73c 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -435,6 +435,9 @@ 0304E35E28BDC86D00A80191 /* LearnMoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304E35D28BDC86D00A80191 /* LearnMoreViewModel.swift */; }; 0304E36428BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0304E36328BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib */; }; 0304E36628BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304E36528BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift */; }; + 03076D36290C162E008EE839 /* WebViewSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D35290C162E008EE839 /* WebViewSheet.swift */; }; + 03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D37290C223D008EE839 /* WooNavigationSheet.swift */; }; + 03076D3A290C22BE008EE839 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D39290C22BE008EE839 /* WebView.swift */; }; 0313651128AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651028AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift */; }; 0313651328ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651228ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift */; }; 0313651728ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651628ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift */; }; @@ -2364,6 +2367,9 @@ 0304E35D28BDC86D00A80191 /* LearnMoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreViewModel.swift; sourceTree = ""; }; 0304E36328BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LeftImageTitleSubtitleToggleTableViewCell.xib; sourceTree = ""; }; 0304E36528BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftImageTitleSubtitleToggleTableViewCell.swift; sourceTree = ""; }; + 03076D35290C162E008EE839 /* WebViewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSheet.swift; sourceTree = ""; }; + 03076D37290C223D008EE839 /* WooNavigationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooNavigationSheet.swift; sourceTree = ""; }; + 03076D39290C22BE008EE839 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 0313651028AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift; sourceTree = ""; }; 0313651228ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsOnboardingErrorMainContentView.swift; sourceTree = ""; }; 0313651628ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift; sourceTree = ""; }; @@ -5930,6 +5936,9 @@ B9E4364B287587D300883CFA /* FeatureAnnouncementCardView.swift */, B9E4364D287589E200883CFA /* BadgeView.swift */, 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */, + 03076D39290C22BE008EE839 /* WebView.swift */, + 03076D35290C162E008EE839 /* WebViewSheet.swift */, + 03076D37290C223D008EE839 /* WooNavigationSheet.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -9682,6 +9691,7 @@ 0240B3AC230A910C000A866C /* StoreStatsV4ChartAxisHelper.swift in Sources */, 31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */, CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */, + 03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */, E107FCE326C13A0D00BAF51B /* InPersonPaymentsSupportLink.swift in Sources */, 2662D90626E1571900E25611 /* ListSelector.swift in Sources */, 74D0A5302139CF1300E2919F /* String+Helpers.swift in Sources */, @@ -9906,6 +9916,7 @@ AEACCB6D2785FF4A000D01F0 /* NavigationRow.swift in Sources */, DE50294928BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift in Sources */, 02E8B17E23E2C8D900A43403 /* ProductImageActionHandler.swift in Sources */, + 03076D3A290C22BE008EE839 /* WebView.swift in Sources */, 023D877925EC8BCB00625963 /* UIScrollView+LargeTitleWorkaround.swift in Sources */, 2664210326F40FB1001FC5B4 /* View+ScrollModifiers.swift in Sources */, 02695770237281A9001BA0BF /* AztecTextViewAttachmentHandler.swift in Sources */, @@ -10356,6 +10367,7 @@ 260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */, 2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */, 09F5DE5D27CF948000E5A4D2 /* BulkUpdateOptionsModel.swift in Sources */, + 03076D36290C162E008EE839 /* WebViewSheet.swift in Sources */, 450C2CBA24D3127500D570DD /* ProductReviewsTableViewCell.swift in Sources */, 029D444922F13F8A00DEFA8A /* DashboardUI.swift in Sources */, D8C2A28F231BD00500F503E9 /* ReviewsViewModel.swift in Sources */,