Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ MINOR
## X.Y.Z - changes pending release
### PaymentSheet
* [Added] StripePaymentSheet now depends on `StripeFinancialConnectionsLite` to support lightweight bank payment flows. If you use StripePaymentSheet with Carthage or by manually embedding the .xcframeworks, [you must also embed StripeFinancialConnectionsLite.xcframework](https://github.com/stripe/stripe-ios/blob/master/MIGRATING.md#migrating-from-versions--2670) in your app. No action is required for CocoaPods or Swift Package Manager users.
* [Added] Added private preview support for setting up Alipay payment methods for future off-session use with PaymentIntents and SetupIntents.

## 26.6.0 2026-08-10
### PaymentSheet
Expand Down
12 changes: 12 additions & 0 deletions Stripe/StripeiOSTests/STPConfirmPaymentMethodOptionsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ class STPConfirmPaymentMethodOptionsTest: XCTestCase {

XCTAssertEqual(propertyToFieldMap, expected)
}

func testAlipayOptionsFormEncoding() {
let propertyToFieldMap = STPConfirmAlipayOptions.propertyNamesToFormFieldNamesMapping()
let expected = [
"currency": "currency",
"appBundleID": "app_bundle_id",
"appVersionKey": "app_version_key",
"setupFutureUsageRawString": "setup_future_usage",
]

XCTAssertEqual(propertyToFieldMap, expected)
}
}
21 changes: 21 additions & 0 deletions Stripe/StripeiOSTests/STPPaymentIntentConfirmParamsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ class STPPaymentIntentConfirmParamsTest: XCTestCase {
}
}

func testAlipayFutureUsageFormEncoding() {
// Given
let params = STPPaymentIntentConfirmParams(
clientSecret: "pi_123_secret_456",
paymentMethodType: .alipay
)
let alipayOptions = STPConfirmAlipayOptions()
alipayOptions.setupFutureUsage = .offSession
let paymentMethodOptions = STPConfirmPaymentMethodOptions()
paymentMethodOptions.alipayOptions = alipayOptions
params.paymentMethodOptions = paymentMethodOptions

// When
let encoded = STPFormEncoder.dictionary(forObject: params)

// Then
let encodedPaymentMethodOptions = encoded["payment_method_options"] as? [String: Any]
let encodedAlipayOptions = encodedPaymentMethodOptions?["alipay"] as? [String: Any]
XCTAssertEqual(encodedAlipayOptions?["setup_future_usage"] as? String, "off_session")
}

// #pragma clang diagnostic pop

// MARK: STPFormEncodable Tests
Expand Down
10 changes: 10 additions & 0 deletions Stripe/StripeiOSTests/STPPaymentMethodOptionsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ import StripePaymentsTestUtils

