From 272b7c7058571ac027a86d0527befdd0e1913883 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 28 Oct 2022 15:14:54 +0100 Subject: [PATCH 1/3] 7855 Extract WebView creation from existing cards --- .../UpsellCardReadersCampaign.swift | 4 -- .../Orders/OrderListViewController.swift | 28 +++----- .../Payment Methods/PaymentMethodsView.swift | 22 +++---- .../SwiftUI Components/WebView.swift | 65 +++++++++++++++++++ .../SwiftUI Components/WebViewSheet.swift | 38 +++++++++++ .../WooNavigationSheet.swift | 38 +++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 12 ++++ 7 files changed, 169 insertions(+), 38 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebView.swift create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WebViewSheet.swift create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift 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/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..be7e6202115 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift @@ -0,0 +1,38 @@ +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() + } + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 17ba406748f..28ab487b083 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 */; }; @@ -2362,6 +2365,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 = ""; }; @@ -5925,6 +5931,9 @@ B9E4364B287587D300883CFA /* FeatureAnnouncementCardView.swift */, B9E4364D287589E200883CFA /* BadgeView.swift */, 6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */, + 03076D39290C22BE008EE839 /* WebView.swift */, + 03076D35290C162E008EE839 /* WebViewSheet.swift */, + 03076D37290C223D008EE839 /* WooNavigationSheet.swift */, ); path = "SwiftUI Components"; sourceTree = ""; @@ -9675,6 +9684,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 */, @@ -9899,6 +9909,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 */, @@ -10349,6 +10360,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 */, From 9a07cc77c94e2154ebab41785785972659385490 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 28 Oct 2022 15:49:57 +0100 Subject: [PATCH 2/3] 7855 Show webview when JITM CTA is tapped --- ...TimeMessageAnnouncementCardViewModel.swift | 4 ++- .../Dashboard/DashboardViewController.swift | 22 +++++++++++++-- .../Dashboard/DashboardViewModel.swift | 27 ++++++++++++++++--- 3 files changed, 47 insertions(+), 6 deletions(-) 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/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 1f232debd04..5e6a18733f4 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -126,6 +126,7 @@ final class DashboardViewController: UIViewController { observeStatsVersionForDashboardUIUpdates() trackProductsOnboardingEligibility() observeAnnouncements() + observeShowWebViewSheet() if hasAnnouncementFeatureFlag { viewModel.syncAnnouncements(for: siteID) } @@ -305,6 +306,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 @@ -323,8 +341,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 98872927d4c..b58c1c37818 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -9,6 +9,8 @@ final class DashboardViewModel { @Published private(set) var announcementViewModel: AnnouncementCardViewModelProtocol? = nil + @Published private(set) var showWebViewSheet: WebViewSheetViewModel? = nil + private let stores: StoresManager init(stores: StoresManager = ServiceLocator.stores) { @@ -107,9 +109,20 @@ final class DashboardViewModel { hook: .adminNotices) { result in switch result { case let .success(.some(message)): - let viewModel = JustInTimeMessageAnnouncementCardViewModel(title: message.title, - message: message.detail, - buttonTitle: message.buttonTitle) + 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 @@ -118,6 +131,13 @@ final class DashboardViewModel { stores.dispatch(action) } } + + private func needsAuthenticatedWebView(url: URL) -> Bool { + guard let host = url.host else { + return false + } + return Constants.trustedDomains.contains(host) + } } // MARK: - Constants @@ -126,5 +146,6 @@ private extension DashboardViewModel { enum Constants { static let topEarnerStatsLimit: Int = 5 static let dashboardScreenName = "my_store" + static let trustedDomains = ["woocommerce.com", "wordpress.com"] } } From c319e23253915e64fd9509a241fa7787284e3703 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 31 Oct 2022 11:02:11 +0000 Subject: [PATCH 3/3] StackNavigationViewStyle avoids split view on iPad Previously when the sheet was created on iPad, the content would be rendered as the master view of a split view, with no detail view. Often this would lead to no content being displayed, and a confusing back button added. StackNavigationViewStyle makes no change to the iPhone design but makes this sheet suitable for form/page sheet presentation on iPad, showing the content immediately. --- .../ReusableViews/SwiftUI Components/WooNavigationSheet.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift index be7e6202115..d49899eb1eb 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/WooNavigationSheet.swift @@ -34,5 +34,6 @@ struct WooNavigationSheet: View { } .wooNavigationBarStyle() } + .navigationViewStyle(StackNavigationViewStyle()) } }