Skip to content
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ extension PlaygroundController {
}

// Only set PMO SFU on the Intent if we're Intent-first, never set it for deferred intents.
if settings.integrationType == .normal {
if settings.integrationType == .normal || settings.integrationType == .checkoutSession {
body["payment_method_options_setup_future_usage"] = settings.paymentMethodOptionsSetupFutureUsage.toDictionary()
}
if shouldCreateCustomerKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class STPCheckoutSession: NSObject {
/// Top-level setup_future_usage for payment-mode checkout sessions.
let setupFutureUsage: String?

/// Per-payment-method setup_future_usage overrides for payment-mode checkout sessions.
/// Parsed from `setup_future_usage_for_payment_method_type` in the init response.
let setupFutureUsageForPaymentMethodType: [String: String]

/// Whether billing address collection is required for this session.
/// Derived from `billing_address_collection == "required"` in the API response.
let requiresBillingAddress: Bool
Expand Down Expand Up @@ -257,6 +261,7 @@ class STPCheckoutSession: NSObject {
taxAmounts: [STPCheckoutSessionTaxAmount],
savedPaymentMethodsOfferSave: STPCheckoutSessionSavedPaymentMethodsOfferSave?,
setupFutureUsage: String?,
setupFutureUsageForPaymentMethodType: [String: String],
requiresBillingAddress: Bool,
allowedShippingCountries: [String]?,
localizedPricesMetas: [STPCheckoutSessionLocalizedPriceMeta],
Expand Down Expand Up @@ -292,6 +297,7 @@ class STPCheckoutSession: NSObject {
self.taxAmounts = taxAmounts
self.savedPaymentMethodsOfferSave = savedPaymentMethodsOfferSave
self.setupFutureUsage = setupFutureUsage
self.setupFutureUsageForPaymentMethodType = setupFutureUsageForPaymentMethodType
self.requiresBillingAddress = requiresBillingAddress
self.allowedShippingCountries = allowedShippingCountries
self.localizedPricesMetas = localizedPricesMetas
Expand Down Expand Up @@ -367,6 +373,7 @@ extension STPCheckoutSession: STPAPIResponseDecodable {
from: dict["customer_managed_saved_payment_methods_offer_save"] as? [AnyHashable: Any]
)
let setupFutureUsage = dict["setup_future_usage"] as? String
let setupFutureUsageForPaymentMethodType = dict["setup_future_usage_for_payment_method_type"] as? [String: String] ?? [:]

// Parse address collection settings
let requiresBillingAddress = (dict["billing_address_collection"] as? String) == "required"
Expand Down Expand Up @@ -450,6 +457,7 @@ extension STPCheckoutSession: STPAPIResponseDecodable {
taxAmounts: taxAmounts,
savedPaymentMethodsOfferSave: savedPaymentMethodsOfferSave,
setupFutureUsage: setupFutureUsage,
setupFutureUsageForPaymentMethodType: setupFutureUsageForPaymentMethodType,
requiresBillingAddress: requiresBillingAddress,
allowedShippingCountries: allowedShippingCountries,
localizedPricesMetas: localizedPricesMetas,
Expand All @@ -465,8 +473,16 @@ extension STPCheckoutSession: STPAPIResponseDecodable {
// MARK: - Parsing Helpers

extension STPCheckoutSession {
var isPaymentMethodOptionsSetupFutureUsageSet: Bool {
return !setupFutureUsageForPaymentMethodType.isEmpty
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit/question: The existing STPPaymentMethodOptions.isSetupFutureUsageSet (line 45-52 of STPPaymentMethodOptions.swift) checks whether any PMO entry contains the key setup_future_usage. Might be a bit safer to align with STPPaymentMethodOptions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting that we use the payment_method_options property on the checkout session as the data source for PMO SFU rather than setup_future_usage_for_payment_method_type or something else? If the former, I have a question about which to use down in the API team questions list, but for now my intuition is to use the setup_future_usage_for_payment_method_type property since it is specific to checkout sessions and what is used by web: https://stripe.sourcegraphcloud.com/deepsearch/c4c1b6ed-ecad-4140-a808-600a15fa1da9#answer-169850

}

func setupFutureUsage(for paymentMethodType: STPPaymentMethodType) -> String? {
_ = paymentMethodType
let perPaymentMethodSetupFutureUsage = setupFutureUsageForPaymentMethodType[paymentMethodType.identifier]
if let perPaymentMethodSetupFutureUsage {
return perPaymentMethodSetupFutureUsage
}

return setupFutureUsage
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,8 @@ enum Intent {
return !setupFutureUsageValues.isEmpty
}
return nil
case .checkoutSession:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to line 133 here, can't select it.

For payment mode, it returns the top-level setupFutureUsage only. The setupFutureUsageString property is used in analytics. Is it intentional that this reports only the top-level value? I think this is consistent with others but just want to double check.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I intentionally made it only report the top-level to align with intent flows

// TODO(gbirch): implement during PMO SFU work
return nil
case .checkoutSession(let checkoutSession):
return checkoutSession.isPaymentMethodOptionsSetupFutureUsageSet
case .setupIntent:
return nil
}
Expand All @@ -195,7 +194,7 @@ enum Intent {
case .checkoutSession(let checkoutSession):
switch checkoutSession.mode {
case .payment:
guard let setupFutureUsage = checkoutSession.setupFutureUsage else {
guard let setupFutureUsage = checkoutSession.setupFutureUsage(for: paymentMethodType) else {
return false
}
return setupFutureUsage != "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertEqual(session.setupFutureUsage, "off_session")
}

func testDecodedObjectParsesPerPaymentMethodSetupFutureUsage() {
let session = makeCheckoutSession([
"payment_method_types": ["card", "us_bank_account"],
"setup_future_usage_for_payment_method_type": [
"card": "off_session",
"us_bank_account": "none",
],
])

XCTAssertEqual(
session.setupFutureUsageForPaymentMethodType as NSDictionary,
[
"card": "off_session",
"us_bank_account": "none",
] as NSDictionary
)
}

func testDecodedObjectParsesCanDetachPaymentMethodTrue() {
let json: [String: Any] = [
"session_id": "cs_test_detach_true",
Expand Down Expand Up @@ -386,6 +404,25 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertFalse(session.merchantWillSavePaymentMethod(.card))
}

func testMerchantWillSavePaymentMethod_paymentModeWithPerPaymentMethodSetupFutureUsage() {
let session = STPCheckoutSession.decodedObject(fromAPIResponse: [
"session_id": "cs_test_payment_per_pm_sfu",
"object": "checkout.session",
"livemode": false,
"mode": "payment",
"payment_status": "unpaid",
"payment_method_types": ["card", "us_bank_account"],
"customer": ["id": "cus_123"],
"setup_future_usage_for_payment_method_type": [
"card": "off_session",
"us_bank_account": "none",
],
])!

XCTAssertTrue(session.merchantWillSavePaymentMethod(.card))
XCTAssertFalse(session.merchantWillSavePaymentMethod(.USBankAccount))
}

func testMerchantWillSavePaymentMethod_paymentModeWithoutCustomer() {
let session = STPCheckoutSession.decodedObject(fromAPIResponse: [
"session_id": "cs_test_payment_no_customer",
Expand Down Expand Up @@ -435,6 +472,17 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertEqual(Intent.checkoutSession(session).setupFutureUsageString, "off_session")
}

func testCheckoutSessionIntent_isPaymentMethodOptionsSetupFutureUsageSet() {
let session = makeCheckoutSession([
"setup_future_usage_for_payment_method_type": [
"paypal": "off_session",
],
"payment_method_types": ["paypal"],
])

XCTAssertEqual(Intent.checkoutSession(session).isPaymentMethodOptionsSetupFutureUsageSet, true)
}

func testCheckoutSessionIntent_isSetupFutureUsageSet_topLevel() {
let session = makeCheckoutSession([
"setup_future_usage": "off_session",
Expand All @@ -454,4 +502,27 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertFalse(Intent.checkoutSession(session).isSetupFutureUsageSet(for: .payPal))
}

func testCheckoutSessionIntent_isSetupFutureUsageSet_perPaymentMethod() {
let session = makeCheckoutSession([
"setup_future_usage_for_payment_method_type": [
"paypal": "off_session",
],
"payment_method_types": ["paypal"],
])

XCTAssertTrue(Intent.checkoutSession(session).isSetupFutureUsageSet(for: .payPal))
}

func testCheckoutSessionIntent_isSetupFutureUsageSet_perPaymentMethodNoneOverridesTopLevel() {
let session = makeCheckoutSession([
"setup_future_usage": "off_session",
"setup_future_usage_for_payment_method_type": [
"paypal": "none",
],
"payment_method_types": ["paypal"],
])

XCTAssertFalse(Intent.checkoutSession(session).isSetupFutureUsageSet(for: .payPal))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,42 @@ final class PaymentSheetAPIMockTest: APIStubbedTestCase {

waitForExpectations(timeout: 10)
}

func testCheckoutSessionConfirmWithNonCardPaymentMethodIncludesSavePaymentMethod() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests in this PR are quite light considering what it's adding. Let's add PaymentSheetLPMConfirmFlowTests coverage: we already test PMO+SFU paymentIntentWithPMOSetupFutureUsagefor PI/deferred PI, but that suite doesn't test CheckoutSession+PMO SFU

var checkoutSessionJSON = MockJson.checkoutSession
checkoutSessionJSON["payment_method_types"] = ["paypal"]
let checkoutSession = STPCheckoutSession.decodedObject(fromAPIResponse: checkoutSessionJSON)!
let elementsSession = STPElementsSession._testValue(paymentMethodTypes: ["paypal"])
let confirmParams = IntentConfirmParams(type: .stripe(.payPal))
confirmParams.saveForFutureUseCheckboxState = .selected

stubCreatePaymentMethodExpecting(allowRedisplay: "always")
stubCheckoutSessionConfirm(
sessionId: checkoutSession.stripeId,
savePaymentMethod: true
)

let configuration = MockParams.configuration(pk: MockParams.publicKey)
let exp = expectation(description: "confirm completed")
let paymentHandler = STPPaymentHandler(apiClient: configuration.apiClient)

PaymentSheet.confirm(
configuration: configuration,
authenticationContext: self,
intent: .checkoutSession(checkoutSession),
elementsSession: elementsSession,
paymentOption: .new(confirmParams: confirmParams),
paymentHandler: paymentHandler,
analyticsHelper: ._testValue(),
completion: { result, _ in
XCTAssertEqual(result, .completed)
exp.fulfill()
}
)

waitForExpectations(timeout: 10)
}

}

extension PaymentSheetAPIMockTest: STPAuthenticationContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,13 +835,67 @@ extension PaymentSheetLPMConfirmFlowTests {

// MARK: - Helper methods
extension PaymentSheetLPMConfirmFlowTests {
struct SetupFutureUsageSupport {
let paymentIntentSetupFutureUsage: Bool
let paymentIntentPaymentMethodOptionsSetupFutureUsage: Bool
let checkoutSessionSetupFutureUsage: Bool
let checkoutSessionPaymentMethodOptionsSetupFutureUsage: Bool

static let fullySupported = SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: true,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: true
)
}

enum IntentKind: CaseIterable {
case paymentIntent
case paymentIntentWithSetupFutureUsage
case paymentIntentWithPMOSetupFutureUsage
case setupIntent
}

static let setupFutureUsageSupportByPaymentMethod: [STPPaymentMethodType: SetupFutureUsageSupport] = [
// Payment+SFU and PMO SFU are not always available on payment methods that support them for intents.
// Verified against `/create_checkout_session` on April 18, 2026 for payment methods
// that already support PaymentIntent top-level SFU and/or PMO SFU in these tests.
.AUBECSDebit: SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: true,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: false
),
.bancontact: SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: false,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: false
),
.klarna: SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: true,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: false
),
.satispay: SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: true,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: false
),
.iDEAL: SetupFutureUsageSupport(
paymentIntentSetupFutureUsage: true,
paymentIntentPaymentMethodOptionsSetupFutureUsage: true,
checkoutSessionSetupFutureUsage: true,
checkoutSessionPaymentMethodOptionsSetupFutureUsage: false
),
]

func setupFutureUsageSupport(for paymentMethod: STPPaymentMethodType) -> SetupFutureUsageSupport {
Self.setupFutureUsageSupportByPaymentMethod[paymentMethod] ?? .fullySupported
}

func _testConfirm(
intentKinds: [IntentKind],
currency: String,
Expand Down Expand Up @@ -1040,6 +1094,7 @@ extension PaymentSheetLPMConfirmFlowTests {

var intents: [(String, Intent)] = []
let paymentMethodTypes = [paymentMethod.identifier].compactMap { $0 }
let setupFutureUsageSupport = setupFutureUsageSupport(for: paymentMethod)
switch intentKind {
case .paymentIntent:
let paymentIntent: STPPaymentIntent = try await {
Expand Down Expand Up @@ -1133,6 +1188,9 @@ extension PaymentSheetLPMConfirmFlowTests {

return intents
case .paymentIntentWithSetupFutureUsage:
guard setupFutureUsageSupport.paymentIntentSetupFutureUsage else {
return []
}
let paymentIntent: STPPaymentIntent = try await {
let clientSecret = try await STPTestingAPIClient.shared.fetchPaymentIntent(
types: paymentMethodTypes,
Expand Down Expand Up @@ -1194,14 +1252,38 @@ extension PaymentSheetLPMConfirmFlowTests {
)
})

return [
var intents: [(String, Intent)] = [
("PaymentIntent", .paymentIntent(paymentIntent)),
("Deferred PaymentIntent w/ setup_future_usage - client side confirmation with payment method flow", makeDeferredIntent(deferredCSC)),
("Deferred PaymentIntent w/ setup_future_usage - server side confirmation with payment method flow", makeDeferredIntent(deferredSSC)),
("Deferred PaymentIntent w/ setup_future_usage - client side confirmation with confirmation token", makeDeferredIntent(deferredCSCWithConfirmationToken)),
("Deferred PaymentIntent w/ setup_future_usage - server side confirmation with confirmation token", makeDeferredIntent(deferredSSCWithConfirmationToken)),
]

// Payment+SFU and PMO SFU are not always available on payment methods that support them for intents.
// We conditionally add testing for them accordingly.
if setupFutureUsageSupport.checkoutSessionSetupFutureUsage {
let checkoutSessionResponse = try await STPTestingAPIClient.shared.fetchCheckoutSessionPaymentMode(
types: paymentMethodTypes,
currency: currency,
amount: amount,
merchantCountry: merchantCountry.rawValue,
customerID: customer,
setupFutureUsage: "off_session"
)
let csApiClient = STPAPIClient(publishableKey: checkoutSessionResponse.publishableKey)
let checkoutSession = try await csApiClient.initCheckoutSession(
checkoutSessionId: checkoutSessionResponse.id,
adaptivePricingAllowed: true
)
intents.append(("CheckoutSession w/ setup_future_usage", .checkoutSession(checkoutSession)))
}

return intents
case .paymentIntentWithPMOSetupFutureUsage:
guard setupFutureUsageSupport.paymentIntentPaymentMethodOptionsSetupFutureUsage else {
return []
}
// This tests the scenario where IntentConfiguration has PMO setup_future_usage.
let paymentIntent: STPPaymentIntent = try await {
// Regular PI: Backend DOES have PMO SFU set
Expand Down Expand Up @@ -1283,13 +1365,33 @@ extension PaymentSheetLPMConfirmFlowTests {
}
)

return [
var intents: [(String, Intent)] = [
("PaymentIntent", .paymentIntent(paymentIntent)),
("Deferred PaymentIntent w/ PMO setup_future_usage - client side confirmation", makeDeferredIntent(deferredCSC)),
("Deferred PaymentIntent w/ PMO setup_future_usage - server side confirmation", makeDeferredIntent(deferredSSC)),
("Deferred PaymentIntent w/ PMO setup_future_usage - client side confirmation with confirmation token", makeDeferredIntent(deferredCSCWithConfirmationToken)),
("Deferred PaymentIntent w/ PMO setup_future_usage - server side confirmation with confirmation token", makeDeferredIntent(deferredSSCWithConfirmationToken)),
]
if setupFutureUsageSupport.checkoutSessionPaymentMethodOptionsSetupFutureUsage {
let checkoutSessionResponse = try await STPTestingAPIClient.shared.fetchCheckoutSessionPaymentMode(
types: paymentMethodTypes,
currency: currency,
amount: amount,
merchantCountry: merchantCountry.rawValue,
customerID: customer,
paymentMethodOptionsSetupFutureUsage: [
paymentMethod.identifier: "off_session",
]
)
let csApiClient = STPAPIClient(publishableKey: checkoutSessionResponse.publishableKey)
let checkoutSession = try await csApiClient.initCheckoutSession(
checkoutSessionId: checkoutSessionResponse.id,
adaptivePricingAllowed: true
)
intents.append(("CheckoutSession w/ PMO setup_future_usage", .checkoutSession(checkoutSession)))
}

return intents
case .setupIntent:
let setupIntent: STPSetupIntent = try await {
let clientSecret = try await STPTestingAPIClient.shared.fetchSetupIntent(types: paymentMethodTypes, merchantCountry: merchantCountry.rawValue, customerID: customer)
Expand Down
Loading
Loading