Skip to content

Commit dc1f4ba

Browse files
authored
Merge pull request #8255 from woocommerce/issue/8189-loading-state
[Analytics Hub] Add loading state
2 parents 577ac14 + 71eaaa9 commit dc1f4ba

File tree

8 files changed

+163
-13
lines changed

8 files changed

+163
-13
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ struct AnalyticsHubView: View {
7575
.navigationBarTitleDisplayMode(.inline)
7676
.background(Color(uiColor: .listBackground))
7777
.edgesIgnoringSafeArea(.horizontal)
78+
.task {
79+
await viewModel.updateData()
80+
}
7881
}
7982
}
8083

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,6 @@ final class AnalyticsHubViewModel: ObservableObject {
2424
previousRangeSubtitle: timeRangeGenerator.generatePreviousRangeDescription())
2525

2626
bindViewModelsWithData()
27-
Task.init {
28-
do {
29-
try await retrieveOrderStats()
30-
} catch {
31-
DDLogWarn("⚠️ Error fetching analytics data: \(error)")
32-
}
33-
}
3427
}
3528

3629
/// Revenue Card ViewModel
@@ -58,13 +51,26 @@ final class AnalyticsHubViewModel: ObservableObject {
5851
/// Order stats for the previous time period (for comparison)
5952
///
6053
@Published private var previousOrderStats: OrderStatsV4? = nil
54+
55+
/// Request stats data from network
56+
///
57+
func updateData() async {
58+
do {
59+
try await retrieveOrderStats()
60+
} catch {
61+
switchToErrorState()
62+
DDLogWarn("⚠️ Error fetching analytics data: \(error)")
63+
}
64+
}
6165
}
6266

