Skip to content

Commit 118a3b8

Browse files
authored
Merge pull request #8205 from woocommerce/feat/8108-web-checkout-yosemite
Store creation M2: checkout flow - Yosemite layer changes
2 parents 2464fca + 3edd299 commit 118a3b8

File tree

5 files changed

+311
-0
lines changed

5 files changed

+311
-0
lines changed

Yosemite/Yosemite.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
02E4F5E423CD5628003B0010 /* NSOrderedSet+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4F5E323CD5628003B0010 /* NSOrderedSet+Array.swift */; };
8383
02E7FFD52562226B00C53030 /* ShippingLabelStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFD42562226B00C53030 /* ShippingLabelStoreTests.swift */; };
8484
02E7FFD92562234F00C53030 /* MockShippingLabelRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFD82562234F00C53030 /* MockShippingLabelRemote.swift */; };
85+
02EF1666292DB65000D90AD6 /* PaymentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1665292DB65000D90AD6 /* PaymentStore.swift */; };
86+
02EF1668292DB68C00D90AD6 /* PaymentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1667292DB68C00D90AD6 /* PaymentAction.swift */; };
87+
02F2722D292F18BF00C36419 /* PaymentStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */; };
88+
02F2722F292F18FD00C36419 /* MockPaymentRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */; };
8589
02F6AAAC270556A4002425D0 /* Models+Copiable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6AAAB270556A4002425D0 /* Models+Copiable.generated.swift */; };
8690
02FF054D23D983F30058E6E7 /* MediaFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF054523D983F30058E6E7 /* MediaFileManager.swift */; };
8791
02FF054E23D983F30058E6E7 /* MediaImageExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF054623D983F30058E6E7 /* MediaImageExporter.swift */; };
@@ -507,6 +511,10 @@
507511
02E4F5E323CD5628003B0010 /* NSOrderedSet+Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSOrderedSet+Array.swift"; sourceTree = "<group>"; };
508512
02E7FFD42562226B00C53030 /* ShippingLabelStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelStoreTests.swift; sourceTree = "<group>"; };
509513
02E7FFD82562234F00C53030 /* MockShippingLabelRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShippingLabelRemote.swift; sourceTree = "<group>"; };
514+
02EF1665292DB65000D90AD6 /* PaymentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStore.swift; sourceTree = "<group>"; };
515+
02EF1667292DB68C00D90AD6 /* PaymentAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentAction.swift; sourceTree = "<group>"; };
516+
02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStoreTests.swift; sourceTree = "<group>"; };
517+
02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaymentRemote.swift; sourceTree = "<group>"; };
510518
02F6AAAB270556A4002425D0 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
511519
02FF054523D983F30058E6E7 /* MediaFileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFileManager.swift; sourceTree = "<group>"; };
512520
02FF054623D983F30058E6E7 /* MediaImageExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaImageExporter.swift; sourceTree = "<group>"; };
@@ -1206,6 +1214,7 @@
12061214
021BA0C328576940006E9886 /* MockDotcomAccountRemote.swift */,
12071215
02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */,
12081216
02616F932921E1CD0095BC00 /* MockSiteRemote.swift */,
1217+
02F2722E292F18FD00C36419 /* MockPaymentRemote.swift */,
12091218
);
12101219
path = Remote;
12111220
sourceTree = "<group>";
@@ -1419,6 +1428,7 @@
14191428
02E3B622290267D3007E0F13 /* AccountCreationStore.swift */,
14201429
02393066291A02AC00B2632F /* DomainStore.swift */,
14211430
021940E5291E8AD80090354E /* SiteStore.swift */,
1431+
02EF1665292DB65000D90AD6 /* PaymentStore.swift */,
14221432
);
14231433
path = Stores;
14241434
sourceTree = "<group>";
@@ -1480,6 +1490,7 @@
14801490
02E3B629290622DE007E0F13 /* AccountCreationStoreTests.swift */,
14811491
02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */,
14821492
02616F912921E1530095BC00 /* SiteStoreTests.swift */,
1493+
02F2722C292F18BF00C36419 /* PaymentStoreTests.swift */,
14831494
);
14841495
path = Stores;
14851496
sourceTree = "<group>";
@@ -1667,6 +1678,7 @@
16671678
02E3B624290267F2007E0F13 /* AccountCreationAction.swift */,
16681679
02393064291A018600B2632F /* DomainAction.swift */,
16691680
021940E3291E8A660090354E /* SiteAction.swift */,
1681+
02EF1667292DB68C00D90AD6 /* PaymentAction.swift */,
16701682
);
16711683
path = Actions;
16721684
sourceTree = "<group>";
@@ -2001,6 +2013,7 @@
20012013
261F94E6242EFF8700762B58 /* ProductCategoryStore.swift in Sources */,
20022014
57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */,
20032015
7492FAD9217FAD1000ED2C69 /* SiteSetting+ReadOnlyConvertible.swift in Sources */,
2016+
02EF1666292DB65000D90AD6 /* PaymentStore.swift in Sources */,
20042017
E1BD4D0027ABF84D006416D9 /* CardPresentPaymentsConfiguration.swift in Sources */,
20052018
CE3B7AD52225EBF10050FE4B /* OrderStatusAction.swift in Sources */,
20062019
7493750C224987D9007D85D1 /* ProductAttribute+ReadOnlyConvertible.swift in Sources */,
@@ -2115,6 +2128,7 @@
21152128
B52E002E211A3F5500700FDE /* ReadOnlyType.swift in Sources */,
21162129
5726456F250BD4E4005BBD7C /* OrdersUpsertUseCase.swift in Sources */,
21172130
3147030C2670333200EF253A /* WCPayAccount+PaymentGatewayAccount.swift in Sources */,
2131+
02EF1668292DB68C00D90AD6 /* PaymentAction.swift in Sources */,
21182132
B5C9DE162087FF0E006B910A /* Store.swift in Sources */,
21192133
02FF056123D98FD40058E6E7 /* ImageSourceWriter.swift in Sources */,
21202134
0212AC5E242C67FA00C51F6C /* ProductsSortOrder.swift in Sources */,
@@ -2279,6 +2293,7 @@
22792293
02FF055B23D9846A0058E6E7 /* MediaDirectoryTests.swift in Sources */,
22802294
265BCA0024301ACD004E53EE /* ProductCategoryStoreTests.swift in Sources */,
22812295
02FF056D23DEDCB90058E6E7 /* MockImageSourceWriter.swift in Sources */,
2296+
02F2722F292F18FD00C36419 /* MockPaymentRemote.swift in Sources */,
22822297
FE28F6F2268462A6004465C7 /* UserStoreTests.swift in Sources */,
22832298
02DAE7FA291A9F36009342B7 /* MockDomainRemote.swift in Sources */,
22842299
741F34842195F752005F5BD9 /* CommentStoreTests.swift in Sources */,
@@ -2295,6 +2310,7 @@
22952310
031FD8A026FC970400B315C7 /* RosettaTestingHelper.swift in Sources */,
22962311
077F39E526A5C98200ABEADC /* SystemStatusStoreTests.swift in Sources */,
22972312
022F931D257F27B40011CD94 /* MockShippingLabelAddress.swift in Sources */,
2313+
02F2722D292F18BF00C36419 /* PaymentStoreTests.swift in Sources */,
22982314
02FF056923DECD5B0058E6E7 /* MediaImageExporterTests.swift in Sources */,
22992315
0202B6972387AFBF00F3EBE0 /* MockInMemoryStorage.swift in Sources */,
23002316
028BCE2422DE22BB00056966 /* SiteVisitStatsStoreErrorTests.swift in Sources */,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Foundation
2+
3+
/// PaymentAction: Defines all of the Actions supported by the PaymentStore.
4+
///
5+
public enum PaymentAction: Action {
6+
/// Loads a specific WPCOM plan.
7+
/// - Parameters:
8+
/// - productID: The ID of the WPCOM product to return.
9+
/// - completion: Invoked when the WPCOM plan that matches the given ID is loaded.
10+
case loadPlan(productID: Int64,
11+
completion: (Result<WPComPlan, Error>) -> Void)
12+
13+
/// Creates a cart with a WPCOM plan.
14+
/// - Parameters:
15+
/// - productID: The ID of the WPCOM plan product. It is of string type to integrate with `InAppPurchasesForWPComPlansProtocol`.
16+
/// If the value is not a string of integer value, an error `CreateCartError.invalidProductID` is returned.
17+
/// - siteID: The site ID for the WPCOM plan to be attached to.
18+
/// - completion: The result of cart creation.
19+
case createCart(productID: String,
20+
siteID: Int64,
21+
completion: (Result<Void, Error>) -> Void)
22+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Foundation
2+
import Networking
3+
import protocol Storage.StorageManagerType
4+
5+
/// Handles `PaymentAction`
6+
///
7+
public final class PaymentStore: Store {
8+
// Keeps a strong reference to remote to keep requests alive.
9+
private let remote: PaymentRemoteProtocol
10+
11+
public init(remote: PaymentRemoteProtocol,
12+
dispatcher: Dispatcher,
13+
storageManager: StorageManagerType,
14+
network: Network) {
15+
self.remote = remote
16+
super.init(dispatcher: dispatcher, storageManager: storageManager, network: network)
17+
}
18+
19+
public convenience override init(dispatcher: Dispatcher,
20+
storageManager: StorageManagerType,
21+
network: Network) {
22+
let remote = PaymentRemote(network: network)
23+
self.init(remote: remote,
24+
dispatcher: dispatcher,
25+
storageManager: storageManager,
26+
network: network)
27+
}
28+
29+
public override func registerSupportedActions(in dispatcher: Dispatcher) {
30+
dispatcher.register(processor: self, for: PaymentAction.self)
31+
}
32+
33+
/// Called whenever a given Action is dispatched.
34+
///
35+
public override func onAction(_ action: Action) {
36+
guard let action = action as? PaymentAction else {
37+
assertionFailure("PaymentStore received an unsupported action: \(action)")
38+
return
39+
}
40+
switch action {
41+
case .loadPlan(let productID, let completion):
42+
loadPlan(productID: productID, completion: completion)
43+
case .createCart(let productID, let siteID, let completion):
44+
createCart(productID: productID, siteID: siteID, completion: completion)
45+
}
46+
}
47+
}
48+
49+
private extension PaymentStore {
50+
func loadPlan(productID: Int64,
51+
completion: @escaping (Result<WPComPlan, Error>) -> Void) {
52+
Task { @MainActor in
53+
do {
54+
let plan = try await remote.loadPlan(thatMatchesID: productID)
55+
completion(.success(plan))
56+
} catch {
57+
completion(.failure(error))
58+
}
59+
}
60+
}
61+
62+
func createCart(productID: String,
63+
siteID: Int64,
64+
completion: @escaping (Result<Void, Error>) -> Void) {
65+
Task { @MainActor in
66+
do {
67+
guard let productID = Int64(productID) else {
68+
return completion(.failure(CreateCartError.invalidProductID))
69+
}
70+
_ = try await remote.createCart(siteID: siteID, productID: productID)
71+
completion(.success(()))
72+
} catch {
73+
switch error {
74+
case let networkError as Networking.CreateCartError:
75+
switch networkError {
76+
case .productNotInCart:
77+
completion(.failure(CreateCartError.productNotInCart))
78+
}
79+
default:
80+
completion(.failure(error))
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
/// Possible cart creation errors.
88+
public enum CreateCartError: Error, Equatable {
89+
/// Product ID is not in the correct format for WPCOM plans.
90+
case invalidProductID
91+
/// The expected product is not in the created cart.
92+
case productNotInCart
93+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Networking
2+
import XCTest
3+
4+
/// Mock for `PaymentRemote`.
5+
///
6+
final class MockPaymentRemote {
7+
/// The results to return in `loadPlan`.
8+
private var loadPlanResult: Result<WPComPlan, Error>?
9+
10+
/// The results to return in `createCart`.
11+
private var createCartResult: Result<Void, Error>?
12+
13+
/// Returns the value when `loadPlan` is called.
14+
func whenLoadingPlan(thenReturn result: Result<WPComPlan, Error>) {
15+
loadPlanResult = result
16+
}
17+
18+
/// Returns the value when `createCart` is called.
19+
func whenCreatingCart(thenReturn result: Result<Void, Error>) {
20+
createCartResult = result
21+
}
22+
}
23+
24+
extension MockPaymentRemote: PaymentRemoteProtocol {
25+
func loadPlan(thatMatchesID productID: Int64) async throws -> Networking.WPComPlan {
26+
guard let result = loadPlanResult else {
27+
XCTFail("Could not find result for loading a plan.")
28+
throw NetworkError.notFound
29+
}
30+
return try result.get()
31+
}
32+
33+
func createCart(siteID: Int64, productID: Int64) async throws {
34+
guard let result = createCartResult else {
35+
XCTFail("Could not find result for creating a cart.")
36+
throw NetworkError.notFound
37+
}
38+
return try result.get()
39+
}
40+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import TestKit
2+
import XCTest
3+
@testable import Networking
4+
@testable import Yosemite
5+
6+
final class PaymentStoreTests: XCTestCase {
7+
/// Mock Dispatcher.
8+
private var dispatcher: Dispatcher!
9+
10+
/// Mock Storage: InMemory.
11+
private var storageManager: MockStorageManager!
12+
13+
/// Mock Network: Allows us to inject predefined responses.
14+
private var network: MockNetwork!
15+
16+
private var remote: MockPaymentRemote!
17+
private var store: PaymentStore!
18+
19+
override func setUp() {
20+
super.setUp()
21+
dispatcher = Dispatcher()
22+
storageManager = MockStorageManager()
23+
network = MockNetwork()
24+
remote = MockPaymentRemote()
25+
store = PaymentStore(remote: remote, dispatcher: dispatcher, storageManager: storageManager, network: network)
26+
}
27+
28+
override func tearDown() {
29+
store = nil
30+
remote = nil
31+
network = nil
32+
storageManager = nil
33+
dispatcher = nil
34+
super.tearDown()
35+
}
36+
37+
// MARK: - `loadPlan`
38+
39+
func test_loadPlan_returns_plan_on_success() throws {
40+
// Given
41+
remote.whenLoadingPlan(thenReturn: .success(.init(productID: 12, name: "woo", formattedPrice: "$16.8")))
42+
43+
// When
44+
let result = waitFor { promise in
45+
self.store.onAction(PaymentAction.loadPlan(productID: 12) { result in
46+
promise(result)
47+
})
48+
}
49+
50+
// Then
51+
XCTAssertTrue(result.isSuccess)
52+
let plan = try XCTUnwrap(result.get())
53+
XCTAssertEqual(plan, .init(productID: 12, name: "woo", formattedPrice: "$16.8"))
54+
}
55+
56+
func test_loadPlan_returns_failure_on_error() throws {
57+
// Given
58+
remote.whenLoadingPlan(thenReturn: .failure(NetworkError.timeout))
59+
60+
// When
61+
let result = waitFor { promise in
62+
self.store.onAction(PaymentAction.loadPlan(productID: 12) { result in
63+
promise(result)
64+
})
65+
}
66+
67+
// Then
68+
XCTAssertTrue(result.isFailure)
69+
let error = try XCTUnwrap(result.failure)
70+
XCTAssertEqual(error as? NetworkError, .timeout)
71+
}
72+
73+
// MARK: - `createCart`
74+
75+
func test_createCart_returns_on_success() throws {
76+
// Given
77+
remote.whenCreatingCart(thenReturn: .success(()))
78+
79+
// When
80+
let result = waitFor { promise in
81+
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
82+
promise(result)
83+
})
84+
}
85+
86+
// Then
87+
XCTAssertTrue(result.isSuccess)
88+
}
89+
90+
func test_createCart_returns_invalidProductID_error_when_productID_is_not_integer() throws {
91+
// Given
92+
remote.whenCreatingCart(thenReturn: .failure(NetworkError.timeout))
93+
94+
// When
95+
let result = waitFor { promise in
96+
self.store.onAction(PaymentAction.createCart(productID: "wo0", siteID: 62) { result in
97+
promise(result)
98+
})
99+
}
100+
101+
// Then
102+
XCTAssertTrue(result.isFailure)
103+
let error = try XCTUnwrap(result.failure)
104+
XCTAssertEqual(error as? Yosemite.CreateCartError, .invalidProductID)
105+
}
106+
107+
func test_createCart_relays_networking_CreateCartError_failure() throws {
108+
// Given
109+
remote.whenCreatingCart(thenReturn: .failure(Networking.CreateCartError.productNotInCart))
110+
111+
// When
112+
let result = waitFor { promise in
113+
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
114+
promise(result)
115+
})
116+
}
117+
118+
// Then
119+
XCTAssertTrue(result.isFailure)
120+
let error = try XCTUnwrap(result.failure)
121+
XCTAssertEqual(error as? Yosemite.CreateCartError, .productNotInCart)
122+
}
123+
124+
func test_createCart_returns_failure_on_error() throws {
125+
// Given
126+
remote.whenCreatingCart(thenReturn: .failure(NetworkError.timeout))
127+
128+
// When
129+
let result = waitFor { promise in
130+
self.store.onAction(PaymentAction.createCart(productID: "12", siteID: 62) { result in
131+
promise(result)
132+
})
133+
}
134+
135+
// Then
136+
XCTAssertTrue(result.isFailure)
137+
let error = try XCTUnwrap(result.failure)
138+
XCTAssertEqual(error as? NetworkError, .timeout)
139+
}
140+
}

0 commit comments

Comments
 (0)