Restrict manual confirmation usage to FlowController (#2633)

* Restrict manual confirmation usage to FlowController

* Add comment
This commit is contained in:
Nick Porter 2023-06-07 09:25:11 -07:00 committed by GitHub
parent 53f4a18511
commit b0a9cad965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 57 additions and 12 deletions

View File

@ -14,7 +14,9 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
func testMismatchedIntentAndIntentConfiguration() throws {
let pi = STPFixtures.makePaymentIntent()
let intentConfig_si = PaymentSheet.IntentConfiguration(mode: .setup(currency: "USD"), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi, intentConfiguration: intentConfig_si)) { error in
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig_si,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. You returned a PaymentIntent client secret but used a PaymentSheet.IntentConfiguration in setup mode.")
}
let si = STPFixtures.makeSetupIntent()
@ -27,7 +29,9 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
func testPaymentIntentMismatchedCurrency() throws {
let pi = STPFixtures.makePaymentIntent(amount: 100, currency: "GBP")
let intentConfig = PaymentSheet.IntentConfiguration(mode: .payment(amount: 100, currency: "USD"), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi, intentConfiguration: intentConfig)) { error in
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. Your PaymentIntent currency (GBP) does not match the PaymentSheet.IntentConfiguration currency (USD).")
}
}
@ -35,7 +39,9 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
func testPaymentIntentMismatchedSetupFutureUsage() throws {
let pi = STPFixtures.makePaymentIntent(amount: 100, currency: "USD", setupFutureUsage: .offSession)
let intentConfig = PaymentSheet.IntentConfiguration(mode: .payment(amount: 100, currency: "USD"), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi, intentConfiguration: intentConfig)) { error in
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. Your PaymentIntent setupFutureUsage (offSession) does not match the PaymentSheet.IntentConfiguration setupFutureUsage (nil).")
}
}
@ -43,7 +49,9 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
func testPaymentIntentMismatchedAmount() throws {
let pi = STPFixtures.makePaymentIntent(amount: 1000, currency: "USD")
let intentConfig = PaymentSheet.IntentConfiguration(mode: .payment(amount: 100, currency: "USD"), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi, intentConfiguration: intentConfig)) { error in
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. Your PaymentIntent amount (1000) does not match the PaymentSheet.IntentConfiguration amount (100).")
}
}
@ -51,11 +59,23 @@ final class PaymentSheetDeferredValidatorTests: XCTestCase {
func testPaymentIntentMismatchedCaptureMethod() throws {
let pi = STPFixtures.makePaymentIntent(amount: 100, currency: "USD", captureMethod: "manual")
let intentConfig = PaymentSheet.IntentConfiguration(mode: .payment(amount: 100, currency: "USD", captureMethod: .automatic), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi, intentConfiguration: intentConfig)) { error in
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. Your PaymentIntent captureMethod (manual) does not match the PaymentSheet.IntentConfiguration amount (automatic).")
}
}
func testPaymentIntentNotFlowControllerManualConfirmationMethod() throws {
let pi = STPFixtures.makePaymentIntent(amount: 1000, currency: "USD", confirmationMethod: "manual")
let intentConfig = PaymentSheet.IntentConfiguration(mode: .payment(amount: 1000, currency: "USD"), confirmHandler: confirmHandler)
XCTAssertThrowsError(try PaymentSheetDeferredValidator.validate(paymentIntent: pi,
intentConfiguration: intentConfig,
isFlowController: false)) { error in
XCTAssertEqual("\(error)", "An error occured in PaymentSheet. Your PaymentIntent confirmationMethod (manual) can only be used with PaymentSheet.FlowController.")
}
}
func testSetupIntentMismatchedUsage() throws {
let si = STPFixtures.makeSetupIntent(usage: "on_session")
let intentConfig = PaymentSheet.IntentConfiguration(mode: .setup(currency: "USD", setupFutureUsage: .offSession), confirmHandler: confirmHandler)

View File

@ -717,6 +717,7 @@ extension STPFixtures {
setupFutureUsage: STPPaymentIntentSetupFutureUsage? = nil,
paymentMethodOptions: STPPaymentMethodOptions? = nil,
captureMethod: String = "automatic",
confirmationMethod: String = "automatic",
shippingProvided: Bool = false
) -> STPPaymentIntent {
var json = STPTestUtils.jsonNamed(STPTestJSONPaymentIntent)!
@ -726,6 +727,7 @@ extension STPFixtures {
json["amount"] = amount
json["currency"] = currency
json["capture_method"] = captureMethod
json["confirmation_method"] = confirmationMethod
if let paymentMethodTypes = paymentMethodTypes {
json["payment_method_types"] = paymentMethodTypes.map {
STPPaymentMethod.string(from: $0)

View File

@ -163,7 +163,8 @@ import UIKit
authenticationContext: AuthenticationContext(presentingViewController: presentingViewController, appearance: .default),
intent: intent,
paymentOption: paymentOption,
paymentHandler: STPPaymentHandler(apiClient: configuration.apiClient)
paymentHandler: STPPaymentHandler(apiClient: configuration.apiClient),
isFlowController: false
) { result in
switch result {
case .completed:

View File

@ -80,6 +80,7 @@ extension PayWithLinkController: PayWithLinkViewControllerDelegate {
intent: intent,
paymentOption: paymentOption,
paymentHandler: paymentHandler,
isFlowController: false,
completion: completion
)
}

View File

@ -33,6 +33,7 @@ extension PaymentSheet {
intent: Intent,
paymentOption: PaymentOption,
paymentHandler: STPPaymentHandler,
isFlowController: Bool = false,
paymentMethodID: String? = nil,
completion: @escaping (PaymentSheetResult) -> Void
) {
@ -129,6 +130,7 @@ extension PaymentSheet {
intentConfig: intentConfig,
authenticationContext: authenticationContext,
paymentHandler: paymentHandler,
isFlowController: isFlowController,
completion: completion
)
}
@ -170,6 +172,7 @@ extension PaymentSheet {
intentConfig: intentConfig,
authenticationContext: authenticationContext,
paymentHandler: paymentHandler,
isFlowController: isFlowController,
completion: completion
)
}
@ -206,6 +209,7 @@ extension PaymentSheet {
intentConfig: intentConfig,
authenticationContext: authenticationContext,
paymentHandler: paymentHandler,
isFlowController: isFlowController,
completion: completion
)
}

View File

@ -18,6 +18,7 @@ extension PaymentSheet {
intentConfig: PaymentSheet.IntentConfiguration,
authenticationContext: STPAuthenticationContext,
paymentHandler: STPPaymentHandler,
isFlowController: Bool,
completion: @escaping (PaymentSheetResult) -> Void
) {
// Hack: Add deferred to analytics product usage as a hack to get it into the payment_user_agent string in the request to create a PaymentMethod
@ -51,7 +52,9 @@ extension PaymentSheet {
switch intentConfig.mode {
case .payment:
let paymentIntent = try await configuration.apiClient.retrievePaymentIntent(clientSecret: clientSecret, expand: ["payment_method"])
try PaymentSheetDeferredValidator.validate(paymentIntent: paymentIntent, intentConfiguration: intentConfig)
try PaymentSheetDeferredValidator.validate(paymentIntent: paymentIntent,
intentConfiguration: intentConfig,
isFlowController: isFlowController)
// Check if it needs confirmation
if [STPPaymentIntentStatus.requiresPaymentMethod, STPPaymentIntentStatus.requiresConfirmation].contains(paymentIntent.status) {
// 4a. Client-side confirmation

View File

@ -284,7 +284,8 @@ extension PaymentSheet: PaymentSheetViewControllerDelegate {
authenticationContext: self.bottomSheetViewController,
intent: paymentSheetViewController.intent,
paymentOption: paymentOption,
paymentHandler: self.paymentHandler)
paymentHandler: self.paymentHandler,
isFlowController: false)
{ result in
if case let .failed(error) = result {
self.mostRecentError = error
@ -370,7 +371,8 @@ extension PaymentSheet: PayWithLinkViewControllerDelegate {
authenticationContext: self.bottomSheetViewController,
intent: intent,
paymentOption: paymentOption,
paymentHandler: self.paymentHandler)
paymentHandler: self.paymentHandler,
isFlowController: false)
{ result in
if case let .failed(error) = result {
self.mostRecentError = error

View File

@ -9,7 +9,9 @@ import Foundation
import StripePayments
struct PaymentSheetDeferredValidator {
static func validate(paymentIntent: STPPaymentIntent, intentConfiguration: PaymentSheet.IntentConfiguration) throws {
static func validate(paymentIntent: STPPaymentIntent,
intentConfiguration: PaymentSheet.IntentConfiguration,
isFlowController: Bool) throws {
guard case let .payment(amount, currency, setupFutureUsage, captureMethod) = intentConfiguration.mode else {
throw PaymentSheetError.unknown(debugDescription: "You returned a PaymentIntent client secret but used a PaymentSheet.IntentConfiguration in setup mode.")
}
@ -25,9 +27,18 @@ struct PaymentSheetDeferredValidator {
guard paymentIntent.captureMethod == captureMethod else {
throw PaymentSheetError.unknown(debugDescription: "Your PaymentIntent captureMethod (\(paymentIntent.captureMethod)) does not match the PaymentSheet.IntentConfiguration amount (\(captureMethod)).")
}
/*
Manual confirmation is only available using FlowController because merchants own the final step of confirmation.
Showing a successful payment in the complete flow may be misleading when merchants still need to do a final confirmation which could fail e.g., bad network
*/
if !isFlowController && paymentIntent.confirmationMethod == .manual {
throw PaymentSheetError.unknown(debugDescription: "Your PaymentIntent confirmationMethod (\(paymentIntent.confirmationMethod)) can only be used with PaymentSheet.FlowController.")
}
}
static func validate(setupIntent: STPSetupIntent, intentConfiguration: PaymentSheet.IntentConfiguration) throws {
static func validate(setupIntent: STPSetupIntent,
intentConfiguration: PaymentSheet.IntentConfiguration) throws {
guard case let .setup(_, setupFutureUsage) = intentConfiguration.mode else {
throw PaymentSheetError.unknown(debugDescription: "You returned a SetupIntent client secret but used a PaymentSheet.IntentConfiguration in payment mode.")
}

View File

@ -310,7 +310,8 @@ extension PaymentSheet {
authenticationContext: authenticationContext,
intent: intent,
paymentOption: paymentOption,
paymentHandler: paymentHandler
paymentHandler: paymentHandler,
isFlowController: true
) { [intent, configuration] result in
STPAnalyticsClient.sharedClient.logPaymentSheetPayment(
isCustom: true,