Skip to content

Commit a19201e

Browse files
authored
Merge pull request #8334 from woocommerce/task/jitm-new-placements
[JITM] Support in other screens infrastructure
2 parents eed8364 + fa90daf commit a19201e

File tree

5 files changed

+158
-85
lines changed

5 files changed

+158
-85
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
import Yosemite
3+
4+
enum JustInTimeMessagesSourceScreen {
5+
case dashboard
6+
}
7+
8+
/// Provides the Just in Time Messages content for a given source screen and site. It also tracks the requests success or error.
9+
///
10+
final class JustInTimeMessagesProvider {
11+
private let stores: StoresManager
12+
private let analytics: Analytics
13+
private let appScreenJitmSourceMapping: [JustInTimeMessagesSourceScreen: String] = [.dashboard: "my_store"]
14+
15+
init(stores: StoresManager = ServiceLocator.stores,
16+
analytics: Analytics = ServiceLocator.analytics) {
17+
self.stores = stores
18+
self.analytics = analytics
19+
}
20+
21+
func loadMessage(for screen: JustInTimeMessagesSourceScreen, siteID: Int64) async throws -> JustInTimeMessageAnnouncementCardViewModel? {
22+
guard let source = appScreenJitmSourceMapping[screen] else {
23+
DDLogInfo("Could not load JITM for \(screen) because there is no mapping for the given screen")
24+
return nil
25+
}
26+
27+
return try await withCheckedThrowingContinuation { continuation in
28+
let action = JustInTimeMessageAction.loadMessage(
29+
siteID: siteID,
30+
screen: source,
31+
hook: .adminNotices) { [weak self] result in
32+
guard let self = self else { return }
33+
switch result {
34+
case let .success(messages):
35+
guard let message = messages.first else {
36+
return continuation.resume(returning: nil)
37+
}
38+
self.analytics.track(event:
39+
.JustInTimeMessage.fetchSuccess(source: source,
40+
messageID: message.messageID,
41+
count: Int64(messages.count)))
42+
let viewModel = JustInTimeMessageAnnouncementCardViewModel(
43+
justInTimeMessage: message,
44+
screenName: source,
45+
siteID: siteID)
46+
continuation.resume(returning: viewModel)
47+
case let .failure(error):
48+
self.analytics.track(event:
49+
.JustInTimeMessage.fetchFailure(source: source,
50+
error: error))
51+
continuation.resume(throwing: error)
52+
}
53+
}
54+
Task { @MainActor in
55+
stores.dispatch(action)
56+
}
57+
}
58+
}
59+
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,9 @@ final class DashboardViewController: UIViewController {
132132
observeAnnouncements()
133133
observeShowWebViewSheet()
134134
observeAddProductTrigger()
135-
viewModel.syncAnnouncements(for: siteID)
135+
136136
Task { @MainActor in
137+
await viewModel.syncAnnouncements(for: siteID)
137138
await reloadDashboardUIStatsVersion(forced: true)
138139
}
139140
}
@@ -387,7 +388,9 @@ private extension DashboardViewController {
387388
let webViewSheet = WebViewSheet(viewModel: viewModel) { [weak self] in
388389
guard let self = self else { return }
389390
self.dismiss(animated: true)
390-
self.viewModel.syncAnnouncements(for: self.siteID)
391+
Task {
392+
await self.viewModel.syncAnnouncements(for: self.siteID)
393+
}
391394
}
392395
let hostingController = UIHostingController(rootView: webViewSheet)
393396
hostingController.presentationController?.delegate = self
@@ -411,7 +414,9 @@ private extension DashboardViewController {
411414
coordinator.onProductCreated = { [weak self] in
412415
guard let self else { return }
413416
self.viewModel.announcementViewModel = nil // Remove the products onboarding banner
414-
self.viewModel.syncAnnouncements(for: self.siteID)
417+
Task {
418+
await self.viewModel.syncAnnouncements(for: self.siteID)
419+
}
415420
}
416421
coordinator.start()
417422
}
@@ -428,19 +433,20 @@ private extension DashboardViewController {
428433
func observeAnnouncements() {
429434
viewModel.$announcementViewModel.sink { [weak self] viewModel in
430435
guard let self = self else { return }
431-
self.removeAnnouncement()
432-
guard let viewModel = viewModel else {
433-
return
434-
}
435-
436-
let cardView = FeatureAnnouncementCardView(
437-
viewModel: viewModel,
438-
dismiss: { [weak self] in
439-
self?.viewModel.announcementViewModel = nil
440-
},
441-
callToAction: {})
436+
Task { @MainActor in
437+
self.removeAnnouncement()
438+
guard let viewModel = viewModel else {
439+
return
440+
}
442441

443-
self.showAnnouncement(AnnouncementCardWrapper(cardView: cardView))
442+
let cardView = FeatureAnnouncementCardView(
443+
viewModel: viewModel,
444+
dismiss: { [weak self] in
445+
self?.viewModel.announcementViewModel = nil
446+
},
447+
callToAction: {})
448+
self.showAnnouncement(AnnouncementCardWrapper(cardView: cardView))
449+
}
444450
}
445451
.store(in: &subscriptions)
446452
}
@@ -509,7 +515,9 @@ extension DashboardViewController: DashboardUIScrollDelegate {
509515
extension DashboardViewController: UIAdaptivePresentationControllerDelegate {
510516
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
511517
if presentationController.presentedViewController is UIHostingController<WebViewSheet> {
512-
viewModel.syncAnnouncements(for: siteID)
518+
Task {
519+
await viewModel.syncAnnouncements(for: siteID)
520+
}
513521
}
514522
}
515523
}
@@ -622,7 +630,7 @@ private extension DashboardViewController {
622630

623631
func pullToRefresh() async {
624632
ServiceLocator.analytics.track(.dashboardPulledToRefresh)
625-
viewModel.syncAnnouncements(for: siteID)
633+
await viewModel.syncAnnouncements(for: siteID)
626634
await reloadDashboardUIStatsVersion(forced: true)
627635
}
628636
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import enum Networking.DotcomError
44
import enum Storage.StatsVersion
55
import protocol Experiments.FeatureFlagService
66

7+
private enum ProductsOnboardingSyncingError: Error {
8+
case noContentToShow // there is no content to show, because the site is not eligible, it was already shown, or other reason
9+
}
10+
711
/// Syncs data for dashboard stats UI and determines the state of the dashboard UI based on stats version.
812
final class DashboardViewModel {
913
/// Stats v4 is shown by default, then falls back to v3 if store stats are unavailable.
@@ -20,13 +24,15 @@ final class DashboardViewModel {
2024
private let stores: StoresManager
2125
private let featureFlagService: FeatureFlagService
2226
private let analytics: Analytics
27+
private let justInTimeMessagesManager: JustInTimeMessagesProvider
2328

2429
init(stores: StoresManager = ServiceLocator.stores,
2530
featureFlags: FeatureFlagService = ServiceLocator.featureFlagService,
2631
analytics: Analytics = ServiceLocator.analytics) {
2732
self.stores = stores
2833
self.featureFlagService = featureFlags
2934
self.analytics = analytics
35+
self.justInTimeMessagesManager = JustInTimeMessagesProvider(stores: stores, analytics: analytics)
3036
}
3137

3238
/// Syncs store stats for dashboard UI.
@@ -116,37 +122,46 @@ final class DashboardViewModel {
116122

117123
/// Checks for announcements to show on the dashboard
118124
///
119-
func syncAnnouncements(for siteID: Int64) {
120-
syncProductsOnboarding(for: siteID) { [weak self] in
121-
self?.syncJustInTimeMessages(for: siteID)
125+
func syncAnnouncements(for siteID: Int64) async {
126+
// For now, products onboarding takes precedence over Just In Time Messages,
127+
// so we can stop if there is an onboarding announcement to display.
128+
// This should be revisited when either onboarding or JITMs are expanded. See: pe5pgL-11B-p2
129+
do {
130+
try await syncProductsOnboarding(for: siteID)
131+
} catch {
132+
await syncJustInTimeMessages(for: siteID)
122133
}
123134
}
124135

125-
/// Checks if a store is eligible for products onboarding and prepares the onboarding announcement if needed.
136+
/// Checks if a store is eligible for products onboarding -returning error otherwise- and prepares the onboarding announcement if needed.
126137
///
127-
private func syncProductsOnboarding(for siteID: Int64, onCompletion: @escaping () -> Void) {
128-
let action = ProductAction.checkProductsOnboardingEligibility(siteID: siteID) { [weak self] result in
129-
switch result {
130-
case .success(let isEligible):
131-
if isEligible {
132-
ServiceLocator.analytics.track(event: .ProductsOnboarding.storeIsEligible())
138+
private func syncProductsOnboarding(for siteID: Int64) async throws {
139+
try await withCheckedThrowingContinuation { [weak self] continuation in
140+
let action = ProductAction.checkProductsOnboardingEligibility(siteID: siteID) { [weak self] result in
141+
switch result {
142+
case .success(let isEligible):
143+
if isEligible {
144+
ServiceLocator.analytics.track(event: .ProductsOnboarding.storeIsEligible())
133145

134-
self?.setProductsOnboardingBannerIfNeeded()
135-
}
146+
self?.setProductsOnboardingBannerIfNeeded()
147+
}
148+
149+
if self?.announcementViewModel is ProductsOnboardingAnnouncementCardViewModel {
150+
continuation.resume(returning: (()))
151+
} else {
152+
continuation.resume(throwing: ProductsOnboardingSyncingError.noContentToShow)
153+
}
136154

137-
// For now, products onboarding takes precedence over Just In Time Messages,
138-
// so we can stop if there is an onboarding announcement to display.
139-
// This should be revisited when either onboarding or JITMs are expanded. See: pe5pgL-11B-p2
140-
if self?.announcementViewModel is ProductsOnboardingAnnouncementCardViewModel {
141-
return
155+
case .failure(let error):
156+
DDLogError("⛔️ Dashboard — Error checking products onboarding eligibility: \(error)")
157+
continuation.resume(throwing: error)
142158
}
143-
onCompletion()
144-
case .failure(let error):
145-
DDLogError("⛔️ Dashboard — Error checking products onboarding eligibility: \(error)")
146-
onCompletion()
159+
}
160+
161+
Task { @MainActor in
162+
stores.dispatch(action)
147163
}
148164
}
149-
stores.dispatch(action)
150165
}
151166

152167
/// Sets the view model for the products onboarding banner if the user hasn't dismissed it before.
@@ -166,39 +181,14 @@ final class DashboardViewModel {
166181

167182
/// Checks for Just In Time Messages and prepares the announcement if needed.
168183
///
169-
private func syncJustInTimeMessages(for siteID: Int64) {
184+
private func syncJustInTimeMessages(for siteID: Int64) async {
170185
guard featureFlagService.isFeatureFlagEnabled(.justInTimeMessagesOnDashboard) else {
171186
return
172187
}
173188

174-
let action = JustInTimeMessageAction.loadMessage(
175-
siteID: siteID,
176-
screen: Constants.dashboardScreenName,
177-
hook: .adminNotices) { [weak self] result in
178-
guard let self = self else { return }
179-
switch result {
180-
case let .success(messages):
181-
guard let message = messages.first else {
182-
self.announcementViewModel = nil
183-
return
184-
}
185-
self.analytics.track(event:
186-
.JustInTimeMessage.fetchSuccess(source: Constants.dashboardScreenName,
187-
messageID: message.messageID,
188-
count: Int64(messages.count)))
189-
let viewModel = JustInTimeMessageAnnouncementCardViewModel(
190-
justInTimeMessage: message,
191-
screenName: Constants.dashboardScreenName,
192-
siteID: siteID)
193-
self.announcementViewModel = viewModel
194-
viewModel.$showWebViewSheet.assign(to: &self.$showWebViewSheet)
195-
case let .failure(error):
196-
self.analytics.track(event:
197-
.JustInTimeMessage.fetchFailure(source: Constants.dashboardScreenName,
198-
error: error))
199-
}
200-
}
201-
stores.dispatch(action)
189+
let viewModel = try? await justInTimeMessagesManager.loadMessage(for: .dashboard, siteID: siteID)
190+
viewModel?.$showWebViewSheet.assign(to: &self.$showWebViewSheet)
191+
announcementViewModel = viewModel
202192
}
203193
}
204194

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,7 @@
13871387
B910686027F1F28F00AD0575 /* GhostableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B910685F27F1F28F00AD0575 /* GhostableViewController.swift */; };
13881388
B9151B3F2840EB330036180F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9151B3E2840EB330036180F /* WooFoundation.framework */; };
13891389
B9151B402840EB340036180F /* WooFoundation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B9151B3E2840EB330036180F /* WooFoundation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1390+
B92639FF293E2D4C00A257E0 /* JustInTimeMessagesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92639FE293E2D4C00A257E0 /* JustInTimeMessagesProvider.swift */; };
13901391
B92FF9AE27FC7217005C34E3 /* OrderListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B92FF9AD27FC7217005C34E3 /* OrderListViewController.xib */; };
13911392
B92FF9B027FC7821005C34E3 /* ProductsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B92FF9AF27FC7821005C34E3 /* ProductsViewController.xib */; };
13921393
B94403C9289ABB4D00323FC2 /* SimplePaymentsAmountFlowOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94403C8289ABB4D00323FC2 /* SimplePaymentsAmountFlowOpener.swift */; };
@@ -3413,6 +3414,7 @@
34133414
B6F3796F293798ED00718561 /* AnalyticsHubTodayRangeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubTodayRangeData.swift; sourceTree = "<group>"; };
34143415
B910685F27F1F28F00AD0575 /* GhostableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostableViewController.swift; sourceTree = "<group>"; };
34153416
B9151B3E2840EB330036180F /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3417+
B92639FE293E2D4C00A257E0 /* JustInTimeMessagesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessagesProvider.swift; sourceTree = "<group>"; };
34163418
B92FF9AD27FC7217005C34E3 /* OrderListViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OrderListViewController.xib; sourceTree = "<group>"; };
34173419
B92FF9AF27FC7821005C34E3 /* ProductsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProductsViewController.xib; sourceTree = "<group>"; };
34183420
B94403C8289ABB4D00323FC2 /* SimplePaymentsAmountFlowOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePaymentsAmountFlowOpener.swift; sourceTree = "<group>"; };
@@ -7177,6 +7179,7 @@
71777179
B56DB3F12049C0B800D4AA8E /* Classes */ = {
71787180
isa = PBXGroup;
71797181
children = (
7182+
B912603F2940B2C400CACD4B /* JustInTimeMessages */,
71807183
B958A7C528B3D42000823EEF /* Universal Links */,
71817184
B57B67882107545B00AF8905 /* Model */,
71827185
D8D15F81230A178100D48B3F /* ServiceLocator */,
@@ -7528,6 +7531,14 @@
75287531
path = "Time Range";
75297532
sourceTree = "<group>";
75307533
};
7534+
B912603F2940B2C400CACD4B /* JustInTimeMessages */ = {
7535+
isa = PBXGroup;
7536+
children = (
7537+
B92639FE293E2D4C00A257E0 /* JustInTimeMessagesProvider.swift */,
7538+
);
7539+
path = JustInTimeMessages;
7540+
sourceTree = "<group>";
7541+
};
75317542
B958A7C528B3D42000823EEF /* Universal Links */ = {
75327543
isa = PBXGroup;
75337544
children = (
@@ -10189,6 +10200,7 @@
1018910200
D8C2A28823190B2300F503E9 /* StorageProductReview+Woo.swift in Sources */,
1019010201
2664210126F3E1BB001FC5B4 /* ModalHostingPresentationController.swift in Sources */,
1019110202
D8B4D5F026C2C7EC00F34E94 /* InPersonPaymentsStripeAccountPendingView.swift in Sources */,
10203+
B92639FF293E2D4C00A257E0 /* JustInTimeMessagesProvider.swift in Sources */,
1019210204
02784A03238B8BC800BDD6A8 /* UIView+Border.swift in Sources */,
1019310205
CE1CCB402056F21C000EE3AC /* Style.swift in Sources */,
1019410206
45EF7984244F26BB00B22BA2 /* Array+IndexPath.swift in Sources */,

0 commit comments

Comments
 (0)