Skip to content

Commit a894857

Browse files
committed
Merge branch 'trunk' into feat/WOOMOB-935-refactor-pos-entry-point
# Conflicts: # WooCommerce/Classes/POS/Models/POSIneligibleReason.swift # WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift # WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift
2 parents 72c52be + 6a1c368 commit a894857

File tree

78 files changed

+2463
-494
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2463
-494
lines changed

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
swiftlint_version: 0.58.2
22

33
excluded:
4+
- build
45
- BuildTools/.build
56
- DerivedData
67
- fastlane

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
8484
return false
8585
case .inventoryProductLabelsInPOS:
8686
return false
87-
case .pointOfSaleReceipts:
88-
return true
8987
case .productImageOptimizedHandling:
9088
return true
9189
case .pointOfSaleOrdersi1:

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,6 @@ public enum FeatureFlag: Int {
179179
///
180180
case inventoryProductLabelsInPOS
181181

182-
/// Enables sending POS specific email receipts for eligible stores
183-
///
184-
case pointOfSaleReceipts
185-
186182
/// Enables displaying Point Of Sale details in order list and order details
187183
///
188184
case pointOfSaleOrdersi1
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Foundation
2+
3+
/// Tracks the waiting time for a given scenario, allowing to evaluate as analytics
4+
/// how much time in seconds it took between the init and `end` function call
5+
///
6+
public class WaitingTimeTracker {
7+
private let trackScenario: WooAnalyticsEvent.WaitingTime.Scenario
8+
private let currentTimestampSeconds: () -> TimeInterval
9+
private let waitingStartedTimestamp: TimeInterval
10+
11+
public enum TrackingUnit {
12+
case seconds
13+
case milliseconds
14+
}
15+
16+
public init(trackScenario: WooAnalyticsEvent.WaitingTime.Scenario,
17+
currentTimestampSeconds: @escaping () -> TimeInterval = { Date().timeIntervalSince1970 }
18+
) {
19+
self.trackScenario = trackScenario
20+
self.currentTimestampSeconds = currentTimestampSeconds
21+
waitingStartedTimestamp = currentTimestampSeconds()
22+
}
23+
24+
/// Default `end()` method to preserve interface compatibility. By default, tracks in `.seconds`
25+
/// - Returns: The analytics event to be tracked.
26+
///
27+
public func end() -> WooAnalyticsEvent {
28+
end(using: .seconds)
29+
}
30+
31+
/// End the waiting time by evaluating the elapsed time from the init,
32+
/// and returning an analytics event for tracking.
33+
///
34+
/// - Parameter trackingUnit: Defines whether the elapsed time should be tracked in `.seconds` or `.milliseconds` (default is `.seconds`).
35+
/// - Returns: The analytics event to be tracked.
36+
///
37+
public func end(using trackingUnit: TrackingUnit = .seconds) -> WooAnalyticsEvent {
38+
let elapsedTime = calculateElapsedTime(in: trackingUnit)
39+
return .WaitingTime.waitingFinished(scenario: trackScenario, elapsedTime: elapsedTime)
40+
}
41+
42+
/// Calculates elapsed time in the specified tracking unit.
43+
///
44+
private func calculateElapsedTime(in trackingUnit: TrackingUnit) -> TimeInterval {
45+
let elapsedTime = currentTimestampSeconds() - waitingStartedTimestamp
46+
return trackingUnit == .milliseconds ? elapsedTime * 1000 : elapsedTime
47+
}
48+
}
49+
50+
// MARK: - Waiting Time measurement
51+
//
52+
public extension WooAnalyticsEvent {
53+
enum WaitingTime {
54+
/// Possible Waiting time scenarios
55+
public enum Scenario {
56+
case orderDetails
57+
case dashboardTopPerformers
58+
case dashboardMainStats
59+
case analyticsHub
60+
case appStartup
61+
case pointOfSaleLoaded
62+
}
63+
64+
private enum Keys {
65+
static let waitingTime = "waiting_time"
66+
static let millisecondsTimeElapsedInSplashScreen = "milliseconds_time_elapsed_in_splash_screen"
67+
}
68+
69+
static func waitingFinished(scenario: Scenario, elapsedTime: TimeInterval) -> WooAnalyticsEvent {
70+
switch scenario {
71+
case .orderDetails:
72+
return WooAnalyticsEvent(statName: .orderDetailWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
73+
case .dashboardTopPerformers:
74+
return WooAnalyticsEvent(statName: .dashboardTopPerformersWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
75+
case .dashboardMainStats:
76+
return WooAnalyticsEvent(statName: .dashboardMainStatsWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
77+
case .analyticsHub:
78+
return WooAnalyticsEvent(statName: .analyticsHubWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
79+
case .appStartup:
80+
return WooAnalyticsEvent(statName: .applicationOpenedWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
81+
case .pointOfSaleLoaded:
82+
return WooAnalyticsEvent(statName: .pointOfSaleLoaded, properties: [Keys.millisecondsTimeElapsedInSplashScreen: elapsedTime])
83+
}
84+
}
85+
}
86+
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ public enum BookingAction: Action {
1414
pageNumber: Int,
1515
pageSize: Int = BookingsRemote.Default.pageSize,
1616
onCompletion: (Result<Bool, Error>) -> Void)
17+
18+
/// Checks if the store already has any bookings.
19+
/// Returns `false` if the store has no bookings.
20+
///
21+
case checkIfStoreHasBookings(siteID: Int64,
22+
onCompletion: (Result<Bool, Error>) -> Void)
1723
}

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class BookingStore: Store {
3737
switch action {
3838
case let .synchronizeBookings(siteID, pageNumber, pageSize, onCompletion):
3939
synchronizeBookings(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
40+
case let .checkIfStoreHasBookings(siteID, onCompletion):
41+
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
4042
}
4143
}
4244
}
@@ -65,6 +67,31 @@ private extension BookingStore {
6567
}
6668
}
6769
}
70+
71+
/// Checks if the store already has any bookings.
72+
/// Returns `false` if the store has no bookings.
73+
///
74+
func checkIfStoreHasBookings(siteID: Int64, onCompletion: @escaping (Result<Bool, Error>) -> Void) {
75+
let derivedStorage = storageManager.viewStorage
76+
let hasLocalBookings = derivedStorage.countObjects(ofType: StorageBooking.self, matching: NSPredicate(format: "siteID == %lld", siteID)) > 0
77+
78+
if hasLocalBookings {
79+
onCompletion(.success(true))
80+
return
81+
}
82+
83+
Task { @MainActor in
84+
do {
85+
let bookings = try await remote.loadAllBookings(for: siteID,
86+
pageNumber: 1,
87+
pageSize: 1)
88+
let hasRemoteBookings = !bookings.isEmpty
89+
onCompletion(.success(hasRemoteBookings))
90+
} catch {
91+
onCompletion(.failure(error))
92+
}
93+
}
94+
}
6895
}
6996

7097

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
import GRDB
3+
import protocol Storage.GRDBManagerProtocol
4+
5+
public protocol POSCatalogSettingsServiceProtocol {
6+
/// Gets catalog information for the specified site.
7+
/// - Parameter siteID: The site ID to get catalog information for.
8+
/// - Returns: Catalog information including statistics and sync dates.
9+
func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo
10+
}
11+
12+
public struct POSCatalogInfo {
13+
public let productCount: Int
14+
public let variationCount: Int
15+
public let lastFullSyncDate: Date?
16+
public let lastIncrementalSyncDate: Date?
17+
18+
public init(productCount: Int, variationCount: Int, lastFullSyncDate: Date?, lastIncrementalSyncDate: Date?) {
19+
self.productCount = productCount
20+
self.variationCount = variationCount
21+
self.lastFullSyncDate = lastFullSyncDate
22+
self.lastIncrementalSyncDate = lastIncrementalSyncDate
23+
}
24+
}
25+
26+
public class POSCatalogSettingsService: POSCatalogSettingsServiceProtocol {
27+
private let grdbManager: GRDBManagerProtocol
28+
29+
public init(grdbManager: GRDBManagerProtocol) {
30+
self.grdbManager = grdbManager
31+
}
32+
33+
public func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo {
34+
try await grdbManager.databaseConnection.read { db in
35+
let productCount = try PersistedProduct.filter { $0.siteID == siteID }.fetchCount(db)
36+
let variationCount = try PersistedProductVariation.filter { $0.siteID == siteID }.fetchCount(db)
37+
let site = try PersistedSite.filter(key: siteID).fetchOne(db)
38+
return POSCatalogInfo(
39+
productCount: productCount,
40+
variationCount: variationCount,
41+
lastFullSyncDate: site?.lastCatalogFullSyncDate,
42+
lastIncrementalSyncDate: site?.lastCatalogIncrementalSyncDate
43+
)
44+
}
45+
}
46+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import XCTest
2+
@testable import WooFoundation
3+
4+
/// WaitingTimeTracker Unit Tests
5+
///
6+
final class WaitingTimeTrackerTests: XCTestCase {
7+
func testTimeElapsedEvaluationIsCorrect() {
8+
var currentTimeCallCounter = 0.0
9+
10+
// Given
11+
let waitingTracker = WaitingTimeTracker(trackScenario: .orderDetails) {
12+
currentTimeCallCounter += 1
13+
return currentTimeCallCounter * 10
14+
}
15+
16+
// When
17+
let event = waitingTracker.end()
18+
19+
// Then
20+
XCTAssertEqual(event.properties["waiting_time"] as? TimeInterval, 10.0)
21+
}
22+
23+
func testOrderDetailsTrackScenarioTriggersExpectedAnalyticsStat() {
24+
// Given
25+
let waitingTracker = WaitingTimeTracker(trackScenario: .orderDetails, currentTimestampSeconds: { 0 })
26+
27+
// When
28+
let event = waitingTracker.end()
29+
30+
// Then
31+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.orderDetailWaitingTimeLoaded.rawValue)
32+
}
33+
34+
func testTopPerformersTrackScenarioTriggersExpectedAnalyticsStat() {
35+
// Given
36+
let waitingTracker = WaitingTimeTracker(trackScenario: .dashboardTopPerformers,
37+
currentTimestampSeconds: { 0 }
38+
)
39+
40+
// When
41+
let event = waitingTracker.end()
42+
43+
// Then
44+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.dashboardTopPerformersWaitingTimeLoaded.rawValue)
45+
}
46+
47+
func testMainStatsTrackScenarioTriggersExpectedAnalyticsStat() {
48+
// Given
49+
let waitingTracker = WaitingTimeTracker(trackScenario: .dashboardMainStats,
50+
currentTimestampSeconds: { 0 }
51+
)
52+
53+
// When
54+
let event = waitingTracker.end()
55+
56+
// Then
57+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.dashboardMainStatsWaitingTimeLoaded.rawValue)
58+
}
59+
60+
func test_analytics_hub_track_scenario_triggers_expected_analytics_stat() {
61+
// Given
62+
let waitingTracker = WaitingTimeTracker(trackScenario: .analyticsHub,
63+
currentTimestampSeconds: { 0 }
64+
)
65+
66+
// When
67+
let event = waitingTracker.end()
68+
69+
// Then
70+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.analyticsHubWaitingTimeLoaded.rawValue)
71+
}
72+
73+
func test_appStartup_track_scenario_triggers_expected_analytics_stat() {
74+
// Given
75+
let waitingTracker = WaitingTimeTracker(trackScenario: .appStartup,
76+
currentTimestampSeconds: { 0 }
77+
)
78+
79+
// When
80+
let event = waitingTracker.end()
81+
82+
// Then
83+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.applicationOpenedWaitingTimeLoaded.rawValue)
84+
}
85+
86+
func test_timeElapsed_evaluation_in_milliseconds_is_correct() {
87+
// Given
88+
var currentTimeCallCounter = 0.0
89+
let expectedReceivedWaitingTime = 10_000.0 // 10s * 1000 ms
90+
let waitingTracker = WaitingTimeTracker(trackScenario: .orderDetails) {
91+
currentTimeCallCounter += 1
92+
return currentTimeCallCounter * 10
93+
}
94+
95+
// When
96+
let event = waitingTracker.end(using: .milliseconds)
97+
98+
// Then
99+
XCTAssertEqual(event.properties["waiting_time"] as? TimeInterval, expectedReceivedWaitingTime)
100+
}
101+
102+
func test_track_scenario_triggers_expected_analytics_stat_in_milliseconds() {
103+
// Given
104+
let waitingTracker = WaitingTimeTracker(trackScenario: .pointOfSaleLoaded,
105+
currentTimestampSeconds: { 0 })
106+
107+
// When
108+
let event = waitingTracker.end(using: .milliseconds)
109+
110+
// Then
111+
XCTAssertEqual(event.statName.rawValue, WooAnalyticsStat.pointOfSaleLoaded.rawValue)
112+
}
113+
}

0 commit comments

Comments
 (0)