Skip to content

Commit c45d2e0

Browse files
authored
Merge pull request #8374 from woocommerce/issue/8318-unblock-networking
[Analytics Hub] Update networking to load data in parallel
2 parents cc593fb + c65cebc commit c45d2e0

File tree

6 files changed

+145
-69
lines changed

6 files changed

+145
-69
lines changed

WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ struct AnalyticsHubView: View {
9797
VStack(spacing: Layout.dividerSpacing) {
9898
Divider()
9999

100-
AnalyticsProductCard(viewModel: viewModel.productCard)
100+
AnalyticsProductCard(statsViewModel: viewModel.productsStatsCard, itemsViewModel: viewModel.itemsSoldCard)
101101
.padding(.horizontal, insets: safeAreaInsets)
102102
.background(Color(uiColor: .listForeground))
103103

WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubViewModel.swift

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ final class AnalyticsHubViewModel: ObservableObject {
4141
///
4242
@Published var ordersCard = AnalyticsHubViewModel.ordersCard(currentPeriodStats: nil, previousPeriodStats: nil)
4343

44-
/// Products Card ViewModel
44+
/// Products Stats Card ViewModel
4545
///
46-
@Published var productCard = AnalyticsHubViewModel.productCard(currentPeriodStats: nil, previousPeriodStats: nil, itemsSoldStats: nil)
46+
@Published var productsStatsCard = AnalyticsHubViewModel.productsStatsCard(currentPeriodStats: nil, previousPeriodStats: nil)
47+
48+
/// Items Sold Card ViewModel
49+
///
50+
@Published var itemsSoldCard = AnalyticsHubViewModel.productsItemsSoldCard(itemsSoldStats: nil)
4751

4852
/// Sessions Card ViewModel
4953
///
@@ -92,7 +96,7 @@ final class AnalyticsHubViewModel: ObservableObject {
9296
@MainActor
9397
func updateData() async {
9498
do {
95-
try await retrieveOrderStats()
99+
try await retrieveData()
96100
} catch is AnalyticsHubTimeRangeSelection.TimeRangeGeneratorError {
97101
dismissNotice = Notice(title: Localization.timeRangeGeneratorError, feedbackType: .error)
98102
ServiceLocator.analytics.track(event: .AnalyticsHub.dateRangeSelectionFailed(for: timeRangeSelectionType))
@@ -114,26 +118,44 @@ final class AnalyticsHubViewModel: ObservableObject {
114118
private extension AnalyticsHubViewModel {
115119

116120
@MainActor
117-
func retrieveOrderStats() async throws {
121+
func retrieveData() async throws {
118122
switchToLoadingState()
119123

120124
let currentTimeRange = try timeRangeSelection.unwrapCurrentTimeRange()
121125
let previousTimeRange = try timeRangeSelection.unwrapPreviousTimeRange()
122126

127+
try await withThrowingTaskGroup(of: Void.self) { group in
128+
group.addTask {
129+
try await self.retrieveOrderStats(currentTimeRange: currentTimeRange, previousTimeRange: previousTimeRange)
130+
}
131+
group.addTask {
132+
try await self.retrieveVisitorStats(currentTimeRange: currentTimeRange, previousTimeRange: previousTimeRange)
133+
}
134+
try await group.waitForAll()
135+
}
136+
}
137+
138+
@MainActor
139+
func retrieveOrderStats(currentTimeRange: AnalyticsHubTimeRange, previousTimeRange: AnalyticsHubTimeRange) async throws {
123140
async let currentPeriodRequest = retrieveStats(earliestDateToInclude: currentTimeRange.start,
124141
latestDateToInclude: currentTimeRange.end,
125142
forceRefresh: true)
126143
async let previousPeriodRequest = retrieveStats(earliestDateToInclude: previousTimeRange.start,
127144
latestDateToInclude: previousTimeRange.end,
128145
forceRefresh: true)
129146

147+
let (currentPeriodStats, previousPeriodStats) = try await (currentPeriodRequest, previousPeriodRequest)
148+
self.currentOrderStats = currentPeriodStats
149+
self.previousOrderStats = previousPeriodStats
150+
}
151+
152+
@MainActor
153+
func retrieveVisitorStats(currentTimeRange: AnalyticsHubTimeRange, previousTimeRange: AnalyticsHubTimeRange) async throws {
130154
async let itemsSoldRequest = retrieveTopItemsSoldStats(earliestDateToInclude: currentTimeRange.start,
131155
latestDateToInclude: currentTimeRange.end,
132156
forceRefresh: true)
133157

134-
let (currentPeriodStats, previousPeriodStats, itemsSoldStats) = try await (currentPeriodRequest, previousPeriodRequest, itemsSoldRequest)
135-
self.currentOrderStats = currentPeriodStats
136-
self.previousOrderStats = previousPeriodStats
158+
let itemsSoldStats = try await itemsSoldRequest
137159
self.itemsSoldStats = itemsSoldStats
138160
}
139161

@@ -181,7 +203,8 @@ private extension AnalyticsHubViewModel {
181203
func switchToLoadingState() {
182204
self.revenueCard = revenueCard.redacted
183205
self.ordersCard = ordersCard.redacted
184-
self.productCard = productCard.redacted
206+
self.productsStatsCard = productsStatsCard.redacted
207+
self.itemsSoldCard = itemsSoldCard.redacted
185208
}
186209

187210
@MainActor
@@ -192,25 +215,33 @@ private extension AnalyticsHubViewModel {
192215
}
193216

194217
func bindViewModelsWithData() {
195-
Publishers.CombineLatest3($currentOrderStats, $previousOrderStats, $itemsSoldStats)
196-
.sink { [weak self] currentOrderStats, previousOrderStats, itemsSoldStats in
218+
Publishers.CombineLatest($currentOrderStats, $previousOrderStats)
219+
.sink { [weak self] currentOrderStats, previousOrderStats in
197220
guard let self else { return }
198221

199222
self.revenueCard = AnalyticsHubViewModel.revenueCard(currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
200223
self.ordersCard = AnalyticsHubViewModel.ordersCard(currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
201-
self.productCard = AnalyticsHubViewModel.productCard(currentPeriodStats: currentOrderStats,
202-
previousPeriodStats: previousOrderStats,
203-
itemsSoldStats: itemsSoldStats)
224+
self.productsStatsCard = AnalyticsHubViewModel.productsStatsCard(currentPeriodStats: currentOrderStats, previousPeriodStats: previousOrderStats)
204225

205226
}.store(in: &subscriptions)
206227

228+
$itemsSoldStats
229+
.sink { [weak self] itemsSoldStats in
230+
guard let self else { return }
231+
232+
self.itemsSoldCard = AnalyticsHubViewModel.productsItemsSoldCard(itemsSoldStats: itemsSoldStats)
233+
}.store(in: &subscriptions)
234+
207235
$timeRangeSelectionType
236+
.dropFirst() // do not trigger refresh action on initial value
208237
.removeDuplicates()
209238
.sink { [weak self] newSelectionType in
210239
guard let self else { return }
211240
self.timeRangeSelection = AnalyticsHubTimeRangeSelection(selectionType: newSelectionType)
212241
self.timeRangeCard = AnalyticsHubViewModel.timeRangeCard(timeRangeSelection: self.timeRangeSelection,
213242
usageTracksEventEmitter: self.usageTracksEventEmitter)
243+
244+
// Update data on range selection change
214245
Task.init {
215246
await self.updateData()
216247
}
@@ -254,22 +285,26 @@ private extension AnalyticsHubViewModel {
254285
syncErrorMessage: Localization.OrderCard.noOrders)
255286
}
256287

257-
/// Helper function to create a `AnalyticsProductCardViewModel` from the fetched stats.
288+
/// Helper function to create a `AnalyticsProductsStatsCardViewModel` from the fetched stats.
258289
///
259-
static func productCard(currentPeriodStats: OrderStatsV4?,
260-
previousPeriodStats: OrderStatsV4?,
261-
itemsSoldStats: TopEarnerStats?) -> AnalyticsProductCardViewModel {
290+
static func productsStatsCard(currentPeriodStats: OrderStatsV4?,
291+
previousPeriodStats: OrderStatsV4?) -> AnalyticsProductsStatsCardViewModel {
262292
let showStatsError = currentPeriodStats == nil || previousPeriodStats == nil
263-
let showItemsSoldError = itemsSoldStats == nil
264293
let itemsSold = StatsDataTextFormatter.createItemsSoldText(orderStats: currentPeriodStats)
265294
let itemsSoldDelta = StatsDataTextFormatter.createOrderItemsSoldDelta(from: previousPeriodStats, to: currentPeriodStats)
266295

267-
return AnalyticsProductCardViewModel(itemsSold: itemsSold,
268-
delta: itemsSoldDelta,
269-
itemsSoldData: itemSoldRows(from: itemsSoldStats),
270-
isRedacted: false,
271-
showStatsError: showStatsError,
272-
showItemsSoldError: showItemsSoldError)
296+
return AnalyticsProductsStatsCardViewModel(itemsSold: itemsSold,
297+
delta: itemsSoldDelta,
298+
isRedacted: false,
299+
showStatsError: showStatsError)
300+
}
301+
302+
/// Helper function to create a `AnalyticsItemsSoldViewModel` from the fetched stats.
303+
///
304+
static func productsItemsSoldCard(itemsSoldStats: TopEarnerStats?) -> AnalyticsItemsSoldViewModel {
305+
let showItemsSoldError = itemsSoldStats == nil
306+
307+
return AnalyticsItemsSoldViewModel(itemsSoldData: itemSoldRows(from: itemsSoldStats), isRedacted: false, showItemsSoldError: showItemsSoldError)
273308
}
274309

275310
/// Helper functions to create `TopPerformersRow.Data` items rom the provided `TopEarnerStats`.

WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/AnalyticsProductCard.swift

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@ struct AnalyticsProductCard: View {
1717
/// Delta Tag text color.
1818
let deltaTextColor: UIColor
1919

20-
/// Items Solds data to render.
21-
///
22-
let itemsSoldData: [TopPerformersRow.Data]
23-
2420
/// Indicates if the values should be hidden (for loading state)
2521
///
26-
let isRedacted: Bool
22+
let isStatsRedacted: Bool
2723

2824
/// Indicates if there was an error loading stats part of the card.
2925
///
3026
let showStatsError: Bool
3127

28+
/// Items Solds data to render.
29+
///
30+
let itemsSoldData: [TopPerformersRow.Data]
31+
32+
/// Indicates if the values should be hidden (for loading state)
33+
///
34+
let isItemsSoldRedacted: Bool
35+
3236
/// Indicates if there was an error loading items sold part of the card.
3337
///
3438
let showItemsSoldError: Bool
@@ -49,12 +53,12 @@ struct AnalyticsProductCard: View {
4953
Text(itemsSold)
5054
.titleStyle()
5155
.frame(maxWidth: .infinity, alignment: .leading)
52-
.redacted(reason: isRedacted ? .placeholder : [])
53-
.shimmering(active: isRedacted)
56+
.redacted(reason: isStatsRedacted ? .placeholder : [])
57+
.shimmering(active: isStatsRedacted)
5458

5559
DeltaTag(value: delta, backgroundColor: deltaBackgroundColor, textColor: deltaTextColor)
56-
.redacted(reason: isRedacted ? .placeholder : [])
57-
.shimmering(active: isRedacted)
60+
.redacted(reason: isStatsRedacted ? .placeholder : [])
61+
.shimmering(active: isStatsRedacted)
5862
}
5963

6064
if showStatsError {
@@ -65,10 +69,11 @@ struct AnalyticsProductCard: View {
6569
.padding(.top, Layout.columnSpacing)
6670
}
6771

68-
TopPerformersView(itemTitle: Localization.title.localizedCapitalized, valueTitle: Localization.itemsSold, rows: itemsSoldData)
72+
TopPerformersView(itemTitle: Localization.title.localizedCapitalized,
73+
valueTitle: Localization.itemsSold,
74+
rows: itemsSoldData,
75+
isRedacted: isItemsSoldRedacted)
6976
.padding(.top, Layout.columnSpacing)
70-
.redacted(reason: isRedacted ? .placeholder : [])
71-
.shimmering(active: isRedacted)
7277

7378
if showItemsSoldError {
7479
Text(Localization.noItemsSold)
@@ -109,24 +114,26 @@ struct AnalyticsProductCardPreviews: PreviewProvider {
109114
delta: "+23%",
110115
deltaBackgroundColor: .withColorStudio(.green, shade: .shade50),
111116
deltaTextColor: .textInverted,
117+
isStatsRedacted: false,
118+
showStatsError: false,
112119
itemsSoldData: [
113120
.init(imageURL: imageURL, name: "Tabletop Photos", details: "Net Sales: $1,232", value: "32"),
114121
.init(imageURL: imageURL, name: "Kentya Palm", details: "Net Sales: $800", value: "10"),
115122
.init(imageURL: imageURL, name: "Love Ficus", details: "Net Sales: $599", value: "5"),
116123
.init(imageURL: imageURL, name: "Bird Of Paradise", details: "Net Sales: $23.50", value: "2"),
117124
],
118-
isRedacted: false,
119-
showStatsError: false,
125+
isItemsSoldRedacted: false,
120126
showItemsSoldError: false)
121127
.previewLayout(.sizeThatFits)
122128

123129
AnalyticsProductCard(itemsSold: "-",
124130
delta: "0%",
125131
deltaBackgroundColor: .withColorStudio(.gray, shade: .shade0),
126132
deltaTextColor: .text,
127-
itemsSoldData: [],
128-
isRedacted: false,
133+
isStatsRedacted: false,
129134
showStatsError: true,
135+
itemsSoldData: [],
136+
isItemsSoldRedacted: false,
130137
showItemsSoldError: true)
131138
.previewLayout(.sizeThatFits)
132139
.previewDisplayName("No data")
Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Foundation
22
import class UIKit.UIColor
33

4-
/// Analytics Hub Product Card ViewModel.
4+
/// Analytics Hub Products Stats Card ViewModel.
55
/// Used to transmit analytics products data.
66
///
7-
struct AnalyticsProductCardViewModel {
7+
struct AnalyticsProductsStatsCardViewModel {
88
/// Items Sold Value
99
///
1010
let itemsSold: String
@@ -13,51 +13,73 @@ struct AnalyticsProductCardViewModel {
1313
///
1414
let delta: DeltaPercentage
1515

16-
/// Items Solds data to render.
17-
///
18-
let itemsSoldData: [TopPerformersRow.Data]
19-
2016
/// Indicates if the values should be hidden (for loading state)
2117
///
2218
let isRedacted: Bool
2319

2420
/// Indicates if there was an error loading stats part of the card.
2521
///
2622
let showStatsError: Bool
23+
}
24+
25+
/// Analytics Hub Items Sold ViewModel.
26+
/// Used to store top performing products data.
27+
///
28+
struct AnalyticsItemsSoldViewModel {
29+
30+
/// Items Solds data to render.
31+
///
32+
let itemsSoldData: [TopPerformersRow.Data]
33+
34+
/// Indicates if the values should be hidden (for loading state)
35+
///
36+
let isRedacted: Bool
2737

2838
/// Indicates if there was an error loading items sold part of the card.
2939
///
3040
let showItemsSoldError: Bool
3141
}
3242

33-
extension AnalyticsProductCardViewModel {
43+
extension AnalyticsProductsStatsCardViewModel {
3444

3545
/// Make redacted state of the card, replacing values with hardcoded placeholders
3646
///
3747
var redacted: Self {
3848
// Values here are placeholders and will be redacted in the UI
3949
.init(itemsSold: "1000",
4050
delta: DeltaPercentage(string: "0%", direction: .zero),
41-
itemsSoldData: [.init(imageURL: nil, name: "Product Name", details: "Net Sales", value: "$5678")],
4251
isRedacted: true,
43-
showStatsError: false,
44-
showItemsSoldError: false)
52+
showStatsError: false)
4553
}
54+
}
55+
56+
extension AnalyticsItemsSoldViewModel {
4657

58+
/// Make redacted state of the card, replacing values with hardcoded placeholders
59+
///
60+
var redacted: Self {
61+
// Values here are placeholders and will be redacted in the UI
62+
.init(itemsSoldData: [.init(imageURL: nil, name: "Product Name", details: "Net Sales", value: "$5678")],
63+
isRedacted: true,
64+
showItemsSoldError: false)
65+
}
4766
}
4867

49-
/// Convenience extension to create an `AnalyticsReportCard` from a view model.
68+
/// Convenience extension to create an `AnalyticsProductCard` from a view model.
5069
///
5170
extension AnalyticsProductCard {
52-
init(viewModel: AnalyticsProductCardViewModel) {
53-
self.itemsSold = viewModel.itemsSold
54-
self.delta = viewModel.delta.string
55-
self.deltaBackgroundColor = viewModel.delta.direction.deltaBackgroundColor
56-
self.deltaTextColor = viewModel.delta.direction.deltaTextColor
57-
self.itemsSoldData = viewModel.itemsSoldData
58-
self.isRedacted = viewModel.isRedacted
59-
self.showStatsError = viewModel.showStatsError
60-
self.showItemsSoldError = viewModel.showItemsSoldError
71+
init(statsViewModel: AnalyticsProductsStatsCardViewModel, itemsViewModel: AnalyticsItemsSoldViewModel) {
72+
// Header with stats
73+
self.itemsSold = statsViewModel.itemsSold
74+
self.delta = statsViewModel.delta.string
75+
self.deltaBackgroundColor = statsViewModel.delta.direction.deltaBackgroundColor
76+
self.deltaTextColor = statsViewModel.delta.direction.deltaTextColor
77+
self.isStatsRedacted = statsViewModel.isRedacted
78+
self.showStatsError = statsViewModel.showStatsError
6179

80+
// Top performers list
81+
self.itemsSoldData = itemsViewModel.itemsSoldData
82+
self.isItemsSoldRedacted = itemsViewModel.isRedacted
83+
self.showItemsSoldError = itemsViewModel.showItemsSoldError
6284
}
6385
}

WooCommerce/Classes/ViewRelated/Dashboard/MyStore/TopPerformersView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ struct TopPerformersView: View {
1717
///
1818
let rows: [TopPerformersRow.Data]
1919

20+
/// Indicates if the values should be hidden (for loading state)
21+
///
22+
let isRedacted: Bool
23+
2024
/// Used to track the text margin value from the top performers row.
2125
/// Text margin value: Where the row text is placed in the X position.
2226
/// Needed to properly layout the row divider.
@@ -42,6 +46,8 @@ struct TopPerformersView: View {
4246
// Do not render the divider for the last row.
4347
TopPerformersRow(data: row, showDivider: index < rows.count - 1)
4448
}
49+
.redacted(reason: isRedacted ? .placeholder : [])
50+
.shimmering(active: isRedacted)
4551
}
4652
}
4753
}
@@ -141,7 +147,8 @@ struct TopPerformersPreview: PreviewProvider {
141147
.init(imageURL: imageURL, name: "Kentya Palm", details: "Net Sales: $800", value: "10"),
142148
.init(imageURL: imageURL, name: "Love Ficus", details: "Net Sales: $599", value: "5"),
143149
.init(imageURL: imageURL, name: "Bird Of Paradise", details: "Net Sales: $23.50", value: "2")
144-
])
150+
],
151+
isRedacted: false)
145152
.padding()
146153
.previewLayout(.sizeThatFits)
147154
}

0 commit comments

Comments
 (0)