Skip to content

Commit 1fb1a1e

Browse files
authored
Merge pull request #7973 from woocommerce/issue/7855-open-webview-for-jitm-cta
[Just In Time Messages] Open WebView for JITM Call to Action
2 parents 8d7eafe + 89d238e commit 1fb1a1e

File tree

10 files changed

+219
-45
lines changed

10 files changed

+219
-45
lines changed

WooCommerce/Classes/ViewModels/Feature Announcement Cards/JustInTimeMessageAnnouncementCardViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProt
1818
// No-op
1919
}
2020

21+
let onCTATapped: (() -> Void)?
22+
2123
func ctaTapped() {
22-
// No-op
24+
onCTATapped?()
2325
}
2426

2527
var showDismissButton: Bool = true

WooCommerce/Classes/ViewModels/Feature Announcement Cards/UpsellCardReadersCampaign.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ extension UpsellCardReadersCampaign {
5555
static let cardReaderWebViewTitle = NSLocalizedString(
5656
"Purchase Card Reader",
5757
comment: "Title for the WebView opened to upsell card readers")
58-
59-
static let cardReaderWebViewDoneButtonTitle = NSLocalizedString(
60-
"Done",
61-
comment: "Title for the Done button on the WebView opened to upsell card readers")
6258
}
6359
}
6460

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ final class DashboardViewController: UIViewController {
122122
observeNavigationBarHeightForStoreNameLabelVisibility()
123123
observeStatsVersionForDashboardUIUpdates()
124124
observeAnnouncements()
125+
observeShowWebViewSheet()
125126
viewModel.syncAnnouncements(for: siteID)
126127
Task { @MainActor in
127128
await reloadDashboardUIStatsVersion(forced: true)
@@ -282,6 +283,23 @@ private extension DashboardViewController {
282283
}.store(in: &subscriptions)
283284
}
284285

286+
func observeShowWebViewSheet() {
287+
viewModel.$showWebViewSheet.sink { [weak self] viewModel in
288+
guard let self = self else { return }
289+
guard let viewModel = viewModel else { return }
290+
self.openWebView(viewModel: viewModel)
291+
}
292+
.store(in: &subscriptions)
293+
}
294+
295+
private func openWebView(viewModel: WebViewSheetViewModel) {
296+
let cardReaderWebview = WebViewSheet(viewModel: viewModel) { [weak self] in
297+
self?.dismiss(animated: true)
298+
}
299+
let hostingController = UIHostingController(rootView: cardReaderWebview)
300+
present(hostingController, animated: true, completion: nil)
301+
}
302+
285303
// This is used so we have a specific type for the view while applying modifiers.
286304
struct AnnouncementCardWrapper: View {
287305
let cardView: FeatureAnnouncementCardView
@@ -300,8 +318,8 @@ private extension DashboardViewController {
300318
}
301319

302320
let cardView = FeatureAnnouncementCardView(viewModel: viewModel,
303-
dismiss: {},
304-
callToAction: {})
321+
dismiss: {},
322+
callToAction: {})
305323

306324
self.showAnnouncement(AnnouncementCardWrapper(cardView: cardView))
307325
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ final class DashboardViewModel {
1010

1111
@Published private(set) var announcementViewModel: AnnouncementCardViewModelProtocol? = nil
1212

13+
@Published private(set) var showWebViewSheet: WebViewSheetViewModel? = nil
14+
1315
private let stores: StoresManager
1416
private let featureFlagService: FeatureFlagService
1517

@@ -154,18 +156,37 @@ final class DashboardViewModel {
154156
siteID: siteID,
155157
screen: Constants.dashboardScreenName,
156158
hook: .adminNotices) { [weak self] result in
159+
guard let self = self else { return }
157160
switch result {
158161
case let .success(.some(message)):
159-
let viewModel = JustInTimeMessageAnnouncementCardViewModel(title: message.title,
160-
message: message.detail,
161-
buttonTitle: message.buttonTitle)
162-
self?.announcementViewModel = viewModel
162+
let viewModel = JustInTimeMessageAnnouncementCardViewModel(
163+
title: message.title,
164+
message: message.detail,
165+
buttonTitle: message.buttonTitle,
166+
onCTATapped: { [weak self] in
167+
guard let self = self,
168+
let url = URL(string: message.url)
169+
else { return }
170+
let webViewModel = WebViewSheetViewModel(
171+
url: url,
172+
navigationTitle: message.title,
173+
wpComAuthenticated: self.needsAuthenticatedWebView(url: url))
174+
self.showWebViewSheet = webViewModel
175+
})
176+
self.announcementViewModel = viewModel
163177
default:
164178
break
165179
}
166180
}
167181
stores.dispatch(action)
168182
}
183+
184+
private func needsAuthenticatedWebView(url: URL) -> Bool {
185+
guard let host = url.host else {
186+
return false
187+
}
188+
return Constants.trustedDomains.contains(host)
189+
}
169190
}
170191

