Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1042,11 +1042,6 @@ extension PlaygroundController {

/// Builds the common request body parameters used for both loading backend and creating intents
private func buildRequestBody(shouldCreateCustomerKey: Bool = true) -> [String: Any] {
// TODO(porter) Expand to PI+SFU and later, we only support payment and setup mode with CheckoutSession for now
if case .checkoutSession = settings.integrationType, case .paymentWithSetup = settings.mode {
assertionFailure("payment with setup mode is not currently supported with CheckoutSession integration type")
}

var body = [
"customer": customerIdOrType,
"currency": settings.currency.rawValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class STPCheckoutSession: NSObject {
/// Parsed from `customer_managed_saved_payment_methods_offer_save` in the init response.
let savedPaymentMethodsOfferSave: STPCheckoutSessionSavedPaymentMethodsOfferSave?

/// Top-level setup_future_usage for payment-mode checkout sessions.
let setupFutureUsage: 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 @@ -253,6 +256,7 @@ class STPCheckoutSession: NSObject {
selectedShippingOptionId: String?,
taxAmounts: [STPCheckoutSessionTaxAmount],
savedPaymentMethodsOfferSave: STPCheckoutSessionSavedPaymentMethodsOfferSave?,
setupFutureUsage: String?,
requiresBillingAddress: Bool,
allowedShippingCountries: [String]?,
localizedPricesMetas: [STPCheckoutSessionLocalizedPriceMeta],
Expand Down Expand Up @@ -287,6 +291,7 @@ class STPCheckoutSession: NSObject {
self.selectedShippingOptionId = selectedShippingOptionId
self.taxAmounts = taxAmounts
self.savedPaymentMethodsOfferSave = savedPaymentMethodsOfferSave
self.setupFutureUsage = setupFutureUsage
self.requiresBillingAddress = requiresBillingAddress
self.allowedShippingCountries = allowedShippingCountries
self.localizedPricesMetas = localizedPricesMetas
Expand Down Expand Up @@ -361,6 +366,7 @@ extension STPCheckoutSession: STPAPIResponseDecodable {
let savedPaymentMethodsOfferSave = STPCheckoutSessionSavedPaymentMethodsOfferSave.decodedObject(
from: dict["customer_managed_saved_payment_methods_offer_save"] as? [AnyHashable: Any]
)
let setupFutureUsage = dict["setup_future_usage"] as? String

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

extension STPCheckoutSession {
func merchantWillSavePaymentMethod(_ paymentMethodType: STPPaymentMethodType) -> Bool {
func setupFutureUsage(for paymentMethodType: STPPaymentMethodType) -> String? {
_ = paymentMethodType
return setupFutureUsage
}
Comment on lines +468 to +471
Copy link
Copy Markdown
Collaborator Author

@gbirch-stripe gbirch-stripe Apr 2, 2026

Choose a reason for hiding this comment

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

PMO SFU logic will go here


func merchantWillSavePaymentMethod(_ paymentMethodType: STPPaymentMethodType) -> Bool {
guard customerId != nil else {
return false
}
Expand All @@ -468,7 +479,10 @@ extension STPCheckoutSession {
case .setup:
return true
case .payment:
return false
guard let setupFutureUsage = setupFutureUsage(for: paymentMethodType) else {
return false
}
return setupFutureUsage != "none"
case .subscription, .unknown:
stpAssertionFailure("Unknown and subscription modes are not currently supported with checkout sessions")
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,17 @@ enum Intent {
return setupFutureUsage?.rawValue
}
return nil
case .setupIntent, .checkoutSession:
// TODO(porter) Figure out SFU string during confirmation work
case .checkoutSession(let checkoutSession):
switch checkoutSession.mode {
case .payment:
return checkoutSession.setupFutureUsage
case .setup:
return nil
case .subscription, .unknown:
stpAssertionFailure("subscription and unknown not implemented")
return nil
}
case .setupIntent:
return nil
}
}
Expand All @@ -157,8 +166,10 @@ enum Intent {
return !setupFutureUsageValues.isEmpty
}
return nil
case .setupIntent, .checkoutSession:
// TODO(porter) Figure out PMO+SFU during confirmation work
case .checkoutSession:
// TODO(gbirch): implement during PMO SFU work
return nil
case .setupIntent:
return nil
}
}
Expand All @@ -183,11 +194,16 @@ enum Intent {
}
case .checkoutSession(let checkoutSession):
switch checkoutSession.mode {
case .payment, .subscription, .unknown:
// TODO(porter) Figure out SFU during confirmation work
return false
case .payment:
guard let setupFutureUsage = checkoutSession.setupFutureUsage else {
return false
}
return setupFutureUsage != "none"
case .setup:
return true
case .subscription, .unknown:
stpAssertionFailure("subscription and unknown not implemented")
return false
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import StripePaymentsObjcTestUtils
import XCTest

class STPCheckoutSessionTest: XCTestCase {
private func makeCheckoutSession(_ overrides: [String: Any]) -> STPCheckoutSession {
var json: [String: Any] = [
"session_id": "cs_test",
"object": "checkout.session",
"livemode": false,
"mode": "payment",
"payment_status": "unpaid",
"payment_method_types": ["card"],
"customer": ["id": "cus_123"],
]
overrides.forEach { json[$0.key] = $0.value }
return STPCheckoutSession.decodedObject(fromAPIResponse: json)!
}

// MARK: - STPAPIResponseDecodable Tests

Expand Down Expand Up @@ -82,6 +95,7 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertNotNil(session.savedPaymentMethodsOfferSave)
XCTAssertTrue(session.savedPaymentMethodsOfferSave!.enabled)
XCTAssertEqual(session.savedPaymentMethodsOfferSave!.status, .notAccepted)
XCTAssertNil(session.setupFutureUsage)

XCTAssertEqual(
session.paymentMethodTypes,
Expand Down Expand Up @@ -190,6 +204,7 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertNil(session?.url)
XCTAssertNil(session?.returnUrl)
XCTAssertNil(session?.savedPaymentMethodsOfferSave)
XCTAssertNil(session?.setupFutureUsage)
}

func testDecodedObjectWithSetupMode() {
Expand All @@ -214,6 +229,14 @@ class STPCheckoutSessionTest: XCTestCase {
XCTAssertNil(session?.paymentIntentId)
}

func testDecodedObjectParsesTopLevelSetupFutureUsage() {
let session = makeCheckoutSession([
"setup_future_usage": "off_session",
])

XCTAssertEqual(session.setupFutureUsage, "off_session")
}

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

func testMerchantWillSavePaymentMethod_paymentModeWithTopLevelSetupFutureUsage() {
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.

Is it worth adding a test for none setup_future_usage value?

let session = STPCheckoutSession.decodedObject(fromAPIResponse: [
"session_id": "cs_test_payment_sfu",
"object": "checkout.session",
"livemode": false,
"mode": "payment",
"payment_status": "unpaid",
"payment_method_types": ["card"],
"customer": ["id": "cus_123"],
"setup_future_usage": "off_session",
])!

XCTAssertTrue(session.merchantWillSavePaymentMethod(.card))
}

func testMerchantWillSavePaymentMethod_paymentModeWithoutCustomer() {
let session = STPCheckoutSession.decodedObject(fromAPIResponse: [
"session_id": "cs_test_payment_no_customer",
Expand Down Expand Up @@ -379,4 +417,22 @@ class STPCheckoutSessionTest: XCTestCase {

XCTAssertFalse(session.merchantWillSavePaymentMethod(.card))
}

func testCheckoutSessionIntent_setupFutureUsageString() {
let session = makeCheckoutSession([
"setup_future_usage": "off_session",
])

XCTAssertEqual(Intent.checkoutSession(session).setupFutureUsageString, "off_session")
}

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

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

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

waitForExpectations(timeout: 10)
}

func testCheckoutSessionConfirmWithPaymentModeSetupFutureUsageDeselectedUsesLimitedAllowRedisplay() {
var checkoutSessionJSON = MockJson.checkoutSession
checkoutSessionJSON["setup_future_usage"] = "off_session"
let checkoutSession = STPCheckoutSession.decodedObject(fromAPIResponse: checkoutSessionJSON)!
let elementsSession = STPElementsSession._testValue(paymentMethodTypes: ["card"])
var confirmParams = MockParams.intentConfirmParams
confirmParams.saveForFutureUseCheckboxState = .deselected

stubCreatePaymentMethodExpecting(allowRedisplay: "limited")
stubCheckoutSessionConfirm(
sessionId: checkoutSession.stripeId,
savePaymentMethod: false
)

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)
}

func testCheckoutSessionConfirmWithPaymentModeSetupFutureUsageAndOfferSaveDisabledOmitsSaveAndUsesLimitedAllowRedisplay() {
var checkoutSessionJSON = MockJson.checkoutSession
checkoutSessionJSON["setup_future_usage"] = "off_session"
checkoutSessionJSON["customer_managed_saved_payment_methods_offer_save"] = [
"enabled": false,
"status": "not_accepted",
]
let checkoutSession = STPCheckoutSession.decodedObject(fromAPIResponse: checkoutSessionJSON)!
let elementsSession = STPElementsSession._testValue(paymentMethodTypes: ["card"])
var confirmParams = MockParams.intentConfirmParams
confirmParams.saveForFutureUseCheckboxState = .hidden

stubCreatePaymentMethodExpecting(allowRedisplay: "limited")
stubCheckoutSessionConfirm(
sessionId: checkoutSession.stripeId,
savePaymentMethod: nil
)

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 @@ -2821,6 +2821,57 @@ class PaymentSheetFormFactoryTest: XCTestCase {
XCTAssertTrue(paypalForm_setup_paymentOption.didDisplayMandate)
}

func testCheckoutSessionSetupFutureUsage_appliesMandateBehavior() {
func makeCheckoutSessionPayPalForm(
setupFutureUsage: String? = nil,
paymentMethodOptions: [String: Any]? = nil,
previousCustomerInput: IntentConfirmParams? = nil
) -> PaymentMethodElement {
var json: [String: Any] = [
"session_id": "cs_test_paypal",
"object": "checkout.session",
"livemode": false,
"mode": "payment",
"payment_status": "unpaid",
"payment_method_types": ["paypal"],
"customer": ["id": "cus_123"],
]
if let setupFutureUsage {
json["setup_future_usage"] = setupFutureUsage
}
if let paymentMethodOptions {
json["payment_method_options"] = paymentMethodOptions
}
let checkoutSession = STPCheckoutSession.decodedObject(fromAPIResponse: json)!
return PaymentSheetFormFactory(
intent: .checkoutSession(checkoutSession),
elementsSession: ._testValue(paymentMethodTypes: ["paypal"]),
configuration: .paymentElement(PaymentSheet.Configuration._testValue_MostPermissive()),
paymentMethod: .stripe(.payPal),
previousCustomerInput: previousCustomerInput
).make()
}

let paymentForm = makeCheckoutSessionPayPalForm()
guard let paymentOption = paymentForm.updateParams(params: IntentConfirmParams(type: .stripe(.payPal))) else {
XCTFail("payment option should be non-nil")
return
}
XCTAssertFalse(paymentOption.didDisplayMandate)

var setupFutureUsageForm = makeCheckoutSessionPayPalForm(
setupFutureUsage: "off_session",
previousCustomerInput: paymentOption
)
XCTAssertNil(setupFutureUsageForm.updateParams(params: IntentConfirmParams(type: .stripe(.payPal))))
sendEventToSubviews(.viewDidAppear, from: setupFutureUsageForm.view)
guard let setupFutureUsageOption = setupFutureUsageForm.updateParams(params: IntentConfirmParams(type: .stripe(.payPal))) else {
XCTFail("payment option should be non-nil")
return
}
XCTAssertTrue(setupFutureUsageOption.didDisplayMandate)
}

// MARK: - Helpers

func addressSpecProvider(countries: [String]) -> AddressSpecProvider {
Expand Down
Loading