From a52d5d3c70097d5756e295dfd5e5ae9e582e8516 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 12:33:20 +0700 Subject: [PATCH 1/6] Add Booking model --- .../Networking/Model/Bookings/Booking.swift | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 Modules/Sources/Networking/Model/Bookings/Booking.swift diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift new file mode 100644 index 00000000000..1c6974f2e4b --- /dev/null +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -0,0 +1,200 @@ +// periphery:ignore:all +import Codegen +import Foundation + +/// Represents a Booking Entity. +/// +public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable { + public let siteID: Int64 + public let bookingID: Int64 + public let allDay: Bool + public let cost: String + public let customerID: Int64 + public let dateCreated: Date + public let dateModified: Date + public let endDate: Date + public let googleCalendarEventID: String? + public let orderID: Int64 + public let orderItemID: Int64 + public let parentID: Int64 + public let productID: Int64 + public let resourceID: Int64 + public let startDate: Date + public let statusKey: String + public let localTimezone: String + + /// Computed Properties + /// + public var bookingStatus: BookingStatus { + return BookingStatus(rawValue: statusKey) + } + + /// Booking struct initializer. + /// + public init(siteID: Int64, + bookingID: Int64, + allDay: Bool, + cost: String, + customerID: Int64, + dateCreated: Date, + dateModified: Date, + endDate: Date, + googleCalendarEventID: String?, + orderID: Int64, + orderItemID: Int64, + parentID: Int64, + productID: Int64, + resourceID: Int64, + startDate: Date, + statusKey: String, + localTimezone: String) { + self.siteID = siteID + self.bookingID = bookingID + self.allDay = allDay + self.cost = cost + self.customerID = customerID + self.dateCreated = dateCreated + self.dateModified = dateModified + self.endDate = endDate + self.googleCalendarEventID = googleCalendarEventID + self.orderID = orderID + self.orderItemID = orderItemID + self.parentID = parentID + self.productID = productID + self.resourceID = resourceID + self.startDate = startDate + self.statusKey = statusKey + self.localTimezone = localTimezone + } + + /// The public initializer for Booking. + /// + public init(from decoder: Decoder) throws { + guard let siteID = decoder.userInfo[.siteID] as? Int64 else { + throw BookingDecodingError.missingSiteID + } + + let container = try decoder.container(keyedBy: CodingKeys.self) + + let bookingID = try container.decode(Int64.self, forKey: .bookingID) + let allDay = try container.decode(Bool.self, forKey: .allDay) + + // Cost may come as string or number + let cost = container.failsafeDecodeIfPresent(targetType: String.self, + forKey: .cost, + alternativeTypes: [.decimal(transform: { NSDecimalNumber(decimal: $0).stringValue })]) ?? "" + + let customerID = try container.decode(Int64.self, forKey: .customerID) + let dateCreated = try container.decode(Date.self, forKey: .dateCreated) + let dateModified = try container.decode(Date.self, forKey: .dateModified) + let endDate = try container.decode(Date.self, forKey: .endDate) + let googleCalendarEventID = try container.decodeIfPresent(String.self, forKey: .googleCalendarEventID) + let orderID = try container.decode(Int64.self, forKey: .orderID) + let orderItemID = try container.decode(Int64.self, forKey: .orderItemID) + let parentID = try container.decode(Int64.self, forKey: .parentID) + let productID = try container.decode(Int64.self, forKey: .productID) + let resourceID = try container.decode(Int64.self, forKey: .resourceID) + let startDate = try container.decode(Date.self, forKey: .startDate) + let statusKey = try container.decode(String.self, forKey: .statusKey) + let localTimezone = try container.decode(String.self, forKey: .localTimezone) + + self.init(siteID: siteID, + bookingID: bookingID, + allDay: allDay, + cost: cost, + customerID: customerID, + dateCreated: dateCreated, + dateModified: dateModified, + endDate: endDate, + googleCalendarEventID: googleCalendarEventID, + orderID: orderID, + orderItemID: orderItemID, + parentID: parentID, + personCounts: personCounts, + productID: productID, + resourceID: resourceID, + startDate: startDate, + statusKey: statusKey, + localTimezone: localTimezone) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(bookingID, forKey: .bookingID) + try container.encode(allDay, forKey: .allDay) + try container.encode(cost, forKey: .cost) + try container.encode(customerID, forKey: .customerID) + try container.encode(dateCreated, forKey: .dateCreated) + try container.encode(dateModified, forKey: .dateModified) + try container.encode(endDate, forKey: .endDate) + try container.encode(googleCalendarEventID, forKey: .googleCalendarEventID) + try container.encode(orderID, forKey: .orderID) + try container.encode(orderItemID, forKey: .orderItemID) + try container.encode(parentID, forKey: .parentID) + try container.encode(personCounts, forKey: .personCounts) + try container.encode(productID, forKey: .productID) + try container.encode(resourceID, forKey: .resourceID) + try container.encode(startDate, forKey: .startDate) + try container.encode(statusKey, forKey: .statusKey) + try container.encode(localTimezone, forKey: .localTimezone) + } +} + +/// Defines all of the Booking CodingKeys +/// +private extension Booking { + + enum CodingKeys: String, CodingKey { + case bookingID = "id" + case allDay = "all_day" + case cost + case customerID = "customer_id" + case dateCreated = "date_created" + case dateModified = "date_modified" + case endDate = "end" + case googleCalendarEventID = "google_calendar_event_id" + case orderID = "order_id" + case orderItemID = "order_item_id" + case parentID = "parent_id" + case personCounts = "person_counts" + case productID = "product_id" + case resourceID = "resource_id" + case startDate = "start" + case statusKey = "status" + case localTimezone = "local_timezone" + } +} + +// MARK: - Decoding Errors +// +enum BookingDecodingError: Error { + case missingSiteID +} + +// MARK: - Supporting Types +// + +/// Represents a Booking Status. +/// +public enum BookingStatus: String, CaseIterable { + case complete = "complete" + case paid = "paid" + case unpaid = "unpaid" + case cancelled = "cancelled" + + public init(rawValue: String) { + switch rawValue { + case "complete": + self = .complete + case "paid": + self = .paid + case "unpaid": + self = .unpaid + case "cancelled": + self = .cancelled + default: + self = .unpaid + } + } +} From 17b6355c95c8ac63b3ff11e4f27ecba8649dcda5 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 16:25:01 +0700 Subject: [PATCH 2/6] Add copiable for booking --- .../Networking/Model/Bookings/Booking.swift | 30 +++------ .../Copiable/Models+Copiable.generated.swift | 64 ++++++++++++++++++- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index 1c6974f2e4b..b221c13da1b 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -26,7 +26,7 @@ public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable /// Computed Properties /// public var bookingStatus: BookingStatus { - return BookingStatus(rawValue: statusKey) + return BookingStatus(rawValue: statusKey) ?? .unknown } /// Booking struct initializer. @@ -110,7 +110,6 @@ public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable orderID: orderID, orderItemID: orderItemID, parentID: parentID, - personCounts: personCounts, productID: productID, resourceID: resourceID, startDate: startDate, @@ -132,7 +131,6 @@ public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable try container.encode(orderID, forKey: .orderID) try container.encode(orderItemID, forKey: .orderItemID) try container.encode(parentID, forKey: .parentID) - try container.encode(personCounts, forKey: .personCounts) try container.encode(productID, forKey: .productID) try container.encode(resourceID, forKey: .resourceID) try container.encode(startDate, forKey: .startDate) @@ -178,23 +176,11 @@ enum BookingDecodingError: Error { /// Represents a Booking Status. /// public enum BookingStatus: String, CaseIterable { - case complete = "complete" - case paid = "paid" - case unpaid = "unpaid" - case cancelled = "cancelled" - - public init(rawValue: String) { - switch rawValue { - case "complete": - self = .complete - case "paid": - self = .paid - case "unpaid": - self = .unpaid - case "cancelled": - self = .cancelled - default: - self = .unpaid - } - } + case complete + case paid + case unpaid + case cancelled + case pendingConfirmation = "pending-confirmation" + case inCart = "in-cart" + case unknown } diff --git a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift index cd2809a36bc..ee93ed02104 100644 --- a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -428,6 +428,66 @@ extension Networking.BlazeTargetTopic { } } +extension Networking.Booking { + public func copy( + siteID: CopiableProp = .copy, + bookingID: CopiableProp = .copy, + allDay: CopiableProp = .copy, + cost: CopiableProp = .copy, + customerID: CopiableProp = .copy, + dateCreated: CopiableProp = .copy, + dateModified: CopiableProp = .copy, + endDate: CopiableProp = .copy, + googleCalendarEventID: NullableCopiableProp = .copy, + orderID: CopiableProp = .copy, + orderItemID: CopiableProp = .copy, + parentID: CopiableProp = .copy, + productID: CopiableProp = .copy, + resourceID: CopiableProp = .copy, + startDate: CopiableProp = .copy, + statusKey: CopiableProp = .copy, + localTimezone: CopiableProp = .copy + ) -> Networking.Booking { + let siteID = siteID ?? self.siteID + let bookingID = bookingID ?? self.bookingID + let allDay = allDay ?? self.allDay + let cost = cost ?? self.cost + let customerID = customerID ?? self.customerID + let dateCreated = dateCreated ?? self.dateCreated + let dateModified = dateModified ?? self.dateModified + let endDate = endDate ?? self.endDate + let googleCalendarEventID = googleCalendarEventID ?? self.googleCalendarEventID + let orderID = orderID ?? self.orderID + let orderItemID = orderItemID ?? self.orderItemID + let parentID = parentID ?? self.parentID + let productID = productID ?? self.productID + let resourceID = resourceID ?? self.resourceID + let startDate = startDate ?? self.startDate + let statusKey = statusKey ?? self.statusKey + let localTimezone = localTimezone ?? self.localTimezone + + return Networking.Booking( + siteID: siteID, + bookingID: bookingID, + allDay: allDay, + cost: cost, + customerID: customerID, + dateCreated: dateCreated, + dateModified: dateModified, + endDate: endDate, + googleCalendarEventID: googleCalendarEventID, + orderID: orderID, + orderItemID: orderItemID, + parentID: parentID, + productID: productID, + resourceID: resourceID, + startDate: startDate, + statusKey: statusKey, + localTimezone: localTimezone + ) + } +} + extension Networking.Coupon { public func copy( siteID: CopiableProp = .copy, @@ -2749,8 +2809,8 @@ extension Networking.Site { hasSSOEnabled: CopiableProp = .copy, applicationPasswordAvailable: CopiableProp = .copy, isGarden: CopiableProp = .copy, - gardenName: CopiableProp = .copy, - gardenPartner: CopiableProp = .copy + gardenName: NullableCopiableProp = .copy, + gardenPartner: NullableCopiableProp = .copy ) -> Networking.Site { let siteID = siteID ?? self.siteID let name = name ?? self.name From b239a5b8789ccd9d5f51aae0bb1c203eacea1f40 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 16:27:25 +0700 Subject: [PATCH 3/6] Add BookingsRemote --- .../Networking/Remote/BookingsRemote.swift | 59 +++++++++++++++++++ .../Settings/WooAPIVersion.swift | 4 ++ 2 files changed, 63 insertions(+) create mode 100644 Modules/Sources/Networking/Remote/BookingsRemote.swift diff --git a/Modules/Sources/Networking/Remote/BookingsRemote.swift b/Modules/Sources/Networking/Remote/BookingsRemote.swift new file mode 100644 index 00000000000..d8bc1c5fdf7 --- /dev/null +++ b/Modules/Sources/Networking/Remote/BookingsRemote.swift @@ -0,0 +1,59 @@ +// periphery:ignore:all +import Foundation + +/// Protocol for `BookingsRemote` mainly used for mocking. +/// +/// The required methods are intentionally incomplete. Feel free to add the other ones. +/// +public protocol BookingsRemoteProtocol { + func loadAllBookings(for siteID: Int64, + pageNumber: Int, + pageSize: Int) async throws -> [Booking] +} + +/// Booking: Remote Endpoints +/// +public final class BookingsRemote: Remote, BookingsRemoteProtocol { + + // MARK: - Bookings + + /// Retrieves all of the `Bookings` available. + /// + /// - Parameters: + /// - siteID: Site for which we'll fetch remote bookings. + /// - pageNumber: Number of page that should be retrieved. + /// - pageSize: Number of bookings to be retrieved per page. + /// + public func loadAllBookings(for siteID: Int64, + pageNumber: Int = Default.pageNumber, + pageSize: Int = Default.pageSize) async throws -> [Booking] { + let parameters = [ + ParameterKey.page: String(pageNumber), + ParameterKey.perPage: String(pageSize) + ] + + let path = Path.bookings + let request = JetpackRequest(wooApiVersion: .wcBookings, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true) + let mapper = ListMapper(siteID: siteID) + + return try await enqueue(request, mapper: mapper) + } +} + +// MARK: - Constants +// +public extension BookingsRemote { + enum Default { + public static let pageSize: Int = 25 + public static let pageNumber: Int = Remote.Default.firstPageNumber + } + + private enum Path { + static let bookings = "bookings" + } + + private enum ParameterKey { + static let page: String = "page" + static let perPage: String = "per_page" + } +} diff --git a/Modules/Sources/NetworkingCore/Settings/WooAPIVersion.swift b/Modules/Sources/NetworkingCore/Settings/WooAPIVersion.swift index f29d66dd495..1a769396266 100644 --- a/Modules/Sources/NetworkingCore/Settings/WooAPIVersion.swift +++ b/Modules/Sources/NetworkingCore/Settings/WooAPIVersion.swift @@ -50,6 +50,10 @@ public enum WooAPIVersion: String { /// case wooShipping = "wcshipping/v1" + /// WooCommerce Bookings Plugin V1. + /// + case wcBookings = "wc-bookings/v2" + /// Returns the path for the current API Version /// var path: String { From a26c237c1f64b1f5dd7bed12a771711ddae4088e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 17:38:06 +0700 Subject: [PATCH 4/6] Add tests for BookingsRemote --- .../Networking/Model/Bookings/Booking.swift | 8 ++-- .../Remote/BookingsRemoteTests.swift | 38 ++++++++++++++++ .../Responses/booking-list.json | 44 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift create mode 100644 Modules/Tests/NetworkingTests/Responses/booking-list.json diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index b221c13da1b..21d95ddd54e 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -85,16 +85,16 @@ public struct Booking: Codable, GeneratedCopiable, Equatable, GeneratedFakeable alternativeTypes: [.decimal(transform: { NSDecimalNumber(decimal: $0).stringValue })]) ?? "" let customerID = try container.decode(Int64.self, forKey: .customerID) - let dateCreated = try container.decode(Date.self, forKey: .dateCreated) - let dateModified = try container.decode(Date.self, forKey: .dateModified) - let endDate = try container.decode(Date.self, forKey: .endDate) + let dateCreated = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateCreated)) + let dateModified = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .dateModified)) + let endDate = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .endDate)) let googleCalendarEventID = try container.decodeIfPresent(String.self, forKey: .googleCalendarEventID) let orderID = try container.decode(Int64.self, forKey: .orderID) let orderItemID = try container.decode(Int64.self, forKey: .orderItemID) let parentID = try container.decode(Int64.self, forKey: .parentID) let productID = try container.decode(Int64.self, forKey: .productID) let resourceID = try container.decode(Int64.self, forKey: .resourceID) - let startDate = try container.decode(Date.self, forKey: .startDate) + let startDate = Date(timeIntervalSince1970: try container.decode(Double.self, forKey: .startDate)) let statusKey = try container.decode(String.self, forKey: .statusKey) let localTimezone = try container.decode(String.self, forKey: .localTimezone) diff --git a/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift new file mode 100644 index 00000000000..dc9c11801e8 --- /dev/null +++ b/Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift @@ -0,0 +1,38 @@ +import Testing +@testable import Networking + +struct BookingsRemoteTests { + + private let network = MockNetwork() + private let sampleSiteID: Int64 = 1234 + + @Test func test_loadAllBookings_properly_returns_parsed_bookings() async throws { + // Given + let remote = BookingsRemote(network: network) + network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list") + + // When + let bookings = try await remote.loadAllBookings(for: sampleSiteID) + + // Then + #expect(bookings.count == 2) + let firstBooking = try #require(bookings.first) + #expect(firstBooking.bookingID == 80) + #expect(firstBooking.allDay == false) + #expect(firstBooking.bookingStatus == .unpaid) + #expect(firstBooking.orderID == 79) + #expect(firstBooking.productID == 23) + #expect(firstBooking.customerID == 0) + #expect(firstBooking.siteID == sampleSiteID) + } + + @Test func test_loadAllBookings_properly_relays_netwoking_errors() async { + // Given + let remote = BookingsRemote(network: network) + + // Then + await #expect(throws: NetworkError.notFound()) { + _ = try await remote.loadAllBookings(for: sampleSiteID) + } + } +} diff --git a/Modules/Tests/NetworkingTests/Responses/booking-list.json b/Modules/Tests/NetworkingTests/Responses/booking-list.json new file mode 100644 index 00000000000..5532948f4ad --- /dev/null +++ b/Modules/Tests/NetworkingTests/Responses/booking-list.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "id": 80, + "start": 1759417200, + "end": 1759420800, + "all_day": false, + "status": "unpaid", + "cost": "30.00", + "currency": "USD", + "customer_id": 0, + "product_id": 23, + "resource_id": 22, + "date_created": 1758531652, + "date_modified": 1758531652, + "google_calendar_event_id": "0", + "order_id": 79, + "order_item_id": 3, + "parent_id": 0, + "person_counts": [], + "local_timezone": "", + }, + { + "id": 77, + "start": 1759651200, + "end": 1759654800, + "all_day": false, + "status": "confirmed", + "cost": "35.00", + "currency": "USD", + "customer_id": 2, + "product_id": 23, + "resource_id": 19, + "date_created": 1758530260, + "date_modified": 1758531705, + "google_calendar_event_id": "0", + "order_id": 78, + "order_item_id": 2, + "parent_id": 0, + "person_counts": [], + "local_timezone": "", + } + ] +} From 67053582c644a3fb28b57b29b329ec444021223c Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 18:14:45 +0700 Subject: [PATCH 5/6] Add new booking status confirmed --- Modules/Sources/Networking/Model/Bookings/Booking.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index 21d95ddd54e..38ad4a68daf 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -181,6 +181,7 @@ public enum BookingStatus: String, CaseIterable { case unpaid case cancelled case pendingConfirmation = "pending-confirmation" + case confirmed case inCart = "in-cart" case unknown } From 0e16d10a547451f358409843ee78d02dd56ebccd Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 22 Sep 2025 18:20:10 +0700 Subject: [PATCH 6/6] Generate fake for Booking --- .../Sources/Fakes/Networking.generated.swift | 31 ++- .../WooAnalyticsEvent+ProductForm.swift | 18 +- .../WooAnalyticsEvent+ProductFormAI.swift | 18 +- .../WooAnalyticsEvent+ProductListFilter.swift | 34 ++-- .../WooAnalyticsEvent+StoreCreation.swift | 76 ++++--- .../WooAnalyticsEvent+StoreOnboarding.swift | 12 +- .../WooAnalyticsEvent+PointOfSale.swift | 188 +++++++++--------- 7 files changed, 195 insertions(+), 182 deletions(-) diff --git a/Modules/Sources/Fakes/Networking.generated.swift b/Modules/Sources/Fakes/Networking.generated.swift index c640e2ae555..5b4e894b1cb 100644 --- a/Modules/Sources/Fakes/Networking.generated.swift +++ b/Modules/Sources/Fakes/Networking.generated.swift @@ -317,6 +317,31 @@ extension Networking.BlazeTargetTopic { ) } } +extension Networking.Booking { + /// Returns a "ready to use" type filled with fake values. + /// + public static func fake() -> Networking.Booking { + .init( + siteID: .fake(), + bookingID: .fake(), + allDay: .fake(), + cost: .fake(), + customerID: .fake(), + dateCreated: .fake(), + dateModified: .fake(), + endDate: .fake(), + googleCalendarEventID: .fake(), + orderID: .fake(), + orderItemID: .fake(), + parentID: .fake(), + productID: .fake(), + resourceID: .fake(), + startDate: .fake(), + statusKey: .fake(), + localTimezone: .fake() + ) + } +} extension Networking.CompositeComponentOptionType { /// Returns a "ready to use" type filled with fake values. /// @@ -1775,9 +1800,9 @@ extension Networking.Site { wasEcommerceTrial: .fake(), hasSSOEnabled: .fake(), applicationPasswordAvailable: .fake(), - isGarden: false, - gardenName: nil, - gardenPartner: nil + isGarden: .fake(), + gardenName: .fake(), + gardenPartner: .fake() ) } } diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductForm.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductForm.swift index 0575a037137..9019b25375a 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductForm.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductForm.swift @@ -5,6 +5,14 @@ extension WooAnalyticsEvent { static let source = "source" } + /// Source of the share product action. The raw value is the event property value. + enum ShareProductSource: String { + /// From product form in the navigation bar. + case productForm = "product_form" + /// From product form > more menu in the navigation bar. + case moreMenu = "more_menu" + } + /// Tracked when the user taps on the button to share a product. static func productDetailShareButtonTapped(source: ShareProductSource) -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .productDetailShareButtonTapped, @@ -24,13 +32,3 @@ extension WooAnalyticsEvent { } } } - -extension WooAnalyticsEvent.ProductForm { - /// Source of the share product action. The raw value is the event property value. - enum ShareProductSource: String { - /// From product form in the navigation bar. - case productForm = "product_form" - /// From product form > more menu in the navigation bar. - case moreMenu = "more_menu" - } -} diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductFormAI.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductFormAI.swift index 514b27aafad..650ad70579c 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductFormAI.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductFormAI.swift @@ -7,6 +7,14 @@ extension WooAnalyticsEvent { static let language = "language" } + /// Trigger of the product description AI flow. The raw value is the event property value. + enum ProductDescriptionAISource: String { + /// From product description Aztec editor. + case aztecEditor = "aztec_editor" + /// From the product form below the description row. + case productForm = "product_form" + } + /// Tracked when the user taps on the button to start the product description AI flow. static func productDescriptionAIButtonTapped(source: ProductDescriptionAISource) -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .productDescriptionAIButtonTapped, @@ -61,16 +69,6 @@ extension WooAnalyticsEvent { } } -extension WooAnalyticsEvent.ProductFormAI { - /// Trigger of the product description AI flow. The raw value is the event property value. - enum ProductDescriptionAISource: String { - /// From product description Aztec editor. - case aztecEditor = "aztec_editor" - /// From the product form below the description row. - case productForm = "product_form" - } -} - private extension WooAnalyticsEvent.ProductFormAI { enum Constants { static let productDescriptionSource = "product_description" diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductListFilter.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductListFilter.swift index 1b1bc13429e..746eafed4f4 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductListFilter.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+ProductListFilter.swift @@ -7,6 +7,22 @@ extension WooAnalyticsEvent { static let type = "type" } + /// Trigger of the product list filter. The raw value is the event property value. + enum Source: String { + /// From the products tab. + case productsTab = "products_tab" + /// From order form > add products. + case orderForm = "order_form" + /// From coupon form > products. + case couponForm = "coupon_form" + /// From coupon form > usage restrictions > exclude products. + case couponRestrictions = "coupon_restrictions" + /// From Blaze campaign creation flow + case blaze = "blaze" + /// From orders > filter. + case orderFilter = "order_filter" + } + /// Tracked when the user taps on the button to filter products. /// - Parameter source: Source of the product list filter. static func productListViewFilterOptionsTapped(source: Source) -> WooAnalyticsEvent { @@ -28,21 +44,3 @@ extension WooAnalyticsEvent { } } } - -extension WooAnalyticsEvent.ProductListFilter { - /// Trigger of the product list filter. The raw value is the event property value. - enum Source: String { - /// From the products tab. - case productsTab = "products_tab" - /// From order form > add products. - case orderForm = "order_form" - /// From coupon form > products. - case couponForm = "coupon_form" - /// From coupon form > usage restrictions > exclude products. - case couponRestrictions = "coupon_restrictions" - /// From Blaze campaign creation flow - case blaze = "blaze" - /// From orders > filter. - case orderFilter = "order_filter" - } -} diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreCreation.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreCreation.swift index 41b5069ee59..fb5a5cf8248 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreCreation.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreCreation.swift @@ -21,6 +21,43 @@ extension WooAnalyticsEvent { static let initialDomain = "initial_domain" } + enum StorePickerSource: String { + /// From switching stores. + case switchStores = "switching_stores" + /// From the login flow. + case login + /// The store creation flow is originally initiated from login prologue and dismissed, + /// which lands on the store picker. + case loginPrologue = "prologue" + /// Other sources like from any error screens during the login flow. + case other + } + + enum Source: String { + case loginPrologue = "prologue" + case storePicker = "store_picker" + case loginEmailError = "login_email_error" + } + + /// The implementation of store creation flow - native (M2) or web (M1). + enum Flow: String { + case native = "native" + case web = "web" + } + + /// Steps of the native store creation flow. + enum Step: String { + case profilerCategoryQuestion = "store_profiler_industries" + case profilerSellingStatusQuestion = "store_profiler_commerce_journey" + case profilerSellingPlatformsQuestion = "store_profiler_ecommerce_platforms" + case profilerCountryQuestion = "store_profiler_country" + case domainPicker = "domain_picker" + case storeSummary = "store_summary" + case planPurchase = "plan_purchase" + case webCheckout = "web_checkout" + case storeInstallation = "store_installation" + } + static func siteCreationFlowStarted(source: Source) -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .siteCreationFlowStarted, properties: [Key.source: source.rawValue]) @@ -163,45 +200,6 @@ extension WooAnalyticsEvent { } } -extension WooAnalyticsEvent.StoreCreation { - enum StorePickerSource: String { - /// From switching stores. - case switchStores = "switching_stores" - /// From the login flow. - case login - /// The store creation flow is originally initiated from login prologue and dismissed, - /// which lands on the store picker. - case loginPrologue = "prologue" - /// Other sources like from any error screens during the login flow. - case other - } - - enum Source: String { - case loginPrologue = "prologue" - case storePicker = "store_picker" - case loginEmailError = "login_email_error" - } - - /// The implementation of store creation flow - native (M2) or web (M1). - enum Flow: String { - case native = "native" - case web = "web" - } - - /// Steps of the native store creation flow. - enum Step: String { - case profilerCategoryQuestion = "store_profiler_industries" - case profilerSellingStatusQuestion = "store_profiler_commerce_journey" - case profilerSellingPlatformsQuestion = "store_profiler_ecommerce_platforms" - case profilerCountryQuestion = "store_profiler_country" - case domainPicker = "domain_picker" - case storeSummary = "store_summary" - case planPurchase = "plan_purchase" - case webCheckout = "web_checkout" - case storeInstallation = "store_installation" - } -} - private extension CreateAccountError { var analyticsValue: String { switch self { diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreOnboarding.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreOnboarding.swift index 72911cdab50..735b7469e78 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreOnboarding.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+StoreOnboarding.swift @@ -10,6 +10,11 @@ extension WooAnalyticsEvent { static let hide = "hide" } + enum Source: String { + case onboardingList = "onboarding_list" + case settings + } + static func storeOnboardingShown() -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .storeOnboardingShown, properties: [:]) } @@ -37,13 +42,6 @@ extension WooAnalyticsEvent { } } -extension WooAnalyticsEvent.StoreOnboarding { - enum Source: String { - case onboardingList = "onboarding_list" - case settings - } -} - private extension StoreOnboardingTask.TaskType { var analyticsValue: String { switch self { diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index ee93978e17b..50b6e70e32b 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -39,6 +39,99 @@ extension WooAnalyticsEvent { static let scanValue = "scan_value" } + /// Source of the event where the event is triggered + /// Views: Product, Variation, and Coupon Lists. Cart view and Checkout error. + /// + enum SourceView: String { + case product + case variation + case coupon + case cart + case error + + init(itemType: POSItemType) { + switch itemType { + case .product: + self = .product + case .variation: + self = .variation + case .coupon: + self = .coupon + } + } + + init(itemListType: ItemListType) { + switch itemListType { + case .products: + self = .product + case .coupons: + self = .coupon + } + } + } + + /// The state of the view where the event is triggered. + /// E.g. product list, procuct search, or product pre-search view where popular searches are shown. + /// + enum SourceViewType: String { + case list + case search + case preSearch = "pre_search" + case scanner + + init(isSearching: Bool, searchTerm: String = "") { + switch (isSearching, searchTerm.isEmpty) { + case (false, _): + self = .list + case (true, true): + self = .preSearch + case (true, false): + self = .search + } + } + } + + /// Types of high-level items supported in the POS + /// + enum ItemType: String { + case product + case coupon + case loading + case error + + init(cartItem: Cart.PurchasableItem) { + switch cartItem.state { + case .loaded: + self = .product + case .loading: + self = .loading + case .error: + self = .error + } + } + } + + /// Types of products supported in the POS + /// + enum CartItemProductType: String { + case simple + case variation + + init?(cartItem: Cart.PurchasableItem) { + guard case let .loaded(item) = cartItem.state else { + return nil + } + + if item is POSSimpleProduct { + self = .simple + } else if item is POSVariation { + self = .variation + } else { + return nil + } + } + } + static func paymentsOnboardingShown() -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .pointOfSalePaymentsOnboardingShown, properties: [:]) } @@ -295,98 +388,3 @@ private extension WooAnalyticsEvent.PointOfSale { gatewayID ?? "unknown" } } - -extension WooAnalyticsEvent.PointOfSale { - /// Source of the event where the event is triggered - /// Views: Product, Variation, and Coupon Lists. Cart view and Checkout error. - /// - enum SourceView: String { - case product - case variation - case coupon - case cart - case error - - init(itemType: POSItemType) { - switch itemType { - case .product: - self = .product - case .variation: - self = .variation - case .coupon: - self = .coupon - } - } - - init(itemListType: ItemListType) { - switch itemListType { - case .products: - self = .product - case .coupons: - self = .coupon - } - } - } - - /// The state of the view where the event is triggered. - /// E.g. product list, procuct search, or product pre-search view where popular searches are shown. - /// - enum SourceViewType: String { - case list - case search - case preSearch = "pre_search" - case scanner - - init(isSearching: Bool, searchTerm: String = "") { - switch (isSearching, searchTerm.isEmpty) { - case (false, _): - self = .list - case (true, true): - self = .preSearch - case (true, false): - self = .search - } - } - } - - /// Types of high-level items supported in the POS - /// - enum ItemType: String { - case product - case coupon - case loading - case error - - init(cartItem: Cart.PurchasableItem) { - switch cartItem.state { - case .loaded: - self = .product - case .loading: - self = .loading - case .error: - self = .error - } - } - } - - /// Types of products supported in the POS - /// - enum CartItemProductType: String { - case simple - case variation - - init?(cartItem: Cart.PurchasableItem) { - guard case let .loaded(item) = cartItem.state else { - return nil - } - - if item is POSSimpleProduct { - self = .simple - } else if item is POSVariation { - self = .variation - } else { - return nil - } - } - } -}