6367
// MARK: Networking
6468
private extension AnalyticsHubViewModel {
6569

6670
@MainActor
6771
func retrieveOrderStats() async throws {
72+
switchToLoadingState()
73+
6874
let currentTimeRange = try timeRangeGenerator.unwrapCurrentTimeRange()
6975
let previousTimeRange = try timeRangeGenerator.unwrapPreviousTimeRange()
7076

@@ -104,6 +110,17 @@ private extension AnalyticsHubViewModel {
104110
// MARK: Data - UI mapping
105111
private extension AnalyticsHubViewModel {
106112

113+
func switchToLoadingState() {
114+
self.revenueCard = revenueCard.redacted
115+
self.ordersCard = ordersCard.redacted
116+
self.productCard = productCard.redacted
117+
}
118+
119+
func switchToErrorState() {
120+
self.currentOrderStats = nil
121+
self.previousOrderStats = nil
122+
}
123+
107124
func bindViewModelsWithData() {
108125
Publishers.CombineLatest($currentOrderStats, $previousOrderStats)
109126
.sink { [weak self] currentOrderStats, previousOrderStats in
@@ -131,7 +148,8 @@ private extension AnalyticsHubViewModel {
131148
trailingValue: StatsDataTextFormatter.createNetRevenueText(orderStats: currentPeriodStats),
132149
trailingDelta: netDelta.string,
133150
trailingDeltaColor: Constants.deltaColor(for: netDelta.direction),
134-
trailingChartData: StatsIntervalDataParser.getChartData(for: .netRevenue, from: currentPeriodStats))
151+
trailingChartData: StatsIntervalDataParser.getChartData(for: .netRevenue, from: currentPeriodStats),
152+
isRedacted: false)
135153
}
136154

137155
static func ordersCard(currentPeriodStats: OrderStatsV4?, previousPeriodStats: OrderStatsV4?) -> AnalyticsReportCardViewModel {
@@ -149,7 +167,8 @@ private extension AnalyticsHubViewModel {
149167
trailingValue: StatsDataTextFormatter.createAverageOrderValueText(orderStats: currentPeriodStats),
150168
trailingDelta: orderValueDelta.string,
151169
trailingDeltaColor: Constants.deltaColor(for: orderValueDelta.direction),
152-
trailingChartData: StatsIntervalDataParser.getChartData(for: .averageOrderValue, from: currentPeriodStats))
170+
trailingChartData: StatsIntervalDataParser.getChartData(for: .averageOrderValue, from: currentPeriodStats),
171+
isRedacted: false)
153172
}
154173

155174
static func productCard(currentPeriodStats: OrderStatsV4?, previousPeriodStats: OrderStatsV4?) -> AnalyticsProductCardViewModel {
@@ -158,7 +177,8 @@ private extension AnalyticsHubViewModel {
158177

159178
return AnalyticsProductCardViewModel(itemsSold: itemsSold,
160179
delta: itemsSoldDelta.string,
161-
deltaBackgroundColor: Constants.deltaColor(for: itemsSoldDelta.direction))
180+
deltaBackgroundColor: Constants.deltaColor(for: itemsSoldDelta.direction),
181+
isRedacted: false)
162182
}
163183
}
164184

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ struct AnalyticsProductCard: View {
1414
/// Delta Tag background color.
1515
let deltaBackgroundColor: UIColor
1616

17+
/// Indicates if the values should be hidden (for loading state)
18+
///
19+
let isRedacted: Bool
20+
1721
var body: some View {
1822
VStack(alignment: .leading) {
1923

@@ -30,8 +34,12 @@ struct AnalyticsProductCard: View {
3034
Text(itemsSold)
3135
.titleStyle()
3236
.frame(maxWidth: .infinity, alignment: .leading)
37+
.redacted(reason: isRedacted ? .placeholder : [])
38+
.shimmering(active: isRedacted)
3339

3440
DeltaTag(value: delta, backgroundColor: deltaBackgroundColor)
41+
.redacted(reason: isRedacted ? .placeholder : [])
42+
.shimmering(active: isRedacted)
3543
}
3644
}
3745
.padding(Layout.cardPadding)
@@ -56,7 +64,10 @@ private extension AnalyticsProductCard {
5664
// MARK: Previews
5765
struct AnalyticsProductCardPreviews: PreviewProvider {
5866
static var previews: some View {
59-
AnalyticsProductCard(itemsSold: "2,234", delta: "+23%", deltaBackgroundColor: .withColorStudio(.green, shade: .shade50))
67+
AnalyticsProductCard(itemsSold: "2,234",
68+
delta: "+23%",
69+
deltaBackgroundColor: .withColorStudio(.green, shade: .shade50),
70+
isRedacted: false)
6071
.previewLayout(.sizeThatFits)
6172
}
6273
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ struct AnalyticsProductCardViewModel {
1616
/// Delta background color.
1717
///
1818
let deltaBackgroundColor: UIColor
19+
20+
/// Indicates if the values should be hidden (for loading state)
21+
///
22+
let isRedacted: Bool
23+
}
24+
25+
extension AnalyticsProductCardViewModel {
26+
27+
/// Make redacted state of the card, replacing values with hardcoded placeholders
28+
///
29+
var redacted: Self {
30+
// Values here are placeholders and will be redacted in the UI
31+
.init(itemsSold: "1000",
32+
delta: "+50%",
33+
deltaBackgroundColor: .lightGray,
34+
isRedacted: true)
35+
}
1936
}
2037

2138
/// Convenience extension to create an `AnalyticsReportCard` from a view model.
@@ -25,5 +42,6 @@ extension AnalyticsProductCard {
2542
self.itemsSold = viewModel.itemsSold
2643
self.delta = viewModel.delta
2744
self.deltaBackgroundColor = viewModel.deltaBackgroundColor
45+
self.isRedacted = viewModel.isRedacted
2846
}
2947
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ struct AnalyticsReportCard: View {
1616
let trailingDeltaColor: UIColor
1717
let trailingChartData: [Double]
1818

19+
let isRedacted: Bool
20+
1921
// Layout metrics that scale based on accessibility changes
2022
@ScaledMetric private var scaledChartWidth: CGFloat = Layout.chartWidth
2123
@ScaledMetric private var scaledChartHeight: CGFloat = Layout.chartHeight
@@ -38,10 +40,14 @@ struct AnalyticsReportCard: View {
3840

3941
Text(leadingValue)
4042
.titleStyle()
43+
.redacted(reason: isRedacted ? .placeholder : [])
44+
.shimmering(active: isRedacted)
4145

4246
AdaptiveStack(horizontalAlignment: .leading) {
4347
DeltaTag(value: leadingDelta, backgroundColor: leadingDeltaColor)
4448
.frame(maxWidth: .infinity, alignment: .leading)
49+
.redacted(reason: isRedacted ? .placeholder : [])
50+
.shimmering(active: isRedacted)
4551

4652
AnalyticsLineChart(dataPoints: leadingChartData, lineChartColor: leadingDeltaColor)
4753
.frame(width: scaledChartWidth, height: scaledChartHeight)
@@ -58,10 +64,14 @@ struct AnalyticsReportCard: View {
5864

5965
Text(trailingValue)
6066
.titleStyle()
67+
.redacted(reason: isRedacted ? .placeholder : [])
68+
.shimmering(active: isRedacted)
6169

6270
AdaptiveStack(horizontalAlignment: .leading) {
6371
DeltaTag(value: trailingDelta, backgroundColor: trailingDeltaColor)
6472
.frame(maxWidth: .infinity, alignment: .leading)
73+
.redacted(reason: isRedacted ? .placeholder : [])
74+
.shimmering(active: isRedacted)
6575

6676
AnalyticsLineChart(dataPoints: trailingChartData, lineChartColor: trailingDeltaColor)
6777
.frame(width: scaledChartWidth, height: scaledChartHeight)
@@ -99,7 +109,8 @@ struct Previews: PreviewProvider {
99109
trailingValue: "$3.232",
100110
trailingDelta: "-3%",
101111
trailingDeltaColor: .withColorStudio(.red, shade: .shade40),
102-
trailingChartData: [50.0, 15.0, 20.0, 2.0, 10.0, 0.0, 40.0, 15.0, 20.0, 2.0, 10.0, 0.0])
112+
trailingChartData: [50.0, 15.0, 20.0, 2.0, 10.0, 0.0, 40.0, 15.0, 20.0, 2.0, 10.0, 0.0],
113+
isRedacted: false)
103114
.previewLayout(.sizeThatFits)
104115
}
105116
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct AnalyticsReportCardViewModel {
2929
///
3030
let leadingChartData: [Double]
3131

32-
/// Second Column Titlke
32+
/// Second Column Title
3333
///
3434
let trailingTitle: String
3535

@@ -48,6 +48,31 @@ struct AnalyticsReportCardViewModel {
4848
/// Second Column Chart Data
4949
///
5050
let trailingChartData: [Double]
51+
52+
/// Indicates if the values should be hidden (for loading state)
53+
///
54+
let isRedacted: Bool
55+
}
56+
57+
extension AnalyticsReportCardViewModel {
58+
59+
/// Make redacted state of the card, replacing values with hardcoded placeholders
60+
///
61+
var redacted: Self {
62+
// Values here are placeholders and will be redacted in the UI
63+
.init(title: title,
64+
leadingTitle: leadingTitle,
65+
leadingValue: "$1000",
66+
leadingDelta: "+50%",
67+
leadingDeltaColor: .lightGray,
68+
leadingChartData: [],
69+
trailingTitle: trailingTitle,
70+
trailingValue: "$1000",
71+
trailingDelta: "+50%",
72+
trailingDeltaColor: .lightGray,
73+
trailingChartData: [],
74+
isRedacted: true)
75+
}
5176
}
5277

5378
/// Convenience extension to create an `AnalyticsReportCard` from a view model.
@@ -65,5 +90,6 @@ extension AnalyticsReportCard {
6590
self.trailingDelta = viewModel.trailingDelta
6691
self.trailingDeltaColor = viewModel.trailingDeltaColor
6792
self.trailingChartData = viewModel.trailingChartData
93+
self.isRedacted = viewModel.isRedacted
6894
}
6995
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,7 @@
11781178
AE3AA88D290C30E800BE422D /* WebProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3AA88C290C30E800BE422D /* WebProgressView.swift */; };
11791179
AE3AA890290C313600BE422D /* NavigationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3AA88F290C313600BE422D /* NavigationTitleView.swift */; };
11801180
AE457813275644590092F687 /* OrderStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE457812275644590092F687 /* OrderStatusSection.swift */; };
1181+
AE4CCCEB29365CFD00B47EE8 /* AnalyticsHubViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4CCCEA29365CFD00B47EE8 /* AnalyticsHubViewModelTests.swift */; };
11811182
AE56E73428E76CDB00A1292B /* StoreInfoInlineWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE56E73328E76CDB00A1292B /* StoreInfoInlineWidget.swift */; };
11821183
AE56E73628E7787700A1292B /* StoreInfoCircularWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE56E73528E7787700A1292B /* StoreInfoCircularWidget.swift */; };
11831184
AE56E73828E7869800A1292B /* StoreInfoRectangularWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE56E73728E7869800A1292B /* StoreInfoRectangularWidget.swift */; };
@@ -3154,6 +3155,7 @@
31543155
AE3AA88C290C30E800BE422D /* WebProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebProgressView.swift; sourceTree = "<group>"; };
31553156
AE3AA88F290C313600BE422D /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = "<group>"; };
31563157
AE457812275644590092F687 /* OrderStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatusSection.swift; sourceTree = "<group>"; };
3158+
AE4CCCEA29365CFD00B47EE8 /* AnalyticsHubViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubViewModelTests.swift; sourceTree = "<group>"; };
31573159
AE56E73328E76CDB00A1292B /* StoreInfoInlineWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInfoInlineWidget.swift; sourceTree = "<group>"; };
31583160
AE56E73528E7787700A1292B /* StoreInfoCircularWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInfoCircularWidget.swift; sourceTree = "<group>"; };
31593161
AE56E73728E7869800A1292B /* StoreInfoRectangularWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInfoRectangularWidget.swift; sourceTree = "<group>"; };
@@ -7415,6 +7417,7 @@
74157417
B6440FB7292E73E50012D506 /* Analytics Hub */ = {
74167418
isa = PBXGroup;
74177419
children = (
7420+
AE4CCCEA29365CFD00B47EE8 /* AnalyticsHubViewModelTests.swift */,
74187421
B6440FB8292E74230012D506 /* AnalyticsHubTimeRangeGeneratorTests.swift */,
74197422
);
74207423
path = "Analytics Hub";
@@ -11127,6 +11130,7 @@
1112711130
02B653AC2429F7BF00A9C839 /* MockTaxClassStoresManager.swift in Sources */,
1112811131
0277AEA5256CAA4200F45C4A /* MockShippingLabel.swift in Sources */,
1112911132
02BAB02324D0250300F8B06E /* ProductVariation+ProductFormTests.swift in Sources */,
11133+
AE4CCCEB29365CFD00B47EE8 /* AnalyticsHubViewModelTests.swift in Sources */,
1113011134
025C00CC2551524300FAC222 /* BarcodeScannerFrameScalerTests.swift in Sources */,
1113111135
02B1AFEE24BC5BA9005DB1E3 /* LinkedProductListSelectorDataSourceTests.swift in Sources */,
1113211136
028E19BA28053443001C36E0 /* MockOrderDetailsPaymentAlerts.swift in Sources */,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import XCTest
2+
import Yosemite
3+
@testable import WooCommerce
4+
5+
final class AnalyticsHubViewModelTests: XCTestCase {
6+
7+
private var stores: MockStoresManager!
8+
9+
override func setUp() {
10+
stores = MockStoresManager(sessionManager: .makeForTesting())
11+
}
12+
13+
func test_cards_viewmodels_show_correct_data_after_updating_from_network() async {
14+
// Given
15+
let vm = AnalyticsHubViewModel(siteID: 123, statsTimeRange: .thisMonth, stores: stores)
16+
let stats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 15, totalItemsSold: 5, grossRevenue: 62))
17+
stores.whenReceivingAction(ofType: StatsActionV4.self) { action in
18+
if case let .retrieveCustomStats(_, _, _, _, _, _, completion) = action {
19+
completion(.success(stats))
20+
}
21+
}
22+
23+
// When
24+
await vm.updateData()
25+
26+
// Then
27+
XCTAssertFalse(vm.revenueCard.isRedacted)
28+
XCTAssertFalse(vm.ordersCard.isRedacted)
29+
XCTAssertFalse(vm.productCard.isRedacted)
30+
31+
XCTAssertEqual(vm.revenueCard.leadingValue, "$62")
32+
XCTAssertEqual(vm.ordersCard.leadingValue, "15")
33+
XCTAssertEqual(vm.productCard.itemsSold, "5")
34+
}
35+
36+
func test_cards_viewmodels_show_empty_data_after_getting_error_from_network() async {
37+
// Given
38+
let vm = AnalyticsHubViewModel(siteID: 123, statsTimeRange: .thisMonth, stores: stores)
39+
stores.whenReceivingAction(ofType: StatsActionV4.self) { action in
40+
if case let .retrieveCustomStats(_, _, _, _, _, _, completion) = action {
41+
completion(.failure(NSError(domain: "Test", code: 1)))
42+
}
43+
}
44+
45+
// When
46+
await vm.updateData()
47+
48+
// Then
49+
XCTAssertFalse(vm.revenueCard.isRedacted)
50+
XCTAssertFalse(vm.ordersCard.isRedacted)
51+
XCTAssertFalse(vm.productCard.isRedacted)
52+
53+
XCTAssertEqual(vm.revenueCard.leadingValue, "-")
54+
XCTAssertEqual(vm.ordersCard.leadingValue, "-")
55+
XCTAssertEqual(vm.productCard.itemsSold, "-")
56+
}
57+
}

0 commit comments

Comments
 (0)