171192
// MARK: - Constants
@@ -174,5 +195,6 @@ private extension DashboardViewModel {
174195
enum Constants {
175196
static let topEarnerStatsLimit: Int = 5
176197
static let dashboardScreenName = "my_store"
198+
static let trustedDomains = ["woocommerce.com", "wordpress.com"]
177199
}
178200
}

WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -434,30 +434,18 @@ extension OrderListViewController: SyncingCoordinatorDelegate {
434434
private func openCardReaderProductPageInWebView() {
435435
let configuration = CardPresentConfigurationLoader().configuration
436436
let url = configuration.purchaseCardReaderUrl(utmProvider: viewModel.upsellCardReadersCampaign.utmProvider)
437-
let cardReaderWebview = makeCardReaderProductPageWebView(url: url)
437+
let cardReaderWebview = WebViewSheet(
438+
viewModel: WebViewSheetViewModel(
439+
url: url,
440+
navigationTitle: UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle,
441+
wpComAuthenticated: true),
442+
done: { [weak self] in
443+
self?.dismiss(animated: true)
444+
})
438445
let hostingController = UIHostingController(rootView: cardReaderWebview)
439446
present(hostingController, animated: true, completion: nil)
440447
}
441448

442-
private func makeCardReaderProductPageWebView(url: URL) -> some View {
443-
return NavigationView {
444-
AuthenticatedWebView(isPresented: .constant(true),
445-
url: url)
446-
.navigationTitle(UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle)
447-
.navigationBarTitleDisplayMode(.inline)
448-
.toolbar {
449-
ToolbarItem(placement: .confirmationAction) {
450-
Button(action: { [weak self] in
451-
self?.dismiss(animated: true)
452-
}, label: {
453-
Text(UpsellCardReadersCampaign.Localization.cardReaderWebViewDoneButtonTitle)
454-
})
455-
}
456-
}
457-
}
458-
.wooNavigationBarStyle()
459-
}
460-
461449
func updateUpsellCardReaderTopBannerVisibility(with newCollection: UITraitCollection) {
462450
guard viewModel.topBanner == .upsellCardReaders else {
463451
return

WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,14 @@ struct PaymentMethodsView: View {
104104
}
105105
}
106106
.sheet(isPresented: $showingPurchaseCardReaderView) {
107-
NavigationView {
108-
AuthenticatedWebView(isPresented: .constant(true),
109-
url: viewModel.purchaseCardReaderUrl)
110-
.navigationTitle(Text(UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle))
111-
.navigationBarTitleDisplayMode(.inline)
112-
.toolbar {
113-
ToolbarItem(placement: .navigationBarTrailing) {
114-
Button(UpsellCardReadersCampaign.Localization.cardReaderWebViewDoneButtonTitle) {
115-
showingPurchaseCardReaderView = false
116-
}
117-
}
118-
}
119-
}
120-
.wooNavigationBarStyle()
107+
WebViewSheet(
108+
viewModel: WebViewSheetViewModel(
109+
url: viewModel.purchaseCardReaderUrl,
110+
navigationTitle: UpsellCardReadersCampaign.Localization.cardReaderWebViewTitle,
111+
wpComAuthenticated: true),
112+
done: {
113+
showingPurchaseCardReaderView = false
114+
})
121115
.navigationViewStyle(.stack)
122116
}
123117
.shareSheet(isPresented: $sharingPaymentLink) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import SwiftUI
2+
import WebKit
3+
import Alamofire
4+
import class Networking.UserAgent
5+
6+
/// Mirror of AuthenticatedWebView, for equivalent display of URLs in `WKWebView` that do not need authentication on WPCom.
7+
struct WebView: UIViewRepresentable {
8+
@Environment(\.presentationMode) var presentation
9+
@Binding var isPresented: Bool {
10+
didSet {
11+
if !isPresented {
12+
presentation.wrappedValue.dismiss()
13+
}
14+
}
15+
}
16+
17+
let url: URL
18+
19+
/// Optional URL or part of URL to trigger exit
20+
///
21+
var urlToTriggerExit: String?
22+
23+
/// Callback that will be triggered if the destination url containts the `urlToTriggerExit`
24+
///
25+
var exitTrigger: (() -> Void)?
26+
27+
private let credentials = ServiceLocator.stores.sessionManager.defaultCredentials
28+
29+
func makeCoordinator() -> WebViewCoordinator {
30+
WebViewCoordinator(self)
31+
}
32+
33+
func makeUIView(context: Context) -> WKWebView {
34+
let webview = WKWebView()
35+
webview.customUserAgent = UserAgent.defaultUserAgent
36+
webview.navigationDelegate = context.coordinator
37+
38+
webview.load(URLRequest(url: url))
39+
return webview
40+
}
41+
42+
func updateUIView(_ uiView: WKWebView, context: Context) {
43+
44+
}
45+
46+
class WebViewCoordinator: NSObject, WKNavigationDelegate {
47+
private var parent: WebView
48+
49+
init(_ uiWebView: WebView) {
50+
parent = uiWebView
51+
}
52+
53+
func webView(_ webView: WKWebView, decidePolicyFor
54+
navigationAction: WKNavigationAction,
55+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
56+
if let url = webView.url?.absoluteString, let urlTrigger = parent.urlToTriggerExit, url.contains(urlTrigger) {
57+
parent.exitTrigger?()
58+
decisionHandler(.cancel)
59+
webView.navigationDelegate = nil
60+
return
61+
}
62+
decisionHandler(.allow)
63+
}
64+
}
65+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import SwiftUI
2+
3+
struct WebViewSheetViewModel {
4+
let url: URL
5+
let navigationTitle: String
6+
let wpComAuthenticated: Bool
7+
}
8+
9+
struct WebViewSheet: View {
10+
let viewModel: WebViewSheetViewModel
11+
12+
let done: () -> Void
13+
14+
var body: some View {
15+
WooNavigationSheet(viewModel: .init(navigationTitle: viewModel.navigationTitle,
16+
done: done)) {
17+
switch viewModel.wpComAuthenticated {
18+
case true:
19+
AuthenticatedWebView(isPresented: .constant(true),
20+
url: viewModel.url)
21+
case false:
22+
WebView(isPresented: .constant(true),
23+
url: viewModel.url)
24+
}
25+
}
26+
}
27+
}
28+
29+
struct WebViewSheet_Previews: PreviewProvider {
30+
static var previews: some View {
31+
WebViewSheet(
32+
viewModel: WebViewSheetViewModel.init(
33+
url: URL(string: "https://woocommerce.com")!,
34+
navigationTitle: "WooCommerce.com",
35+
wpComAuthenticated: true),
36+
done: { })
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import SwiftUI
2+
3+
struct WooNavigationSheetViewModel {
4+
let navigationTitle: String
5+
let done: () -> Void
6+
let doneButtonTitle = NSLocalizedString(
7+
"Done",
8+
comment: "Title for the Done button on a WebView modal sheet")
9+
}
10+
11+
struct WooNavigationSheet<Content: View>: View {
12+
let content: Content
13+
14+
let viewModel: WooNavigationSheetViewModel
15+
16+
init(viewModel: WooNavigationSheetViewModel,
17+
@ViewBuilder content: () -> Content) {
18+
self.content = content()
19+
self.viewModel = viewModel
20+
}
21+
22+
var body: some View {
23+
NavigationView {
24+
content
25+
.navigationTitle(viewModel.navigationTitle)
26+
.navigationBarTitleDisplayMode(.inline)
27+
.toolbar {
28+
ToolbarItem(placement: .confirmationAction) {
29+
Button(action: viewModel.done,
30+
label: {
31+
Text(viewModel.doneButtonTitle)
32+
})
33+
}
34+
}
35+
.wooNavigationBarStyle()
36+
}
37+
.navigationViewStyle(StackNavigationViewStyle())
38+
}
39+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@
435435
0304E35E28BDC86D00A80191 /* LearnMoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304E35D28BDC86D00A80191 /* LearnMoreViewModel.swift */; };
436436
0304E36428BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0304E36328BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib */; };
437437
0304E36628BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0304E36528BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift */; };
438+
03076D36290C162E008EE839 /* WebViewSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D35290C162E008EE839 /* WebViewSheet.swift */; };
439+
03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D37290C223D008EE839 /* WooNavigationSheet.swift */; };
440+
03076D3A290C22BE008EE839 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03076D39290C22BE008EE839 /* WebView.swift */; };
438441
0313651128AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651028AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift */; };
439442
0313651328ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651228ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift */; };
440443
0313651728ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651628ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift */; };
@@ -2371,6 +2374,9 @@
23712374
0304E35D28BDC86D00A80191 /* LearnMoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreViewModel.swift; sourceTree = "<group>"; };
23722375
0304E36328BE1EDE00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LeftImageTitleSubtitleToggleTableViewCell.xib; sourceTree = "<group>"; };
23732376
0304E36528BE1EED00A80191 /* LeftImageTitleSubtitleToggleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftImageTitleSubtitleToggleTableViewCell.swift; sourceTree = "<group>"; };
2377+
03076D35290C162E008EE839 /* WebViewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSheet.swift; sourceTree = "<group>"; };
2378+
03076D37290C223D008EE839 /* WooNavigationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooNavigationSheet.swift; sourceTree = "<group>"; };
2379+
03076D39290C22BE008EE839 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
23742380
0313651028AB81B100EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpView.swift; sourceTree = "<group>"; };
23752381
0313651228ABCB2D00EEE571 /* InPersonPaymentsOnboardingErrorMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsOnboardingErrorMainContentView.swift; sourceTree = "<group>"; };
23762382
0313651628ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift; sourceTree = "<group>"; };
@@ -5944,6 +5950,9 @@
59445950
B9E4364B287587D300883CFA /* FeatureAnnouncementCardView.swift */,
59455951
B9E4364D287589E200883CFA /* BadgeView.swift */,
59465952
6827140E28A3988300E6E3F6 /* DismissableNoticeView.swift */,
5953+
03076D39290C22BE008EE839 /* WebView.swift */,
5954+
03076D35290C162E008EE839 /* WebViewSheet.swift */,
5955+
03076D37290C223D008EE839 /* WooNavigationSheet.swift */,
59475956
);
59485957
path = "SwiftUI Components";
59495958
sourceTree = "<group>";
@@ -9711,6 +9720,7 @@
97119720
0240B3AC230A910C000A866C /* StoreStatsV4ChartAxisHelper.swift in Sources */,
97129721
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */,
97139722
CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */,
9723+
03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */,
97149724
E107FCE326C13A0D00BAF51B /* InPersonPaymentsSupportLink.swift in Sources */,
97159725
2662D90626E1571900E25611 /* ListSelector.swift in Sources */,
97169726
74D0A5302139CF1300E2919F /* String+Helpers.swift in Sources */,
@@ -9936,6 +9946,7 @@
99369946
AEACCB6D2785FF4A000D01F0 /* NavigationRow.swift in Sources */,
99379947
DE50294928BEF4CF00551736 /* WordPressOrgCredentials+Authenticator.swift in Sources */,
99389948
02E8B17E23E2C8D900A43403 /* ProductImageActionHandler.swift in Sources */,
9949+
03076D3A290C22BE008EE839 /* WebView.swift in Sources */,
99399950
023D877925EC8BCB00625963 /* UIScrollView+LargeTitleWorkaround.swift in Sources */,
99409951
2664210326F40FB1001FC5B4 /* View+ScrollModifiers.swift in Sources */,
99419952
02695770237281A9001BA0BF /* AztecTextViewAttachmentHandler.swift in Sources */,
@@ -10388,6 +10399,7 @@
1038810399
260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */,
1038910400
2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */,
1039010401
09F5DE5D27CF948000E5A4D2 /* BulkUpdateOptionsModel.swift in Sources */,
10402+
03076D36290C162E008EE839 /* WebViewSheet.swift in Sources */,
1039110403
450C2CBA24D3127500D570DD /* ProductReviewsTableViewCell.swift in Sources */,
1039210404
029D444922F13F8A00DEFA8A /* DashboardUI.swift in Sources */,
1039310405
D8C2A28F231BD00500F503E9 /* ReviewsViewModel.swift in Sources */,

0 commit comments

Comments
 (0)