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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ PATCH

## X.Y.Z - changes pending release

### PaymentSheet
* [Added] Added support for new and upcoming payment methods in native Link, such as UPI, Pix, and Crypto

## 25.13.0 2026-05-04
### PaymentSheet
* [Fixed] Fixed an issue where `paymentMethodOrder` did not apply to custom payment methods due to case-sensitive matching.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* Message shown in a confirmation prompt when removing a saved payment method. */
"%@ will no longer be saved to your wallet." = "%@ will no longer be saved to your wallet.";

/* Text for a button that, when tapped, displays another screen where the customer can add payment method details */
"+ Add" = "+ Add";

Expand Down Expand Up @@ -28,12 +31,6 @@
/* Text on a screen asking the user to approve a payment */
"Approve payment" = "Approve payment";

/* Title of confirmation prompt when removing a saved card. */
"Are you sure you want to remove this card?" = "Are you sure you want to remove this card?";

/* Title of confirmation prompt when removing a linked bank account. */
"Are you sure you want to remove this linked account?" = "Are you sure you want to remove this linked account?";

/* Text for back button */
"Back" = "Back";

Expand Down Expand Up @@ -345,12 +342,22 @@ e.g, 'Pay faster at Example, Inc. and thousands of businesses.' */
/* Label title for Przelewy24 Bank */
"Przelewy24 Bank" = "Przelewy24 Bank";

/* Title of confirmation prompt when removing a saved payment method. */
"Remove %@" = "Remove %@";

/* Title for confirmation alert to remove a bank account */
"Remove bank?" = "Remove bank?";

/* Title for confirmation alert to remove a card */
"Remove card?" = "Remove card?";

/* Title for a button that when tapped removes a linked bank account. */
"Remove linked account" = "Remove linked account";

/* Title for a button that when tapped removes a payment method.
Title for confirmation prompt when removing a saved payment method without a display label. */
"Remove payment method" = "Remove payment method";
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.

I wonder if we should just show Remove in the buttons, as the available space is limited. Not immediately required, but let's follow up with that?


