Skip to content

Commit 584bbe8

Browse files
authored
Merge pull request #8339 from woocommerce/issue/8200-ranges-support
Analytics Hub: Add Custom Range Support
2 parents 7071b45 + 198b081 commit 584bbe8

File tree

8 files changed

+146
-20
lines changed

8 files changed

+146
-20
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1806,7 +1806,7 @@ extension WooAnalyticsEvent {
18061806
/// Includes the current device calendar and timezone, for debugging the failure.
18071807
///
18081808
static func dateRangeSelectionFailed(for option: AnalyticsHubTimeRangeSelection.SelectionType) -> WooAnalyticsEvent {
1809-
WooAnalyticsEvent(statName: .analyticsHubDateRangeSelectionFailed, properties: [Keys.option.rawValue: option.rawValue,
1809+
WooAnalyticsEvent(statName: .analyticsHubDateRangeSelectionFailed, properties: [Keys.option.rawValue: option.tracksIdentifier,
18101810
Keys.calendar.rawValue: Locale.current.calendar.debugDescription,
18111811
Keys.timezone.rawValue: TimeZone.current.debugDescription])
18121812
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,18 @@ private extension AnalyticsHubViewModel {
208208
.removeDuplicates()
209209
.sink { [weak self] newSelectionType in
210210
guard let self else { return }
211-
self.timeRangeSelection = AnalyticsHubTimeRangeSelection(selectionType: newSelectionType)
211+
212+
// Temporary while the Custom Range Selection integration
213+
let curatedSelection: AnalyticsHubTimeRangeSelection.SelectionType = {
214+
switch newSelectionType {
215+
case .custom(start: nil, end: nil):
216+
return .custom(start: Date().startOfWeek(timezone: .current), end: Date().endOfDay(timezone: .current))
217+
default:
218+
return newSelectionType
219+
}
220+
}()
221+
222+
self.timeRangeSelection = AnalyticsHubTimeRangeSelection(selectionType: curatedSelection)
212223
self.timeRangeCard = AnalyticsHubViewModel.timeRangeCard(timeRangeSelection: self.timeRangeSelection,
213224
usageTracksEventEmitter: self.usageTracksEventEmitter)
214225
Task.init {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ struct AnalyticsTimeRangeCard: View {
2929
contentKeyPath: \.description,
3030
selected: $selectionType) { selection in
3131
usageTracksEventEmitter.interacted()
32-
ServiceLocator.analytics.track(event: .AnalyticsHub.dateRangeOptionSelected(selection.rawValue))
32+
ServiceLocator.analytics.track(event: .AnalyticsHub.dateRangeOptionSelected(selection.tracksIdentifier))
3333
}
3434
}
3535
}

WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeData.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ protocol AnalyticsHubTimeRangeData {
55
var currentDateEnd: Date? { get }
66
var previousDateStart: Date? { get }
77
var previousDateEnd: Date? { get }
8-
9-
init(referenceDate: Date, timezone: TimeZone, calendar: Calendar)
108
}
119

1210
extension AnalyticsHubTimeRangeData {

WooCommerce/Classes/ViewRelated/Dashboard/Analytics Hub/Time Range/AnalyticsHubTimeRangeSelection.swift

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@ public class AnalyticsHubTimeRangeSelection {
3333
currentDate: Date = Date(),
3434
timezone: TimeZone = TimeZone.current,
3535
calendar: Calendar = Locale.current.calendar) {
36-
let selectionData = selectionType.toRangeData(referenceDate: currentDate, timezone: timezone, calendar: calendar)
36+
37+
// Exit early if we can't generate a selection Data.
38+
guard let selectionData = selectionType.toRangeData(referenceDate: currentDate, timezone: timezone, calendar: calendar) else {
39+
self.currentTimeRange = nil
40+
self.previousTimeRange = nil
41+
self.formattedCurrentRangeText = nil
42+
self.formattedPreviousRangeText = nil
43+
self.rangeSelectionDescription = ""
44+
return
45+
}
46+
3747
let currentTimeRange = selectionData.currentTimeRange
3848
let previousTimeRange = selectionData.previousTimeRange
3949
let useShortFormat = selectionType == .today || selectionType == .yesterday
@@ -67,20 +77,42 @@ public class AnalyticsHubTimeRangeSelection {
6777

6878
// MARK: - Time Range Selection Type
6979
extension AnalyticsHubTimeRangeSelection {
70-
enum SelectionType: String, CaseIterable {
71-
case today = "Today"
72-
case yesterday = "Yesterday"
73-
case lastWeek = "Last Week"
74-
case lastMonth = "Last Month"
75-
case lastQuarter = "Last Quarter"
76-
case lastYear = "Last Year"
77-
case weekToDate = "Week to Date"
78-
case monthToDate = "Month to Date"
79-
case quarterToDate = "Quarter to Date"
80-
case yearToDate = "Year to Date"
80+
enum SelectionType: CaseIterable, Equatable, Hashable {
81+
/// Wee need to provide a custom `allCases` because the `.custom(Date?, Date?)`case disables its synthetization.
82+
///
83+
static var allCases: [AnalyticsHubTimeRangeSelection.SelectionType] {
84+
[
85+
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.analyticsHub) ? .custom(start: nil, end: nil) : nil,
86+
.today,
87+
.yesterday,
88+
.lastWeek,
89+
.lastMonth,
90+
.lastQuarter,
91+
.lastYear,
92+
.weekToDate,
93+
.monthToDate,
94+
.quarterToDate,
95+
yearToDate
96+
].compactMap { $0 }
97+
}
98+
99+
// When adding a new case, remember to add it to `allCases`.
100+
case custom(start: Date?, end: Date?)
101+
case today
102+
case yesterday
103+
case lastWeek
104+
case lastMonth
105+
case lastQuarter
106+
case lastYear
107+
case weekToDate
108+
case monthToDate
109+
case quarterToDate
110+
case yearToDate
81111

82112
var description: String {
83113
switch self {
114+
case .custom:
115+
return Localization.custom
84116
case .today:
85117
return Localization.today
86118
case .yesterday:
@@ -110,7 +142,7 @@ extension AnalyticsHubTimeRangeSelection {
110142
switch self {
111143
case .today, .yesterday:
112144
return .hourly
113-
case .weekToDate, .lastWeek:
145+
case .custom, .weekToDate, .lastWeek:
114146
return .daily
115147
case .monthToDate, .lastMonth:
116148
return .daily
@@ -126,7 +158,7 @@ extension AnalyticsHubTimeRangeSelection {
126158
///
127159
var intervalSize: Int {
128160
switch self {
129-
case .today, .yesterday:
161+
case .custom, .today, .yesterday:
130162
return 24
131163
case .weekToDate, .lastWeek:
132164
return 7
@@ -139,6 +171,33 @@ extension AnalyticsHubTimeRangeSelection {
139171
}
140172
}
141173

174+
var tracksIdentifier: String {
175+
switch self {
176+
case .custom:
177+
return "Custom"
178+
case .today:
179+
return "Today"
180+
case .yesterday:
181+
return "Yesterday"
182+
case .lastWeek:
183+
return "Last Week"
184+
case .lastMonth:
185+
return "Last Month"
186+
case .lastQuarter:
187+
return "Last Quarter"
188+
case .lastYear:
189+
return "Last Year"
190+
case .weekToDate:
191+
return "Week to Date"
192+
case .monthToDate:
193+
return "Month to Date"
194+
case .quarterToDate:
195+
return "Quarter to Date"
196+
case .yearToDate:
197+
return "Year to Date"
198+
}
199+
}
200+
142201
init(_ statsTimeRange: StatsTimeRangeV4) {
143202
switch statsTimeRange {
144203
case .today:
@@ -156,8 +215,14 @@ extension AnalyticsHubTimeRangeSelection {
156215

157216
// MARK: - SelectionType helper functions
158217
private extension AnalyticsHubTimeRangeSelection.SelectionType {
159-
func toRangeData(referenceDate: Date, timezone: TimeZone, calendar: Calendar) -> AnalyticsHubTimeRangeData {
218+
func toRangeData(referenceDate: Date, timezone: TimeZone, calendar: Calendar) -> AnalyticsHubTimeRangeData? {
160219
switch self {
220+
case let .custom(start?, end?):
221+
return AnalyticsHubCustomRangeData(start: start, end: end, timezone: timezone, calendar: calendar)
222+
case .custom:
223+
// Nil custom dates are not supported but can exists when the user has selected the custom range option but hasn't choosen dates yet.
224+
// To properly fix this, we should decouple UI selection types, from ranges selection types.
225+
return nil
161226
case .today:
162227
return AnalyticsHubTodayRangeData(referenceDate: referenceDate, timezone: timezone, calendar: calendar)
163228
case .yesterday:
@@ -190,6 +255,7 @@ extension AnalyticsHubTimeRangeSelection {
190255
}
191256

192257
enum Localization {
258+
static let custom = NSLocalizedString("Custom", comment: "Title of the Analytics Hub Custom selection range")
193259
static let today = NSLocalizedString("Today", comment: "Title of the Analytics Hub Today's selection range")
194260
static let yesterday = NSLocalizedString("Yesterday", comment: "Title of the Analytics Hub Yesterday selection range")
195261
static let lastWeek = NSLocalizedString("Last Week", comment: "Title of the Analytics Hub Last Week selection range")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
/// Responsible for defining two ranges of data based on user provided dates.
4+
/// The current range will be what user provided.
5+
/// The previous range will be a range of the same length ending on the day before the current range starts.
6+
///
7+
/// Current range: Jan 5 - Jan 7, 2022
8+
/// Previous range: Jan 2 - Jan 4, 2022
9+
///
10+
struct AnalyticsHubCustomRangeData: AnalyticsHubTimeRangeData {
11+
var currentDateStart: Date?
12+
var currentDateEnd: Date?
13+
14+
var previousDateStart: Date?
15+
var previousDateEnd: Date?
16+
17+
init(start: Date, end: Date, timezone: TimeZone, calendar: Calendar) {
18+
guard
19+
let dayDifference = calendar.dateComponents([.day], from: start, to: end).day,
20+
let previousEnd = calendar.date(byAdding: .day, value: -1, to: start),
21+
let previousStart = calendar.date(byAdding: .day, value: -dayDifference, to: previousEnd) else {
22+
return
23+
}
24+
25+
self.currentDateStart = start.startOfDay(timezone: timezone)
26+
self.currentDateEnd = end.endOfDay(timezone: timezone)
27+
self.previousDateStart = previousStart.startOfDay(timezone: timezone)
28+
self.previousDateEnd = previousEnd.startOfDay(timezone: timezone)
29+
}
30+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@
632632
26A630FE253F63C300CBC3B1 /* RefundableOrderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A630FD253F63C300CBC3B1 /* RefundableOrderItem.swift */; };
633633
26ABCE532518EAF300721CB0 /* IssueRefundTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ABCE522518EAF300721CB0 /* IssueRefundTableViewCell.swift */; };
634634
26ABCE552518EB0600721CB0 /* IssueRefundTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26ABCE542518EB0600721CB0 /* IssueRefundTableViewCell.xib */; };
635+
26AC0DD92941081500859074 /* AnalyticsHubCustomRangeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC0DD82941081500859074 /* AnalyticsHubCustomRangeData.swift */; };
635636
26AE31B0251E602D004B1BCE /* RefundShippingDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AE31AF251E602D004B1BCE /* RefundShippingDetailsTableViewCell.swift */; };
636637
26AE31B2251E604A004B1BCE /* RefundShippingDetailsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26AE31B1251E604A004B1BCE /* RefundShippingDetailsTableViewCell.xib */; };
637638
26B119B924D0B38F00FED5C7 /* SurveyViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26B119B824D0B38F00FED5C7 /* SurveyViewController.xib */; };
@@ -2654,6 +2655,7 @@
26542655
26A630FD253F63C300CBC3B1 /* RefundableOrderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundableOrderItem.swift; sourceTree = "<group>"; };
26552656
26ABCE522518EAF300721CB0 /* IssueRefundTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueRefundTableViewCell.swift; sourceTree = "<group>"; };
26562657
26ABCE542518EB0600721CB0 /* IssueRefundTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueRefundTableViewCell.xib; sourceTree = "<group>"; };
2658+
26AC0DD82941081500859074 /* AnalyticsHubCustomRangeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubCustomRangeData.swift; sourceTree = "<group>"; };
26572659
26AE31AF251E602D004B1BCE /* RefundShippingDetailsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundShippingDetailsTableViewCell.swift; sourceTree = "<group>"; };
26582660
26AE31B1251E604A004B1BCE /* RefundShippingDetailsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundShippingDetailsTableViewCell.xib; sourceTree = "<group>"; };
26592661
26B119B824D0B38F00FED5C7 /* SurveyViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SurveyViewController.xib; sourceTree = "<group>"; };
@@ -7511,6 +7513,7 @@
75117513
B66D6CEF293A4D990075D4AF /* Range Data Generation */ = {
75127514
isa = PBXGroup;
75137515
children = (
7516+
26AC0DD82941081500859074 /* AnalyticsHubCustomRangeData.swift */,
75147517
B6F3796F293798ED00718561 /* AnalyticsHubTodayRangeData.swift */,
75157518
B6E7DB63293A7C390049B001 /* AnalyticsHubYesterdayRangeData.swift */,
75167519
B6C78B8B293BADAA008934A1 /* AnalyticsHubLastWeekRangeData.swift */,
@@ -10285,6 +10288,7 @@
1028510288
317F679826420E9D00BA2A7A /* CardReaderSettingsViewModelsOrderedList.swift in Sources */,
1028610289
DE279BA426E9C4DC002BA963 /* ShippingLabelPackagesForm.swift in Sources */,
1028710290
CE583A0421076C0100D73C1C /* NewNoteViewController.swift in Sources */,
10291+
26AC0DD92941081500859074 /* AnalyticsHubCustomRangeData.swift in Sources */,
1028810292
CECC759723D607C900486676 /* OrderItemRefund+Woo.swift in Sources */,
1028910293
03EF250228C615A5006A033E /* InPersonPaymentsMenuViewModel.swift in Sources */,
1029010294
0212275C244972660042161F /* BottomSheetListSelectorSectionHeaderView.swift in Sources */,

WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Analytics Hub/AnalyticsHubTimeRangeSelectionTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,23 @@ final class AnalyticsHubTimeRangeSelectionTests: XCTestCase {
416416
XCTAssertEqual(previousRangeDescription, "Jun 30, 2022")
417417
}
418418

419+
func test_custom_ranges_generates_expected_descriptions() throws {
420+
// Given
421+
let start = startDate(from: "2022-12-05")
422+
let end = endDate(from: "2022-12-07")
423+
let timeRange = AnalyticsHubTimeRangeSelection(selectionType: .custom(start: start, end: end),
424+
timezone: testTimezone,
425+
calendar: testCalendar)
426+
427+
// When
428+
let currentRangeDescription = timeRange.currentRangeDescription
429+
let previousRangeDescription = timeRange.previousRangeDescription
430+
431+
// Then
432+
XCTAssertEqual(currentRangeDescription, "Dec 5 - 7, 2022")
433+
XCTAssertEqual(previousRangeDescription, "Dec 2 - 4, 2022")
434+
}
435+
419436
private func currentDate(from date: String) -> Date {
420437
let dateFormatter = DateFormatter()
421438
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"

0 commit comments

Comments
 (0)