Skip to content

Commit c7c27fa

Browse files
authored
Merge pull request #6890 from woocommerce/td/6865-fetch-stats-api-for-selected-tab
Dashboard: fetch stats data only for the visible time range tab
2 parents a06327d + a377b64 commit c7c27fa

12 files changed

+336
-581
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import UIKit
2+
3+
/// Contains all UI content to show on Dashboard
4+
///
5+
protocol DashboardUI: UIViewController {
6+
/// For navigation bar large title workaround.
7+
var scrollDelegate: DashboardUIScrollDelegate? { get set }
8+
9+
/// Called when the Dashboard should display syncing error
10+
var displaySyncingError: () -> Void { get set }
11+
12+
/// Called when the user pulls to refresh
13+
var onPullToRefresh: @MainActor () async -> Void { get set }
14+
15+
/// Reloads data in Dashboard
16+
///
17+
/// - Parameter forced: pass `true` to override sync throttling
18+
@MainActor
19+
func reloadData(forced: Bool) async
20+
}
21+
22+
/// Relays the scroll events to a delegate for navigation bar large title workaround.
23+
protocol DashboardUIScrollDelegate: AnyObject {
24+
/// Called when a dashboard tab `UIScrollView`'s `scrollViewDidScroll` event is triggered from the user.
25+
func dashboardUIScrollViewDidScroll(_ scrollView: UIScrollView)
26+
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ final class DashboardViewController: UIViewController {
1212

1313
private let siteID: Int64
1414

15-
private let dashboardUIFactory: DashboardUIFactory
1615
@Published private var dashboardUI: DashboardUI?
1716

17+
private lazy var deprecatedStatsViewController = DeprecatedDashboardStatsViewController()
18+
private lazy var storeStatsAndTopPerformersViewController = StoreStatsAndTopPerformersViewController(siteID: siteID, dashboardViewModel: viewModel)
19+
1820
// Used to enable subtitle with store name
1921
private var shouldShowStoreNameAsSubtitle: Bool = false
2022

@@ -88,13 +90,14 @@ final class DashboardViewController: UIViewController {
8890
return view
8991
}()
9092

93+
private let viewModel: DashboardViewModel = .init()
94+
9195
private var subscriptions = Set<AnyCancellable>()
9296

9397
// MARK: View Lifecycle
9498

9599
init(siteID: Int64) {
96100
self.siteID = siteID
97-
dashboardUIFactory = DashboardUIFactory(siteID: siteID)
98101
super.init(nibName: nil, bundle: nil)
99102
configureTabBarItem()
100103
}
@@ -112,13 +115,16 @@ final class DashboardViewController: UIViewController {
112115
observeSiteForUIUpdates()
113116
observeBottomJetpackBenefitsBannerVisibilityUpdates()
114117
observeNavigationBarHeightForStoreNameLabelVisibility()
118+
observeStatsVersionForDashboardUIUpdates()
119+
Task { @MainActor in
120+
await reloadDashboardUIStatsVersion(forced: true)
121+
}
115122
}
116123

117124
override func viewWillAppear(_ animated: Bool) {
118125
super.viewWillAppear(animated)
119126
// Reset title to prevent it from being empty right after login
120127
configureTitle()
121-
reloadDashboardUIStatsVersion(forced: false)
122128
}
123129

124130
override func viewDidLayoutSubviews() {
@@ -256,11 +262,23 @@ private extension DashboardViewController {
256262
}
257263
}
258264

259-
func reloadDashboardUIStatsVersion(forced: Bool) {
260-
dashboardUIFactory.reloadDashboardUI(onUIUpdate: { [weak self] dashboardUI in
265+
func reloadDashboardUIStatsVersion(forced: Bool) async {
266+
await storeStatsAndTopPerformersViewController.reloadData(forced: forced)
267+
}
268+
269+
func observeStatsVersionForDashboardUIUpdates() {
270+
viewModel.$statsVersion.removeDuplicates().sink { [weak self] statsVersion in
271+
guard let self = self else { return }
272+
let dashboardUI: DashboardUI
273+
switch statsVersion {
274+
case .v3:
275+
dashboardUI = self.deprecatedStatsViewController
276+
case .v4:
277+
dashboardUI = self.storeStatsAndTopPerformersViewController
278+
}
261279
dashboardUI.scrollDelegate = self
262-
self?.onDashboardUIUpdate(forced: forced, updatedDashboardUI: dashboardUI)
263-
})
280+
self.onDashboardUIUpdate(forced: false, updatedDashboardUI: dashboardUI)
281+
}.store(in: &subscriptions)
264282
}
265283

266284
/// Display the error banner at the top of the dashboard content (below the site title)
@@ -300,8 +318,10 @@ extension DashboardViewController: DashboardUIScrollDelegate {
300318
private extension DashboardViewController {
301319
func onDashboardUIUpdate(forced: Bool, updatedDashboardUI: DashboardUI) {
302320
defer {
303-
// Reloads data of the updated dashboard UI at the end.
304-
reloadData(forced: forced)
321+
Task { @MainActor [weak self] in
322+
// Reloads data of the updated dashboard UI at the end.
323+
await self?.reloadData(forced: true)
324+
}
305325
}
306326

307327
// Optimistically hide the error banner any time the dashboard UI updates (not just pull to refresh)
@@ -327,7 +347,7 @@ private extension DashboardViewController {
327347
dashboardUI = updatedDashboardUI
328348

329349
updatedDashboardUI.onPullToRefresh = { [weak self] in
330-
self?.pullToRefresh()
350+
await self?.pullToRefresh()
331351
}
332352
updatedDashboardUI.displaySyncingError = { [weak self] in
333353
self?.showTopBannerView()
@@ -399,20 +419,20 @@ private extension DashboardViewController {
399419
show(settingsViewController, sender: self)
400420
}
401421

402-
func pullToRefresh() {
422+
func pullToRefresh() async {
403423
ServiceLocator.analytics.track(.dashboardPulledToRefresh)
404-
reloadDashboardUIStatsVersion(forced: true)
424+
await reloadDashboardUIStatsVersion(forced: true)
405425
}
406426
}
407427

408428
// MARK: - Private Helpers
409429
//
410430
private extension DashboardViewController {
411-
func reloadData(forced: Bool) {
431+
@MainActor
432+
func reloadData(forced: Bool) async {
412433
DDLogInfo("♻️ Requesting dashboard data be reloaded...")
413-
dashboardUI?.reloadData(forced: forced, completion: { [weak self] in
414-
self?.configureTitle()
415-
})
434+
await dashboardUI?.reloadData(forced: forced)
435+
configureTitle()
416436
}
417437

418438
func observeSiteForUIUpdates() {
@@ -424,7 +444,9 @@ private extension DashboardViewController {
424444
return
425445
}
426446
self.updateUI(site: site)
427-
self.reloadData(forced: true)
447+
Task { @MainActor [weak self] in
448+
await self?.reloadData(forced: true)
449+
}
428450
}.store(in: &subscriptions)
429451
}
430452

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Yosemite
2+
import enum Networking.DotcomError
3+
import enum Storage.StatsVersion
4+
5+
/// Syncs data for dashboard stats UI and determines the state of the dashboard UI based on stats version.
6+
final class DashboardViewModel {
7+
/// Stats v4 is shown by default, then falls back to v3 if store stats are unavailable.
8+
@Published private(set) var statsVersion: StatsVersion = .v4
9+
10+
private let stores: StoresManager
11+
12+
init(stores: StoresManager = ServiceLocator.stores) {
13+
self.stores = stores
14+
}
15+
16+
/// Syncs store stats for dashboard UI.
17+
func syncStats(for siteID: Int64,
18+
siteTimezone: TimeZone,
19+
timeRange: StatsTimeRangeV4,
20+
latestDateToInclude: Date,
21+
onCompletion: ((Result<Void, Error>) -> Void)? = nil) {
22+
let earliestDateToInclude = timeRange.earliestDate(latestDate: latestDateToInclude, siteTimezone: siteTimezone)
23+
let action = StatsActionV4.retrieveStats(siteID: siteID,
24+
timeRange: timeRange,
25+
earliestDateToInclude: earliestDateToInclude,
26+
latestDateToInclude: latestDateToInclude,
27+
quantity: timeRange.maxNumberOfIntervals,
28+
onCompletion: { [weak self] result in
29+
guard let self = self else { return }
30+
switch result {
31+
case .success:
32+
self.statsVersion = .v4
33+
case .failure(let error):
34+
DDLogError("⛔️ Dashboard (Order Stats) — Error synchronizing order stats v4: \(error)")
35+
if error as? DotcomError == .noRestRoute {
36+
self.statsVersion = .v3
37+
} else {
38+
self.statsVersion = .v4
39+
}
40+
}
41+
onCompletion?(result)
42+
})
43+
stores.dispatch(action)
44+
}
45+
46+
/// Syncs visitor stats for dashboard UI.
47+
func syncSiteVisitStats(for siteID: Int64,
48+
siteTimezone: TimeZone,
49+
timeRange: StatsTimeRangeV4,
50+
latestDateToInclude: Date,
51+
onCompletion: ((Result<Void, Error>) -> Void)? = nil) {
52+
let action = StatsActionV4.retrieveSiteVisitStats(siteID: siteID,
53+
siteTimezone: siteTimezone,
54+
timeRange: timeRange,
55+
latestDateToInclude: latestDateToInclude,
56+
onCompletion: { result in
57+
if case let .failure(error) = result {
58+
DDLogError("⛔️ Error synchronizing visitor stats: \(error)")
59+
}
60+
onCompletion?(result)
61+
})
62+
stores.dispatch(action)
63+
}
64+
65+
/// Syncs top performers data for dashboard UI.
66+
func syncTopEarnersStats(for siteID: Int64,
67+
siteTimezone: TimeZone,
68+
timeRange: StatsTimeRangeV4,
69+
latestDateToInclude: Date,
70+
onCompletion: ((Result<Void, Error>) -> Void)? = nil) {
71+
let earliestDateToInclude = timeRange.earliestDate(latestDate: latestDateToInclude, siteTimezone: siteTimezone)
72+
let action = StatsActionV4.retrieveTopEarnerStats(siteID: siteID,
73+
timeRange: timeRange,
74+
earliestDateToInclude: earliestDateToInclude,
75+
latestDateToInclude: latestDateToInclude,
76+
quantity: Constants.topEarnerStatsLimit,
77+
onCompletion: { result in
78+
switch result {
79+
case .success:
80+
ServiceLocator.analytics.track(event:
81+
.Dashboard.dashboardTopPerformersLoaded(timeRange: timeRange))
82+
case .failure(let error):
83+
DDLogError("⛔️ Dashboard (Top Performers) — Error synchronizing top earner stats: \(error)")
84+
}
85+
onCompletion?(result)
86+
})
87+
stores.dispatch(action)
88+
}
89+
}
90+
91+
// MARK: - Constants
92+
//
93+
private extension DashboardViewModel {
94+
enum Constants {
95+
static let topEarnerStatsLimit: Int = 5
96+
}
97+
}

WooCommerce/Classes/ViewRelated/Dashboard/Factories/DashboardUIFactory.swift

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)