Skip to content

Commit 023cf17

Browse files
committed
Merge branch 'trunk' into issue/8075-ui-updates
2 parents a5f440d + 848f8a7 commit 023cf17

File tree

23 files changed

+528
-111
lines changed

23 files changed

+528
-111
lines changed

Experiments/Experiments/ABTest.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ public enum ABTest: String, CaseIterable {
1616
///
1717
case abTestLoginWithWPComOnly = "woocommerceios_login_wpcom_only"
1818

19+
/// A/B test to measure the sign-in success rate when native Jetpack installation experience is enabled
20+
/// Experiment ref: pbxNRc-29W-p2
21+
///
22+
case nativeJetpackSetupFlow = "woocommerceios_login_jetpack_setup_flow"
23+
1924
/// A/B test for the Products Onboarding banner on the My Store dashboard.
2025
/// Experiment ref: pbxNRc-26F-p2
2126
case productsOnboardingBanner = "woocommerceios_products_onboarding_first_product_banner"
@@ -34,7 +39,7 @@ public enum ABTest: String, CaseIterable {
3439
/// When adding a new experiment, add it to the appropriate case depending on its context (logged-in or logged-out experience).
3540
public var context: ExperimentContext {
3641
switch self {
37-
case .productsOnboardingBanner, .productsOnboardingTemplateProducts:
42+
case .productsOnboardingBanner, .productsOnboardingTemplateProducts, .nativeJetpackSetupFlow:
3843
return .loggedIn
3944
case .aaTestLoggedOut, .abTestLoginWithWPComOnly:
4045
return .loggedOut
@@ -47,6 +52,7 @@ public enum ABTest: String, CaseIterable {
4752
public extension ABTest {
4853
/// Start the AB Testing platform if any experiment exists for the provided context
4954
///
55+
@MainActor
5056
static func start(for context: ExperimentContext) async {
5157
let experiments = ABTest.allCases.filter { $0.context == context }
5258

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
5151
.performanceMonitoringUserInteraction:
5252
// Disabled by default to avoid costs spikes, unless in internal testing builds.
5353
return buildConfig == .alpha
54-
case .nativeJetpackSetupFlow:
55-
return buildConfig == .localDeveloper || buildConfig == .alpha
5654
case .analyticsHub:
5755
return buildConfig == .localDeveloper || buildConfig == .alpha
5856
case .tapToPayOnIPhone:

Experiments/Experiments/FeatureFlag.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,6 @@ public enum FeatureFlag: Int {
130130
/// - Note: The app will ignore this if `performanceMonitoring` is `false`.
131131
case performanceMonitoringViewController
132132

133-
/// Temporary feature flag for the native Jetpack setup flow.
134-
/// TODO-8075: replace this with A/B test.
135-
///
136-
case nativeJetpackSetupFlow
137-
138133
/// Temporary feature flag for the native Jetpack setup flow.
139134
///
140135
case analyticsHub

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
02DD6492248A3EC00082523E /* product-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 02DD6491248A3EC00082523E /* product-external.json */; };
9393
02E7FFCB256218F600C53030 /* ShippingLabelRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */; };
9494
02E7FFCF25621C7900C53030 /* shipping-label-print.json in Resources */ = {isa = PBXBuildFile; fileRef = 02E7FFCE25621C7900C53030 /* shipping-label-print.json */; };
95+
02EF1664292DADDE00D90AD6 /* PaymentRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */; };
96+
02EF166E292F0C5800D90AD6 /* PaymentRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */; };
97+
02EF1670292F0CF400D90AD6 /* create-cart-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 02EF166F292F0CF400D90AD6 /* create-cart-success.json */; };
98+
02EF1672292F0D1900D90AD6 /* load-plan-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 02EF1671292F0D1900D90AD6 /* load-plan-success.json */; };
9599
02F096C22406691100C0C1D5 /* media-library.json in Resources */ = {isa = PBXBuildFile; fileRef = 02F096C12406691100C0C1D5 /* media-library.json */; };
96100
0313651928AE559D00EEE571 /* PaymentGatewayMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */; };
97101
0313651B28AE60E000EEE571 /* payment-gateway-cod.json in Resources */ = {isa = PBXBuildFile; fileRef = 0313651A28AE60E000EEE571 /* payment-gateway-cod.json */; };
@@ -841,6 +845,10 @@
841845
02DD6491248A3EC00082523E /* product-external.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-external.json"; sourceTree = "<group>"; };
842846
02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelRemoteTests.swift; sourceTree = "<group>"; };
843847
02E7FFCE25621C7900C53030 /* shipping-label-print.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "shipping-label-print.json"; sourceTree = "<group>"; };
848+
02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRemote.swift; sourceTree = "<group>"; };
849+
02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRemoteTests.swift; sourceTree = "<group>"; };
850+
02EF166F292F0CF400D90AD6 /* create-cart-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "create-cart-success.json"; sourceTree = "<group>"; };
851+
02EF1671292F0D1900D90AD6 /* load-plan-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "load-plan-success.json"; sourceTree = "<group>"; };
844852
02F096C12406691100C0C1D5 /* media-library.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-library.json"; sourceTree = "<group>"; };
845853
0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayMapper.swift; sourceTree = "<group>"; };
846854
0313651A28AE60E000EEE571 /* payment-gateway-cod.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-cod.json"; sourceTree = "<group>"; };
@@ -1738,6 +1746,7 @@
17381746
68F48B0E28E3BB850045C15B /* WCAnalyticsCustomerRemoteTests.swift */,
17391747
0239306A291A96F800B2632F /* DomainRemoteTests.swift */,
17401748
02616F8B292132800095BC00 /* SiteRemoteTests.swift */,
1749+
02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */,
17411750
);
17421751
path = Remote;
17431752
sourceTree = "<group>";
@@ -1879,6 +1888,7 @@
18791888
68F48B0A28E3B1CD0045C15B /* WCAnalyticsCustomerRemote.swift */,
18801889
023930622918FF5400B2632F /* DomainRemote.swift */,
18811890
021940E1291E3CFD0090354E /* SiteRemote.swift */,
1891+
02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */,
18821892
);
18831893
path = Remote;
18841894
sourceTree = "<group>";
@@ -2006,6 +2016,7 @@
20062016
028CB713290223CB00331C09 /* create-account-error-password.json */,
20072017
028CB71A290224D700331C09 /* create-account-error-username.json */,
20082018
028CB712290223CB00331C09 /* create-account-success.json */,
2019+
02EF166F292F0CF400D90AD6 /* create-cart-success.json */,
20092020
0239306C291A973F00B2632F /* domain-suggestions.json */,
20102021
DE50295F28C609A300551736 /* jetpack-connected-user.json */,
20112022
DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */,
@@ -2072,6 +2083,7 @@
20722083
E16C59B828F927CA007D55BB /* iap-order-create.json */,
20732084
26B2F74624C55A6E0065CCC8 /* leaderboards-year.json */,
20742085
268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */,
2086+
02EF1671292F0D1900D90AD6 /* load-plan-success.json */,
20752087
B505F6D420BEE4E600BB1B69 /* me.json */,
20762088
93D8BBFE226BC1DA00AD2EB3 /* me-settings.json */,
20772089
02F096C12406691100C0C1D5 /* media-library.json */,
@@ -2709,6 +2721,7 @@
27092721
743E84FC22174CE100FAC9D7 /* restnoroute_error.json in Resources */,
27102722
CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */,
27112723
D8FBFF2722D529F2006E3336 /* order-stats-v4-month.json in Resources */,
2724+
02EF1672292F0D1900D90AD6 /* load-plan-success.json in Resources */,
27122725
2685C0DE263B5A4200D9EE97 /* add-on-groups.json in Resources */,
27132726
DEC51A9B274E3206009F3DF4 /* plugin-inactive.json in Resources */,
27142727
CCF48B382628AEAE0034EA83 /* shipping-label-account-settings-no-payment-methods.json in Resources */,
@@ -2800,6 +2813,7 @@
28002813
B554FA8D2180B59700C54DFF /* notifications-load-hashes.json in Resources */,
28012814
022902D622E2436400059692 /* no_stats_permission_error.json in Resources */,
28022815
FE28F6E826842D57004465C7 /* user-complete.json in Resources */,
2816+
02EF1670292F0CF400D90AD6 /* create-cart-success.json in Resources */,
28032817
261CF2CB255C50010090D8D3 /* payment-gateway-list-half.json in Resources */,
28042818
02C254D72563999300A04423 /* order-shipping-labels.json in Resources */,
28052819
45B204BC24890B1200FE6526 /* category.json in Resources */,
@@ -3090,6 +3104,7 @@
30903104
B5A2417D217F9ECC00595DEF /* MetaContainer.swift in Sources */,
30913105
025CA2C4238EBC4300B05C81 /* ProductShippingClassRemote.swift in Sources */,
30923106
D88D5A4B230BCF0A007B6E01 /* ProductReviewListMapper.swift in Sources */,
3107+
02EF1664292DADDE00D90AD6 /* PaymentRemote.swift in Sources */,
30933108
029C9E5C291507A40013E5EE /* UnauthenticatedRequest.swift in Sources */,
30943109
93D8BBFB226BBC5100AD2EB3 /* AccountSettings.swift in Sources */,
30953110
B53EF53821813806003E146F /* DotcomError.swift in Sources */,
@@ -3401,6 +3416,7 @@
34013416
CC851D1425E52AB500249E9C /* Decimal+ExtensionsTests.swift in Sources */,
34023417
B554FA8B2180B1D500C54DFF /* NotificationsRemoteTests.swift in Sources */,
34033418
B518662A20A09C6F00037A38 /* OrdersRemoteTests.swift in Sources */,
3419+
02EF166E292F0C5800D90AD6 /* PaymentRemoteTests.swift in Sources */,
34043420
B5969E1520A47F99005E9DF1 /* RemoteTests.swift in Sources */,
34053421
74C8F06E20EEC1E800B6EDC9 /* OrderNotesMapperTests.swift in Sources */,
34063422
45ED4F10239E8A54004F1BE3 /* TaxClassListMapperTest.swift in Sources */,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Foundation
2+
3+
/// Protocol for `PaymentRemote` mainly used for mocking.
4+
public protocol PaymentRemoteProtocol {
5+
/// Loads the WPCOM plan remotely that matches the product ID.
6+
/// - Parameter productID: The ID of the WPCOM plan product.
7+
/// - Returns: The WPCOM plan that matches the given product ID.
8+
func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan
9+
10+
/// Creates a cart with the given product ID for the site ID.
11+
/// - Parameters:
12+
/// - siteID: The ID of the site that the product is being added to.
13+
/// - productID: The ID of the product to be added to the site.
14+
/// - Returns: The remote response from creating a cart.
15+
func createCart(siteID: Int64, productID: Int64) async throws
16+
}
17+
18+
/// WPCOM Payment Endpoints
19+
///
20+
public class PaymentRemote: Remote, PaymentRemoteProtocol {
21+
public func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan {
22+
let path = Path.products
23+
let request = DotcomRequest(wordpressApiVersion: .mark1_5, method: .get, path: path)
24+
let plans: [WPComPlan] = try await enqueue(request)
25+
guard let plan = plans.first(where: { $0.productID == productID }) else {
26+
throw LoadPlanError.noMatchingPlan
27+
}
28+
return plan
29+
}
30+
31+
public func createCart(siteID: Int64, productID: Int64) async throws {
32+
let path = "\(Path.cartCreation)/\(siteID)"
33+
34+
let parameters: [String: Any] = [
35+
"products": [
36+
[
37+
"product_id": productID,
38+
"volume": 1
39+
]
40+
],
41+
// Necessary to create a persistent cart for later checkout, the default value is `true`.
42+
"temporary": false
43+
]
44+
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .post, path: path, parameters: parameters)
45+
let response: CreateCartResponse = try await enqueue(request)
46+
guard response.products.contains(where: { $0.productID == productID }) else {
47+
throw CreateCartError.productNotInCart
48+
}
49+
}
50+
}
51+
52+
/// Contains necessary data for rendering a WPCOM plan in the app.
53+
public struct WPComPlan: Decodable, Equatable {
54+
public let productID: Int64
55+
public let name: String
56+
public let formattedPrice: String
57+
58+
private enum CodingKeys: String, CodingKey {
59+
case productID = "product_id"
60+
case name = "product_name"
61+
case formattedPrice = "formatted_price"
62+
}
63+
}
64+
65+
/// Possible error cases from loading a WPCOM plan.
66+
public enum LoadPlanError: Error {
67+
case noMatchingPlan
68+
}
69+
70+
/// Possible error cases from creating cart for a site with a WPCOM plan.
71+
public enum CreateCartError: Error {
72+
case productNotInCart
73+
}
74+
75+
/// Contains necessary data for handling the remote response from creating a cart.
76+
private struct CreateCartResponse: Decodable {
77+
let products: [Product]
78+
79+
private enum CodingKeys: String, CodingKey {
80+
case products
81+
}
82+
}
83+
84+
private extension CreateCartResponse {
85+
/// Describes a product in a cart.
86+
struct Product: Decodable {
87+
let productID: Int64
88+
89+
private enum CodingKeys: String, CodingKey {
90+
case productID = "product_id"
91+
}
92+
}
93+
}
94+
95+
// MARK: - Constants
96+
//
97+
private extension PaymentRemote {
98+
enum Path {
99+
static let products = "plans"
100+
static let cartCreation = "me/shopping-cart"
101+
}
102+
}