/* A button used for saving a new payment method
Label on a button that when tapped, updates a card brand. */
"Save" = "Save";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,24 +720,16 @@ extension ParsedEnum where E == LinkSettings.FundingSource {
// MARK: UpdatePaymentDetailsParams

struct UpdatePaymentDetailsParams {
enum DetailsType {
enum PaymentMethodMetadata {
case card(
expiryDate: CardExpiryDate? = nil,
billingDetails: STPPaymentMethodBillingDetails? = nil,
preferredNetwork: String? = nil
)
case bankAccount(
billingDetails: STPPaymentMethodBillingDetails
)
}

let isDefault: Bool?
let details: DetailsType?

init(isDefault: Bool? = nil, details: DetailsType? = nil) {
self.isDefault = isDefault
self.details = details
}
var billingDetails: STPPaymentMethodBillingDetails?
var isDefault: Bool?
var metadata: PaymentMethodMetadata?
}

protocol PaymentSheetLinkAccountDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ final class ConsumerPaymentDetails: Decodable {
let billingAddress: BillingAddress?
let billingEmailAddress: String?
let nickname: String?
let display: DisplayMetadata?
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.

If I remember correctly, we talked about this becoming non-nullable? Is that still the plan for later on?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@tillh-stripe Yep! The backend FF is still there so want to make sure that is removed before we remove nullability here.

var isDefault: Bool

init(stripeID: String,
details: Details,
billingAddress: BillingAddress?,
billingEmailAddress: String?,
nickname: String?,
display: DisplayMetadata? = nil,
isDefault: Bool) {
self.stripeID = stripeID
self.details = details
self.billingAddress = billingAddress
self.billingEmailAddress = billingEmailAddress
self.nickname = nickname
self.display = display
self.isDefault = isDefault
}

Expand All @@ -45,6 +48,7 @@ final class ConsumerPaymentDetails: Decodable {
case billingAddress = "billing_address"
case billingEmailAddress = "billing_email_address"
case nickname
case display
case isDefault
}

Expand All @@ -59,6 +63,7 @@ final class ConsumerPaymentDetails: Decodable {
} else {
self.nickname = nil
}
self.display = try? container.decode(DisplayMetadata.self, forKey: .display)
// The payment details are included in the dictionary, so we pass the whole dict to Details
self.details = try decoder.singleValueContainer().decode(Details.self)
self.isDefault = try container.decode(Bool.self, forKey: .isDefault)
Expand Down Expand Up @@ -101,7 +106,7 @@ extension ConsumerPaymentDetails {
}

switch details {
case .card:
case .card, .unparsable:
// If the merchant is filtering, only allow cards with a billing country
if let country = billingAddress?.countryCode {
return allowedCountries.contains(country)
Expand All @@ -111,8 +116,6 @@ extension ConsumerPaymentDetails {
case .bankAccount:
// These are US bank accounts, so only check for US country code
return allowedCountries.contains("US")
case .unparsable:
return false
}
}

Expand All @@ -134,6 +137,19 @@ extension ConsumerPaymentDetails {
case bankAccount = "BANK_ACCOUNT"
}

struct DisplayMetadata: Decodable {
let label: String
let sublabel: String?

let icon: Icon?
struct Icon: Decodable {
let main: URL?
enum CodingKeys: String, CodingKey {
case main = "default"
}
}
}

// swiftlint:disable:next enum_safe_decodable
enum Details: Decodable {
case card(card: Card)
Expand Down Expand Up @@ -361,7 +377,7 @@ extension ConsumerPaymentDetails {
case .bankAccount(let bank):
return bank.displayName(with: nickname)
case .unparsable:
return ""
return display?.label ?? ""
}
}

Expand All @@ -375,7 +391,9 @@ extension ConsumerPaymentDetails {
case .bankAccount(let bankAccount):
return bankAccount.displayName(with: nickname)
case .unparsable:
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.

We should rename unparsable, but that's for a later pull request.

return nil
guard let display else { return nil }
let components = [display.label, display.sublabel].compactMap { $0 }
return components.joined(separator: " ")
}
}

Expand Down Expand Up @@ -409,7 +427,9 @@ extension ConsumerPaymentDetails {
digits
)
case .unparsable:
return ""
guard let display else { return "" }
let components = [display.label, display.sublabel].compactMap { $0 }
return components.joined(separator: " ")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,30 +539,23 @@ extension STPAPIClient {
"request_surface": requestSurface.rawValue,
]

if let details = updateParams.details, case .card(let expiryDate, let billingDetails, let preferredNetwork) = details {
if let expiryDate {
parameters["exp_month"] = expiryDate.month
parameters["exp_year"] = expiryDate.year
}

if let billingDetails = billingDetails {
parameters["billing_address"] = billingDetails.consumersAPIParams
}

if let billingEmailAddress = billingDetails?.email, !billingEmailAddress.isEmpty {
// This email address needs to be lowercase or the API will reject it
parameters["billing_email_address"] = billingEmailAddress.lowercased()
}

if let preferredNetwork {
parameters["preferred_network"] = preferredNetwork
if let metadata = updateParams.metadata {
switch metadata {
case .card(let expiryDate, let preferredNetwork):
if let expiryDate {
parameters["exp_month"] = expiryDate.month
parameters["exp_year"] = expiryDate.year
}
if let preferredNetwork {
parameters["preferred_network"] = preferredNetwork
}
}
}

if let details = updateParams.details, case .bankAccount(let billingDetails) = details {
if let billingDetails = updateParams.billingDetails {
parameters["billing_address"] = billingDetails.consumersAPIParams

if let billingEmailAddress = billingDetails.email {
if let billingEmailAddress = billingDetails.email, !billingEmailAddress.isEmpty {
// This email address needs to be lowercase or the API will reject it
parameters["billing_email_address"] = billingEmailAddress.lowercased()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ class PaymentMethodWithLinkDetails: NSObject, STPAPIResponseDecodable {
}
}

if let linkDetails, linkDetails.type.isUnparsed {
// TODO(jkelle): We'll be able to render these with the `display` metadata
// coming in https://docs.google.com/document/d/1x834BjHYro9-bDoAVaqgHm7LDPDwzpk4z_5BvxYwwtU/
if let linkDetails, linkDetails.type.isUnparsed, linkDetails.display == nil {
// This is a Link payment method with an unknown type and no display metadata. We can't render it.
return nil
}

Expand Down
Loading
Loading