diff --git a/Stripe/StripeiOSTests/PaymentSheetDeferredValidatorTests.swift b/Stripe/StripeiOSTests/PaymentSheetDeferredValidatorTests.swift index 1bc616477f..75a89d80f4 100644 --- a/Stripe/StripeiOSTests/PaymentSheetDeferredValidatorTests.swift +++ b/Stripe/StripeiOSTests/PaymentSheetDeferredValidatorTests.swift @@ -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) diff --git a/Stripe/StripeiOSTests/PaymentSheetPaymentMethodTypeTest.swift b/Stripe/StripeiOSTests/PaymentSheetPaymentMethodTypeTest.swift index faa62548be..744a7f7f33 100644 --- a/Stripe/StripeiOSTests/PaymentSheetPaymentMethodTypeTest.swift +++ b/Stripe/StripeiOSTests/PaymentSheetPaymentMethodTypeTest.swift @@ -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) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkPaymentController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkPaymentController.swift index ab0744dcd0..f19540f00b 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkPaymentController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/LinkPaymentController.swift @@ -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: diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PayWithLinkController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PayWithLinkController.swift index 7809f3cfc9..54a316ad02 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PayWithLinkController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Link/PayWithLinkController.swift @@ -80,6 +80,7 @@ extension PayWithLinkController: PayWithLinkViewControllerDelegate { intent: intent, paymentOption: paymentOption, paymentHandler: paymentHandler, + isFlowController: false, completion: completion ) } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift index 78ad971801..0ec493caaa 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift @@ -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 ) } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift index 4cd245137e..c506caaeee 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+DeferredAPI.swift @@ -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 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet.swift index a049bcd677..560f5f33a9 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet.swift @@ -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 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift index 087748a258..b3e78dac5d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetDeferredValidator.swift @@ -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.") } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift index 7586f461a1..876df49c41 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift @@ -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,