Networking/Networking/Requests/DotcomRequest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ struct DotcomRequest: Request {
5353

5454
func responseDataValidator() -> ResponseDataValidator {
5555
switch wordpressApiVersion {
56-
case .mark1_1, .mark1_2:
56+
case .mark1_1, .mark1_2, .mark1_5:
5757
return DotcomValidator()
5858
case .wpcomMark2, .wpMark2:
5959
return WordPressApiValidator()

Networking/Networking/Settings/WordPressAPIVersion.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ enum WordPressAPIVersion: String {
1313
///
1414
case mark1_2 = "rest/v1.2/"
1515

16+
/// WordPress.com Endpoint Mark 1.5
17+
///
18+
case mark1_5 = "rest/v1.5/"
19+
1620
/// WPcom REST API Endpoint Mark 2
1721
///
1822
case wpcomMark2 = "wpcom/v2/"
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import XCTest
2+
import TestKit
3+
@testable import Networking
4+
5+
final class PaymentRemoteTests: XCTestCase {
6+
/// Mock network wrapper.
7+
private var network: MockNetwork!
8+
9+
override func setUp() {
10+
super.setUp()
11+
network = MockNetwork()
12+
}
13+
14+
override func tearDown() {
15+
network = nil
16+
super.tearDown()
17+
}
18+
19+
// MARK: - `loadPlan`
20+
21+
func test_loadPlan_returns_plan_on_success() async throws {
22+
// Given
23+
let remote = PaymentRemote(network: network)
24+
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-plan-success")
25+
26+
// When
27+
let plan = try await remote.loadPlan(thatMatchesID: Constants.planProductID)
28+
29+
// Then
30+
XCTAssertEqual(plan, .init(productID: Constants.planProductID,
31+
name: "WordPress.com eCommerce",
32+
formattedPrice: "NT$2,230"))
33+
}
34+
35+
func test_loadPlan_throws_noMatchingPlan_error_when_response_does_not_include_plan_with_given_id() async throws {
36+
// Given
37+
let remote = PaymentRemote(network: network)
38+
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-plan-success")
39+
40+
// When
41+
await assertThrowsError {
42+
_ = try await remote.loadPlan(thatMatchesID: 9)
43+
} errorAssert: { error in
44+
// Then
45+
(error as? LoadPlanError) == .noMatchingPlan
46+
}
47+
}
48+
49+
func test_loadPlan_throws_notFound_error_when_no_response() async throws {
50+
// Given
51+
let remote = PaymentRemote(network: network)
52+
53+
// When
54+
await assertThrowsError {
55+
_ = try await remote.loadPlan(thatMatchesID: 9)
56+
} errorAssert: { error in
57+
// Then
58+
(error as? NetworkError) == .notFound
59+
}
60+
}
61+
62+
// MARK: - `createCart`
63+
64+
func test_createCart_returns_on_success() async throws {
65+
// Given
66+
let siteID: Int64 = 606
67+
let remote = PaymentRemote(network: network)
68+
network.simulateResponse(requestUrlSuffix: "me/shopping-cart/\(siteID)", filename: "create-cart-success")
69+
70+
// When
71+
do {
72+
try await remote.createCart(siteID: siteID, productID: Constants.planProductID)
73+
} catch {
74+
// Then
75+
XCTFail("Unexpected error: \(error)")
76+
}
77+
}
78+
79+
func test_createCart_throws_productNotInCart_error_when_response_does_not_include_plan_with_given_id() async throws {
80+
// Given
81+
let siteID: Int64 = 606
82+
let remote = PaymentRemote(network: network)
83+
network.simulateResponse(requestUrlSuffix: "me/shopping-cart/\(siteID)", filename: "create-cart-success")
84+
85+
// When
86+
await assertThrowsError {
87+
_ = try await remote.createCart(siteID: siteID, productID: 685)
88+
} errorAssert: { error in
89+
// Then
90+
(error as? CreateCartError) == .productNotInCart
91+
}
92+
}
93+
94+
func test_createCart_throws_notFound_error_when_no_response() async throws {
95+
// Given
96+
let remote = PaymentRemote(network: network)
97+
98+
// When
99+
await assertThrowsError {
100+
_ = try await remote.createCart(siteID: 606, productID: 685)
101+
} errorAssert: { error in
102+
// Then
103+
(error as? NetworkError) == .notFound
104+
}
105+
}
106+
}
107+
108+
private extension PaymentRemoteTests {
109+
enum Constants {
110+
static let planProductID: Int64 = 1021
111+
}
112+
}

0 commit comments

Comments
 (0)