Skip to content

Commit 667d162

Browse files
authored
Merge pull request #6638 from woocommerce/merge/8.9.0.1-to-trunk
Merge 8.9.0.1 to trunk
2 parents 51e02b5 + 0357057 commit 667d162

File tree

47 files changed

+2799
-1324
lines changed

Some content is hidden

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

47 files changed

+2799
-1324
lines changed

Hardware/Hardware.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
55CD4BB4273E617C007686D3 /* ReceiptRendererTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD4BB3273E617C007686D3 /* ReceiptRendererTest.swift */; };
2222
5A747BE9FA06EC8752A35752 /* Pods_HardwareTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1DC5B6141B8184FAC29B0A4 /* Pods_HardwareTests.framework */; };
2323
8FFAA245E257B9EB98E2FCBD /* Pods_SampleReceiptPrinter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AFA997D6786C67B0A061854 /* Pods_SampleReceiptPrinter.framework */; };
24+
B9C4AB2327FDE133007008B8 /* Email.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C4AB2227FDE133007008B8 /* Email.swift */; };
2425
C5D2CB7D21CEE28FEBF18BF6 /* Pods_Hardware.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9F0AC202B287C1221EA2C99 /* Pods_Hardware.framework */; platformFilter = ios; };
2526
D80409A625FBE42B006F9BDA /* PaymentIntentParameters+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80409A525FBE42B006F9BDA /* PaymentIntentParameters+Stripe.swift */; };
2627
D80B4652260E19590092EDC0 /* PaymentIntentParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80B4651260E19590092EDC0 /* PaymentIntentParametersTests.swift */; };
@@ -156,6 +157,7 @@
156157
9726331F55A9621F2F887E13 /* Pods-Hardware.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Hardware.release.xcconfig"; path = "Target Support Files/Pods-Hardware/Pods-Hardware.release.xcconfig"; sourceTree = "<group>"; };
157158
AE60F16D43C20AD4523A61A5 /* Pods-SampleReceiptPrinter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleReceiptPrinter.debug.xcconfig"; path = "Target Support Files/Pods-SampleReceiptPrinter/Pods-SampleReceiptPrinter.debug.xcconfig"; sourceTree = "<group>"; };
158159
B1DC5B6141B8184FAC29B0A4 /* Pods_HardwareTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HardwareTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
160+
B9C4AB2227FDE133007008B8 /* Email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Email.swift; sourceTree = "<group>"; };
159161
C61D1642BE09D1A1AD6AA9FA /* Pods-HardwareTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HardwareTests.release.xcconfig"; path = "Target Support Files/Pods-HardwareTests/Pods-HardwareTests.release.xcconfig"; sourceTree = "<group>"; };
160162
C810BBAD03E7D4ECFD29D7AC /* Pods-SampleReceiptPrinter.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleReceiptPrinter.release-alpha.xcconfig"; path = "Target Support Files/Pods-SampleReceiptPrinter/Pods-SampleReceiptPrinter.release-alpha.xcconfig"; sourceTree = "<group>"; };
161163
D80409A525FBE42B006F9BDA /* PaymentIntentParameters+Stripe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentIntentParameters+Stripe.swift"; sourceTree = "<group>"; };
@@ -426,6 +428,7 @@
426428
D845BDC1262D98C400A3E40F /* CardBrand.swift */,
427429
D845BDD9262DAADB00A3E40F /* PaymentMethod.swift */,
428430
317975BF274EB1F9004357B1 /* DeclineReason.swift */,
431+
B9C4AB2227FDE133007008B8 /* Email.swift */,
429432
);
430433
path = CardReader;
431434
sourceTree = "<group>";
@@ -768,6 +771,7 @@
768771
isa = PBXSourcesBuildPhase;
769772
buildActionMask = 2147483647;
770773
files = (
774+
B9C4AB2327FDE133007008B8 /* Email.swift in Sources */,
771775
D845BE59262ED84000A3E40F /* AirPrintReceiptPrinterService.swift in Sources */,
772776
317975C2274EBC1F004357B1 /* DeclineReason+Stripe.swift in Sources */,
773777
D845BDB8262D97B300A3E40F /* ReceiptDetails.swift in Sources */,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// A property wrapper to validate that a property is a valid email
2+
/// Property Wrappers can not throw, so
3+
/// what this wrapper does is return a nil when trying to set an invalid
4+
/// email address as a value of a property of type String.
5+
/// The reason to do this is add an extra layer of validation before passing
6+
/// an instance of PaymentIntentParameters to the Stripe Terminal SDK
7+
/// https://emailregex.com
8+
@propertyWrapper
9+
public struct Email<Value: StringProtocol> {
10+
var value: Value?
11+
12+
public init(wrappedValue value: Value?) {
13+
self.value = value
14+
}
15+
16+
public var wrappedValue: Value? {
17+
get {
18+
return validate(email: value) ? value : nil
19+
}
20+
set {
21+
value = newValue
22+
}
23+
}
24+
private func validate(email: Value?) -> Bool {
25+
guard let email = email else { return false }
26+
// https://emailregex.com
27+
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
28+
let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
29+
return emailPred.evaluate(with: email)
30+
}
31+
}

Hardware/Hardware/CardReader/PaymentIntentParameters.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public struct PaymentIntentParameters {
2424
@StatementDescriptor
2525
public private(set) var statementDescription: String?
2626

27+
/// Email address that the receipt for the resulting payment will be sent to.
28+
@Email
29+
public private(set) var receiptEmail: String?
30+
2731
/// Set of key-value pairs that you can attach to an object.
2832
/// This can be useful for storing additional information about the object in a structured format.
2933
public let metadata: [AnyHashable: Any]?
@@ -38,12 +42,14 @@ public struct PaymentIntentParameters {
3842
currency: String,
3943
receiptDescription: String? = nil,
4044
statementDescription: String? = nil,
45+
receiptEmail: String? = nil,
4146
paymentMethodTypes: [String] = [],
4247
metadata: [AnyHashable: Any]? = nil) {
4348
self.amount = amount
4449
self.currency = currency
4550
self.receiptDescription = receiptDescription
4651
self.statementDescription = statementDescription
52+
self.receiptEmail = receiptEmail
4753
self.paymentMethodTypes = paymentMethodTypes
4854
self.metadata = metadata
4955
}

Hardware/Hardware/CardReader/StripeCardReader/PaymentIntentParameters+Stripe.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ extension Hardware.PaymentIntentParameters {
3232
returnValue.statementDescriptor = descriptor
3333
}
3434

35+
returnValue.receiptEmail = receiptEmail
3536
returnValue.metadata = metadata
3637

3738
return returnValue

Hardware/HardwareTests/PaymentIntentParametersTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import XCTest
22
@testable import Hardware
33

44
final class PaymentIntentParametersTests: XCTestCase {
5+
func test_validEmail_is_saved() {
6+
let params = PaymentIntentParameters(amount: 100, currency: "usd", receiptEmail: "[email protected]", paymentMethodTypes: ["card_present"])
7+
8+
XCTAssertNotNil(params.receiptEmail)
9+
}
10+
11+
func test_not_validEmail_is_ignored() {
12+
let params = PaymentIntentParameters(amount: 100, currency: "usd", receiptEmail: "woocommerce", paymentMethodTypes: ["card_present"])
13+
14+
XCTAssertNil(params.receiptEmail)
15+
}
16+
517
func test_currency_is_lowercased() {
618
let params = PaymentIntentParameters(amount: 100, currency: "USD", paymentMethodTypes: ["card_present"])
719

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Yosemite
2+
import Foundation
3+
import Storage
4+
5+
/// Provides data and helper methods related to the Payments Plugins (WCPay, Stripe).
6+
/// It extracts the information from the provided `StorageManagerType`, but please notice that it does not
7+
/// take care of syncing the data, so it should be done beforehand.
8+
///
9+
protocol CardPresentPluginsDataProviderProtocol {
10+
func getWCPayPlugin() -> Yosemite.SystemPlugin?
11+
func getStripePlugin() -> Yosemite.SystemPlugin?
12+
func bothPluginsInstalledAndActive(wcPay: Yosemite.SystemPlugin?, stripe: Yosemite.SystemPlugin?) -> Bool
13+
func wcPayInstalledAndActive(wcPay: Yosemite.SystemPlugin?) -> Bool
14+
func stripeInstalledAndActive(stripe: Yosemite.SystemPlugin?) -> Bool
15+
func isWCPayVersionSupported(plugin: Yosemite.SystemPlugin) -> Bool
16+
func isStripeVersionSupported(plugin: Yosemite.SystemPlugin) -> Bool
17+
}
18+
19+
struct CardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol {
20+
private let storageManager: StorageManagerType
21+
private let stores: StoresManager
22+
23+
init(
24+
storageManager: StorageManagerType = ServiceLocator.storageManager,
25+
stores: StoresManager = ServiceLocator.stores
26+
) {
27+
self.storageManager = storageManager
28+
self.stores = stores
29+
}
30+
31+
private var siteID: Int64? {
32+
stores.sessionManager.defaultStoreID
33+
}
34+
35+
func getWCPayPlugin() -> Yosemite.SystemPlugin? {
36+
guard let siteID = siteID else {
37+
return nil
38+
}
39+
return storageManager.viewStorage
40+
.loadSystemPlugin(siteID: siteID, name: CardPresentPaymentsPlugins.wcPay.pluginName)?
41+
.toReadOnly()
42+
}
43+
44+
func getStripePlugin() -> Yosemite.SystemPlugin? {
45+
guard let siteID = siteID else {
46+
return nil
47+
}
48+
return storageManager.viewStorage
49+
.loadSystemPlugin(siteID: siteID, name: CardPresentPaymentsPlugins.stripe.pluginName)?
50+
.toReadOnly()
51+
}
52+
53+
func bothPluginsInstalledAndActive(wcPay: Yosemite.SystemPlugin?, stripe: Yosemite.SystemPlugin?) -> Bool {
54+
guard let wcPay = wcPay, let stripe = stripe else {
55+
return false
56+
}
57+
58+
return wcPay.active && stripe.active
59+
}
60+
61+
func wcPayInstalledAndActive(wcPay: Yosemite.SystemPlugin?) -> Bool {
62+
// If the WCPay plugin is not installed, immediately return false
63+
guard let wcPay = wcPay else {
64+
return false
65+
}
66+
67+
return wcPay.active
68+
}
69+
70+
func stripeInstalledAndActive(stripe: Yosemite.SystemPlugin?) -> Bool {
71+
// If the Stripe plugin is not installed, immediately return false
72+
guard let stripe = stripe else {
73+
return false
74+
}
75+
76+
return stripe.active
77+
}
78+
79+
func isWCPayVersionSupported(plugin: Yosemite.SystemPlugin) -> Bool {
80+
VersionHelpers.isVersionSupported(version: plugin.version, minimumRequired: CardPresentPaymentsPlugins.wcPay.minimumSupportedPluginVersion)
81+
}
82+
83+
func isStripeVersionSupported(plugin: Yosemite.SystemPlugin) -> Bool {
84+
VersionHelpers.isVersionSupported(version: plugin.version, minimumRequired: CardPresentPaymentsPlugins.stripe.minimumSupportedPluginVersion)
85+
}
86+
}

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct CardPresentCapturedPaymentData {
1919
final class PaymentCaptureOrchestrator {
2020
private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
2121
private let personNameComponentsFormatter = PersonNameComponentsFormatter()
22+
private let paymentReceiptEmailParameterDeterminer = PaymentReceiptEmailParameterDeterminer()
2223

2324
private let celebration = PaymentCaptureCelebration()
2425

@@ -45,47 +46,50 @@ final class PaymentCaptureOrchestrator {
4546

4647
ServiceLocator.stores.dispatch(setAccount)
4748

48-
guard let parameters = paymentParameters(
49+
paymentParameters(
4950
order: order,
5051
statementDescriptor: paymentGatewayAccount.statementDescriptor,
5152
paymentMethodTypes: paymentMethodTypes
52-
) else {
53-
DDLogError("Error: failed to create payment parameters for an order")
54-
onCompletion(.failure(CardReaderServiceError.paymentCapture()))
55-
return
56-
}
53+
) { [weak self] result in
54+
guard let self = self else { return }
5755

58-
/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
59-
/// reader begins to collect payment.
60-
///
61-
suppressPassPresentation()
62-
63-
let paymentAction = CardPresentPaymentAction.collectPayment(
64-
siteID: order.siteID,
65-
orderID: order.orderID,
66-
parameters: parameters,
67-
onCardReaderMessage: { (event) in
68-
switch event {
69-
case .waitingForInput:
70-
onWaitingForInput()
71-
case .displayMessage(let message):
72-
onDisplayMessage(message)
73-
default:
74-
break
75-
}
76-
},
77-
onCompletion: { [weak self] result in
78-
self?.allowPassPresentation()
79-
onProcessingMessage()
80-
self?.completePaymentIntentCapture(
81-
order: order,
82-
captureResult: result,
83-
onCompletion: onCompletion
56+
switch result {
57+
case let .success(parameters):
58+
/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
59+
/// reader begins to collect payment.
60+
///
61+
self.suppressPassPresentation()
62+
63+
let paymentAction = CardPresentPaymentAction.collectPayment(
64+
siteID: order.siteID,
65+
orderID: order.orderID,
66+
parameters: parameters,
67+
onCardReaderMessage: { (event) in
68+
switch event {
69+
case .waitingForInput:
70+
onWaitingForInput()
71+
case .displayMessage(let message):
72+
onDisplayMessage(message)
73+
default:
74+
break
75+
}
76+
},
77+
onCompletion: { [weak self] result in
78+
self?.allowPassPresentation()
79+
onProcessingMessage()
80+
self?.completePaymentIntentCapture(
81+
order: order,
82+
captureResult: result,
83+
onCompletion: onCompletion
84+
)
85+
}
8486
)
85-
}
86-
)
8787

88-
ServiceLocator.stores.dispatch(paymentAction)
88+
ServiceLocator.stores.dispatch(paymentAction)
89+
case let .failure(error):
90+
onCompletion(Result.failure(error))
91+
}
92+
}
8993
}
9094

9195
func cancelPayment(onCompletion: @escaping (Result<Void, Error>) -> Void) {
@@ -210,27 +214,42 @@ private extension PaymentCaptureOrchestrator {
210214
ServiceLocator.stores.dispatch(action)
211215
}
212216

213-
func paymentParameters(order: Order, statementDescriptor: String?, paymentMethodTypes: [String]) -> PaymentParameters? {
217+
func paymentParameters(order: Order,
218+
statementDescriptor: String?,
219+
paymentMethodTypes: [String],
220+
onCompletion: @escaping ((Result<PaymentParameters, Error>) -> Void)) {
214221
guard let orderTotal = currencyFormatter.convertToDecimal(from: order.total) else {
215222
DDLogError("Error: attempted to collect payment for an order without a valid total.")
216-
return nil
223+
onCompletion(Result.failure(NotValidAmountError.other))
224+
225+
return
217226
}
218227

219-
let metadata = PaymentIntent.initMetadata(
220-
store: ServiceLocator.stores.sessionManager.defaultSite?.name,
221-
customerName: buildCustomerNameFromBillingAddress(order.billingAddress),
222-
customerEmail: order.billingAddress?.email,
223-
siteURL: ServiceLocator.stores.sessionManager.defaultSite?.url,
224-
orderID: order.orderID,
225-
paymentType: PaymentIntent.PaymentTypes.single
226-
)
227-
228-
return PaymentParameters(amount: orderTotal as Decimal,
229-
currency: order.currency,
230-
receiptDescription: receiptDescription(orderNumber: order.number),
231-
statementDescription: statementDescriptor,
232-
paymentMethodTypes: paymentMethodTypes,
233-
metadata: metadata)
228+
paymentReceiptEmailParameterDeterminer.receiptEmail(from: order) { result in
229+
var receiptEmail: String?
230+
if case let .success(email) = result {
231+
receiptEmail = email
232+
}
233+
234+
let metadata = PaymentIntent.initMetadata(
235+
store: ServiceLocator.stores.sessionManager.defaultSite?.name,
236+
customerName: self.buildCustomerNameFromBillingAddress(order.billingAddress),
237+
customerEmail: order.billingAddress?.email,
238+
siteURL: ServiceLocator.stores.sessionManager.defaultSite?.url,
239+
orderID: order.orderID,
240+
paymentType: PaymentIntent.PaymentTypes.single
241+
)
242+
243+
let parameters = PaymentParameters(amount: orderTotal as Decimal,
244+
currency: order.currency,
245+
receiptDescription: self.receiptDescription(orderNumber: order.number),
246+
statementDescription: statementDescriptor,
247+
receiptEmail: receiptEmail,
248+
paymentMethodTypes: paymentMethodTypes,
249+
metadata: metadata)
250+
251+
onCompletion(Result.success(parameters))
252+
}
234253
}
235254

236255
func receiptDescription(orderNumber: String) -> String? {

0 commit comments

Comments
 (0)