class STPPaymentMethodOptionsTest: STPNetworkStubbingTestCase {

func testAlipayCurrency() {
// Given
let options = STPPaymentMethodOptions.decodedObject(
fromAPIResponse: ["alipay": ["currency": "usd"]]
)

// Then
XCTAssertEqual(options?.currency(for: .alipay), "usd")
}

func testUSBankAccountOptions_PaymentIntent() {
let client = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)

Expand Down
23 changes: 22 additions & 1 deletion Stripe/StripeiOSTests/STPSetupIntentConfirmParamsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class STPSetupIntentConfirmParamsTest: XCTestCase {
XCTAssertNotNil(params.additionalAPIParameters)
XCTAssertEqual(params.additionalAPIParameters.count, 0)
XCTAssertNil(params.paymentMethodID)
XCTAssertNil(params.paymentMethodOptions)
XCTAssertNil(params.returnURL)
XCTAssertNil(params.setAsDefaultPM)
XCTAssertNil(params.useStripeSDK)
Expand All @@ -48,7 +49,7 @@ class STPSetupIntentConfirmParamsTest: XCTestCase {
// card type should have no default mandateData
XCTAssertNil(params.mandateData)

for type in ["sepa_debit", "au_becs_debit", "bacs_debit", "bancontact", "ideal", "eps", "link", "us_bank_account", "cashapp", "paypal", "revolut_pay", "klarna"] {
for type in ["sepa_debit", "au_becs_debit", "bacs_debit", "bancontact", "ideal", "eps", "link", "us_bank_account", "cashapp", "paypal", "revolut_pay", "klarna", "alipay"] {
params.mandateData = nil
params.paymentMethodParams?.rawTypeString = type
// Mandate-required type should have mandateData
Expand Down Expand Up @@ -95,6 +96,7 @@ class STPSetupIntentConfirmParamsTest: XCTestCase {
let params = STPSetupIntentConfirmParams(clientSecret: "test_client_secret")
params.paymentMethodParams = STPPaymentMethodParams()
params.paymentMethodID = "test_payment_method_id"
params.paymentMethodOptions = STPConfirmPaymentMethodOptions()
params.returnURL = "fake://testing_only"
params.setAsDefaultPM = true
params.useStripeSDK = true
Expand All @@ -112,6 +114,7 @@ class STPSetupIntentConfirmParamsTest: XCTestCase {
let paramsCopy = params.copy() as! STPSetupIntentConfirmParams
XCTAssertEqual(params.clientSecret, paramsCopy.clientSecret)
XCTAssertEqual(params.paymentMethodID, paramsCopy.paymentMethodID)
XCTAssertEqual(params.paymentMethodOptions, paramsCopy.paymentMethodOptions)

// assert equal, not equal objects, because this is a shallow copy
XCTAssertEqual(params.paymentMethodParams, paramsCopy.paymentMethodParams)
Expand Down Expand Up @@ -148,6 +151,24 @@ class STPSetupIntentConfirmParamsTest: XCTestCase {
XCTAssertEqual(mapping[NSStringFromSelector(#selector(getter: STPSetupIntentConfirmParams.confirmationToken))], "confirmation_token")
}

func testFormEncodingIncludesAlipayOptions() {
// Given
let params = STPSetupIntentConfirmParams(clientSecret: "seti_123_secret_456")
let alipayOptions = STPConfirmAlipayOptions()
alipayOptions.currency = "usd"
let paymentMethodOptions = STPConfirmPaymentMethodOptions()
paymentMethodOptions.alipayOptions = alipayOptions
params.paymentMethodOptions = paymentMethodOptions

// When
let encoded = STPFormEncoder.dictionary(forObject: params)

// Then
let encodedPaymentMethodOptions = encoded["payment_method_options"] as? [String: Any]
let alipay = encodedPaymentMethodOptions?["alipay"] as? [String: Any]
XCTAssertEqual(alipay?["currency"] as? String, "usd")
}

func testClientSecretValidation() {
XCTAssertFalse(
STPSetupIntentConfirmParams.isClientSecretValid("seti_12345"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@
/* Mandate text with link to terms when saving a bank account payment method to a merchant (merchant name replaces %@). */
"By saving your bank account for %@ you agree to authorize payments pursuant to <terms>these terms</terms>." = "By saving your bank account for %@ you agree to authorize payments pursuant to <terms>these terms</terms>.";

/* Alipay mandate text */
"By saving your payment information, you allow %@ to charge you for future payments in accordance with their terms." = "By saving your payment information, you allow %@ to charge you for future payments in accordance with their terms.";

/* Text providing link to terms for bank payments when seller information is provided */
"By submitting your order to %@ and %@, you agree to save your information with %@ for future purchases, and agree to authorize payments pursuant to <terms>these terms</terms>." = "By submitting your order to %1$@ and %2$@, you agree to save your information with %3$@ for future purchases, and agree to authorize payments pursuant to <terms>these terms</terms>.";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ extension String.Localized {
)
}

static var alipay_mandate_text: String {
STPLocalizedString(
"By saving your payment information, you allow %@ to charge you for future payments in accordance with their terms.",

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.

"Alipay mandate text"
)
}

static var revolut_pay_mandate_text: String {
STPLocalizedString(
"By continuing to Revolut Pay, you allow %@ to charge your Revolut Pay account for future payments in accordance with their terms.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ extension PaymentSheet {
switch paymentMethod {
case .card:
return []
case .payPal, .cashApp, .revolutPay, .amazonPay, .klarna, .satispay, .twint:
case .alipay, .payPal, .cashApp, .revolutPay, .amazonPay, .klarna, .satispay, .twint:
return [.returnURL]
case .USBankAccount, .boleto:
return [.userSupportsDelayedPaymentMethods]
Expand All @@ -273,7 +273,7 @@ extension PaymentSheet {
return [.returnURL, .userSupportsDelayedPaymentMethods]
case .cardPresent, .blik, .weChatPay, .grabPay, .FPX, .przelewy24, .EPS,
.netBanking, .OXXO, .afterpayClearpay, .link, .affirm, .paynow, .zip, .alma,
.mobilePay, .vipps, .unknown, .alipay, .konbini, .promptPay, .swish, .multibanco,
.mobilePay, .vipps, .unknown, .konbini, .promptPay, .swish, .multibanco,
.sunbit, .billie, .crypto, .payPay, .wero, .payByBank, .mbWay, .bizum:
return [.unsupportedForSetup]
@unknown default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -955,16 +955,19 @@ extension PaymentSheet {
configuration: PaymentElementConfiguration
) -> STPSetupIntentConfirmParams {
let params: STPSetupIntentConfirmParams
let suppliedPaymentMethodOptions: STPConfirmPaymentMethodOptions?
switch confirmPaymentMethodType {
case let .saved(paymentMethod, _, clientAttributionMetadata, radarOptions):
case let .saved(paymentMethod, paymentMethodOptions, clientAttributionMetadata, radarOptions):
suppliedPaymentMethodOptions = paymentMethodOptions
params = STPSetupIntentConfirmParams(
clientSecret: setupIntent.clientSecret,
paymentMethodType: paymentMethod.type
)
params.paymentMethodID = paymentMethod.stripeId
params.radarOptions = radarOptions
params.clientAttributionMetadata = clientAttributionMetadata
case let .new(paymentMethodParams, _, paymentMethod, _, shouldSetAsDefaultPM):
case let .new(paymentMethodParams, paymentMethodOptions, paymentMethod, _, shouldSetAsDefaultPM):
suppliedPaymentMethodOptions = paymentMethodOptions
if let paymentMethod {
params = STPSetupIntentConfirmParams(
clientSecret: setupIntent.clientSecret,
Expand All @@ -990,6 +993,15 @@ extension PaymentSheet {
if configuration.apiClient.publishableKeyIsUserKey {
params.additionalAPIParameters["payment_method_options"] = ["card": ["moto": true]]
}
if params.paymentMethodType == .alipay,
let currency = setupIntent.paymentMethodOptions?.currency(for: .alipay)
{
let paymentMethodOptions = suppliedPaymentMethodOptions ?? STPConfirmPaymentMethodOptions()
let alipayOptions = paymentMethodOptions.alipayOptions ?? STPConfirmAlipayOptions()
alipayOptions.currency = currency
paymentMethodOptions.alipayOptions = alipayOptions
params.paymentMethodOptions = paymentMethodOptions
}
params.returnURL = configuration.returnURL
return params
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,11 @@ extension PaymentSheet {

/// Payment method types that require mandate data for PaymentIntents when `setup_future_usage` is set
static var requiresMandateDataForPaymentIntent: Set<STPPaymentMethodType> {
[.payPal, .cashApp, .revolutPay, .amazonPay, .klarna, .satispay, .twint]
[.alipay, .payPal, .cashApp, .revolutPay, .amazonPay, .klarna, .satispay, .twint]
}

/// Payment method types that require mandate data for SetupIntents
static var requiresMandateDataForSetupIntent: Set<STPPaymentMethodType> {
[.payPal, .revolutPay, .satispay, .twint]
[.alipay, .payPal, .revolutPay, .satispay, .twint]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ extension PaymentSheetFormFactory {
return makeMandate(mandateText: mandateText)
}

func makeAlipayMandate() -> SimpleMandateElement {
let mandateText = String(format: String.Localized.alipay_mandate_text, configuration.merchantDisplayName)
return makeMandate(mandateText: mandateText)
}

func makeRevolutPayMandate() -> SimpleMandateElement {
let mandateText = String(format: String.Localized.revolut_pay_mandate_text, configuration.merchantDisplayName)
return makeMandate(mandateText: mandateText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,13 @@ class PaymentSheetFormFactory {
return makeWero()
case .SEPADebit:
return makeSepaDebit()
case .grabPay, .alipay, .paynow, .payPay, .mobilePay, .vipps, .zip, .crypto,
case .grabPay, .paynow, .payPay, .mobilePay, .vipps, .zip, .crypto,
.billie, .sunbit, .alma, .payByBank:
return makeContactInformationAndBillingAddressForm()
case .alipay:
return makeContactInformationAndBillingAddressForm(
additionalElements: makeSetupMandateElements(for: paymentMethod)
)
case .promptPay, .multibanco:
return makeContactInformationAndBillingAddressForm(
emailRequired: true,
Expand Down Expand Up @@ -321,6 +325,8 @@ class PaymentSheetFormFactory {
private func makeSetupMandateElements(for paymentMethod: STPPaymentMethodType) -> [Element] {
guard isSettingUp else { return [] }
switch paymentMethod {
case .alipay:
return [makeAlipayMandate()]
case .cashApp:
return [makeCashAppMandate()]
case .payPal:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ enum ExpectedFormHierarchy {

enum Alipay {
static var paymentIntent: FormHierarchyNode { emptyForm }
static var settingUp: FormHierarchyNode {
FormHierarchyNode(type: "FormElement", children: [
FormHierarchyNode(type: "SimpleMandateElement", properties: ["text": "By saving your payment information, you allow Stri..."])
])
}
}

// MARK: - PayNow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,64 @@ class PaymentSheetAPITest: STPNetworkStubbingTestCase {
}
}

func testMakeIntentParams_alipayFutureUsage() {
// Given
let paymentMethodParams = STPPaymentMethodParams(
alipay: STPPaymentMethodAlipayParams(),
billingDetails: nil,
metadata: nil
)
let confirmType = PaymentSheet.ConfirmPaymentMethodType.new(
params: paymentMethodParams,
paymentOptions: STPConfirmPaymentMethodOptions(),
saveForFutureUseCheckboxState: .hidden
)
let configuration = PaymentSheet.Configuration._testValue_MostPermissive()

// When
let regularPaymentIntentParams = PaymentSheet.makePaymentIntentParams(
confirmPaymentMethodType: confirmType,
paymentIntent: STPFixtures.makePaymentIntent(),
configuration: configuration
)
let futureUsagePaymentIntentParams = PaymentSheet.makePaymentIntentParams(
confirmPaymentMethodType: confirmType,
paymentIntent: STPFixtures.makePaymentIntent(setupFutureUsage: .offSession),
configuration: configuration
)
let paymentMethodOptionsFutureUsagePaymentIntentParams = PaymentSheet.makePaymentIntentParams(
confirmPaymentMethodType: confirmType,
paymentIntent: STPFixtures.makePaymentIntent(
paymentMethodOptions: STPPaymentMethodOptions(
usBankAccount: nil,
card: nil,
allResponseFields: ["alipay": ["setup_future_usage": "off_session"]]
)
),
configuration: configuration
)
let setupIntent = STPFixtures.makeSetupIntent(
paymentMethodTypes: [.alipay],
paymentMethodOptions: STPPaymentMethodOptions(
usBankAccount: nil,
card: nil,
allResponseFields: ["alipay": ["currency": "usd"]]
)
)
let setupIntentParams = PaymentSheet.makeSetupIntentParams(
confirmPaymentMethodType: confirmType,
setupIntent: setupIntent,
configuration: configuration
)

// Then
XCTAssertNil(regularPaymentIntentParams.mandateData)
XCTAssertNotNil(futureUsagePaymentIntentParams.mandateData)
XCTAssertNotNil(paymentMethodOptionsFutureUsagePaymentIntentParams.mandateData)
XCTAssertNotNil(setupIntentParams.mandateData)
XCTAssertEqual(setupIntentParams.paymentMethodOptions?.alipayOptions?.currency, "usd")
}

func testMakeDeferredPaymentUserAgent() {
let intentConfig_with_nil_payment_method_types = PaymentSheet.IntentConfiguration(mode: .payment(amount: 1099, currency: "USD"), confirmHandler: { _, _ in return "" })
XCTAssertEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,48 @@ final class PaymentSheet_ConfirmationTokenTests: STPNetworkStubbingTestCase {

// MARK: - Mandate Data Tests

func testCreateConfirmationTokenParams_autoGeneratedMandate_alipay() {
// Given
let paymentMethodParams = STPPaymentMethodParams(
alipay: STPPaymentMethodAlipayParams(),
billingDetails: nil,
metadata: nil
)
let confirmType = PaymentSheet.ConfirmPaymentMethodType.new(
params: paymentMethodParams,
paymentOptions: STPConfirmPaymentMethodOptions(),
saveForFutureUseCheckboxState: .hidden
)
let paymentIntentConfig = PaymentSheet.IntentConfiguration(
mode: .payment(
amount: 100,
currency: "USD",
paymentMethodOptions: .init(setupFutureUsageValues: [.alipay: .offSession])
)
) { _, _ in return "pi_test_123_secret_abc" }
let setupIntentConfig = PaymentSheet.IntentConfiguration(
mode: .setup(currency: "USD", setupFutureUsage: .offSession)
) { _, _ in return "seti_test_123_secret_abc" }

// When
let paymentIntentParams = PaymentSheet.createConfirmationTokenParams(
confirmType: confirmType,
configuration: configuration,
intentConfig: paymentIntentConfig,
elementsSession: .emptyElementsSession
)
let setupIntentParams = PaymentSheet.createConfirmationTokenParams(
confirmType: confirmType,
configuration: configuration,
intentConfig: setupIntentConfig,
elementsSession: .emptyElementsSession
)

// Then
XCTAssertNotNil(paymentIntentParams.mandateData)
XCTAssertNotNil(setupIntentParams.mandateData)
}

func testCreateConfirmationTokenParams_autoGeneratedMandate_payPal() {
let payPalPaymentMethod = STPPaymentMethod.decodedObject(fromAPIResponse: [
"id": "pm_test_paypal",
Expand Down
Loading
Loading