Skip to content

Add Unknown payment method type support in Link wallet - #13140

Merged
jeans-stripe merged 9 commits into
masterfrom
jeans/add-unknown-payment-methods-to-link
Jun 10, 2026
Merged

Add Unknown payment method type support in Link wallet#13140
jeans-stripe merged 9 commits into
masterfrom
jeans/add-unknown-payment-methods-to-link

Conversation

@jeans-stripe

@jeans-stripe jeans-stripe commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Support displaying and interacting with payment methods the SDK doesn't explicitly recognize by introducing ConsumerPaymentDetails.Unknown and LinkPaymentDetails.Unknown. Previously, unrecognized types were silently dropped, now they can be rendered using server-provided display metadata (label, sublabel, icon URL). The unknown types are still being filtered out by SupportedPaymentTypes in this PR but that will be updated in the next PR.

Key changes:

  • Add Unknown data class with DisplayMetadata for server-driven rendering
  • Parse unrecognized payment_details types as Unknown instead of null
  • Thread Unknown through wallet UI, menus, confirmation, and billing utils
  • Add UnknownIcon composable for loading remote icon URLs
  • Make listPaymentDetails types filter nullable to retrieve all types
  • Add screenshot and unit tests for Unknown in wallet flows

Motivation

We'd like forward compatibility for Link payment methods. When the backend adds PMs, they should be able to be rendered and used for payment in prior versions of the SDK. This PR is a first step towards this goal. This proposal provides more context.

Testing

  • Added tests
  • Modified tests
  • Manually verified

Screenshots

New screenshots have been captured

editCardDetailsInteractor = interactor.editCardDetailsInteractor,
)
}
is LinkPaymentDetails.Unknown -> {}

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.

Leaving empty for now to handle the Unknown case but will be adding UI here in a future PR.

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Diffuse output:

OLD: paymentsheet-example-release-master.apk (signature: V1, V2)
NEW: paymentsheet-example-release-pr.apk (signature: V1, V2)

          │            compressed            │           uncompressed           
          ├───────────┬───────────┬──────────┼───────────┬───────────┬──────────
 APK      │ old       │ new       │ diff     │ old       │ new       │ diff     
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
      dex │   4.6 MiB │   4.6 MiB │ +3.8 KiB │   9.7 MiB │   9.7 MiB │ +6.8 KiB 
     arsc │   3.8 MiB │   3.8 MiB │ +1.5 KiB │   3.8 MiB │   3.8 MiB │ +1.5 KiB 
 manifest │   5.8 KiB │   5.8 KiB │      0 B │  30.8 KiB │  30.8 KiB │      0 B 
      res │     1 MiB │     1 MiB │     -3 B │   1.7 MiB │   1.7 MiB │      0 B 
   native │   2.7 MiB │   2.7 MiB │      0 B │   2.7 MiB │   2.7 MiB │      0 B 
    asset │    25 KiB │    25 KiB │    +30 B │  45.8 KiB │  45.8 KiB │    +30 B 
    other │ 245.4 KiB │ 245.4 KiB │     +5 B │ 505.1 KiB │ 505.1 KiB │      0 B 
──────────┼───────────┼───────────┼──────────┼───────────┼───────────┼──────────
    total │  12.3 MiB │  12.3 MiB │ +5.2 KiB │  18.4 MiB │  18.4 MiB │ +8.2 KiB 

 DEX     │ old   │ new   │ diff              
─────────┼───────┼───────┼───────────────────
   files │     1 │     1 │   0               
 strings │ 45287 │ 45310 │ +23 (+82 -59)     
   types │ 14270 │ 14281 │ +11 (+67 -56)     
 classes │ 11592 │ 11599 │  +7 (+9 -2)       
 methods │ 63380 │ 63420 │ +40 (+2470 -2430) 
  fields │ 41157 │ 41193 │ +36 (+2299 -2263) 

 ARSC    │ old  │ new  │ diff       
─────────┼──────┼──────┼────────────
 configs │  319 │  319 │  0         
 entries │ 7476 │ 7478 │ +2 (+2 -0)
APK
     compressed      │     uncompressed     │                                
──────────┬──────────┼───────────┬──────────┤                                
 size     │ diff     │ size      │ diff     │ path                           
──────────┼──────────┼───────────┼──────────┼────────────────────────────────
  4.6 MiB │ +3.8 KiB │   9.7 MiB │ +6.8 KiB │ ∆ classes.dex                  
  3.8 MiB │ +1.5 KiB │   3.8 MiB │ +1.5 KiB │ ∆ resources.arsc               
  8.9 KiB │    +20 B │   8.8 KiB │    +20 B │ ∆ assets/dexopt/baseline.prof  
  1.2 KiB │    +10 B │     1 KiB │    +10 B │ ∆ assets/dexopt/baseline.profm 
 55.6 KiB │    +10 B │ 130.6 KiB │      0 B │ ∆ META-INF/MANIFEST.MF         
 58.9 KiB │     -5 B │ 130.6 KiB │      0 B │ ∆ META-INF/CERT.SF             
    854 B │     -2 B │   1.8 KiB │      0 B │ ∆ res/8Q.xml                   
  1.5 KiB │     -1 B │     8 KiB │      0 B │ ∆ res/kw.xml                   
──────────┼──────────┼───────────┼──────────┼────────────────────────────────
  8.5 MiB │ +5.2 KiB │  13.8 MiB │ +8.2 KiB │ (total)
DEX
STRINGS:

   old   │ new   │ diff          
  ───────┼───────┼───────────────
   45287 │ 45310 │ +23 (+82 -59) 
  
  + _this_Icon
  + , nextActionTypes=
  + , rawType=
  + Display(label=
  + Generic
  + Generic(id=
  + Generic(nickname=
  + Icon(defaultUrl=
  + Lem/m5;
  + Lnh/u0;
  + Lnh/v0;
  + Lvh/na;
  + Lvh/oa;
  + Lvh/pa;
  + Lvh/qa;
  + Lxh/j;
  + Lz0/u5;
  + [Lem/b2;
  + [Lem/d4;
  + [Lem/q2;
  + [Lem/v3;
  + [Lem/y3;
  + [Lnh/r;
  + [Lvh/a1;
  + [Lvh/a2;
  + [Lvh/b4;
  + [Lvh/da;
  + [Lvh/e1;
  + [Lvh/ea;
  + [Lvh/f1;
  + [Lvh/f5;
  + [Lvh/f8;
  + [Lvh/fa;
  + [Lvh/g2;
  + [Lvh/g4;
  + [Lvh/h4;
  + [Lvh/i6;
  + [Lvh/j7;
  + [Lvh/j9;
  + [Lvh/k5;
  + [Lvh/l2;
  + [Lvh/l3;
  + [Lvh/m1;
  + [Lvh/m8;
  + [Lvh/n1;
  + [Lvh/n2;
  + [Lvh/na;
  + [Lvh/o4;
  + [Lvh/oa;
  + [Lvh/p6;
  + [Lvh/pa;
  + [Lvh/q5;
  + [Lvh/qa;
  + [Lvh/r3;
  + [Lvh/s2;
  + [Lvh/s9;
  + [Lvh/t3;
  + [Lvh/u2;
  + [Lvh/w0;
  + [Lvh/w4;
  + [Lvh/x4;
  + [Lvh/x7;
  + [Lvh/y1;
  + [Lvh/y4;
  + [Lvh/y9;
  + [Lvh/z1;
  + [Lvh/z3;
  + [Lvh/z4;
  + [Lwk/i0;
  + [Lwk/l0;
  + [Lxh/i;
  + [Lz0/k1;
  + [Lz0/y1;
  + [Lz9/b;
  + [Lz9/h;
  + defaultUrl
  + nextActionTypes
  + next_action_types
  + r8-map-id-ab5009afbdfd4fda6d6c664e4fc7fa409b407cbb3e74cef7bc75fb2e101d4c94
  + rawType
  + sublabel
  + ~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":23,"pg-map-id":"ab5009afbdfd4fda6d6c664e4fc7fa409b407cbb3e74cef7bc75fb2e101d4c94","r8-mode":"full","version":"8.13.19"}
  
  - Lz/z1;
  - Lz9/l;
  - [Lem/a2;
  - [Lem/c4;
  - [Lem/o2;
  - [Lem/u3;
  - [Lem/x3;
  - [Lnh/p;
  - [Lvh/b5;
  - [Lvh/c2;
  - [Lvh/c5;
  - [Lvh/d2;
  - [Lvh/d3;
  - [Lvh/d5;
  - [Lvh/e2;
  - [Lvh/e4;
  - [Lvh/e8;
  - [Lvh/ga;
  - [Lvh/ha;
  - [Lvh/i1;
  - [Lvh/i4;
  - [Lvh/i9;
  - [Lvh/ia;
  - [Lvh/j1;
  - [Lvh/j2;
  - [Lvh/j4;
  - [Lvh/j5;
  - [Lvh/j8;
  - [Lvh/ja;
  - [Lvh/k1;
  - [Lvh/k4;
  - [Lvh/m6;
  - [Lvh/n7;
  - [Lvh/o3;
  - [Lvh/o5;
  - [Lvh/q2;
  - [Lvh/q8;
  - [Lvh/r2;
  - [Lvh/s4;
  - [Lvh/s6;
  - [Lvh/t2;
  - [Lvh/v1;
  - [Lvh/v4;
  - [Lvh/w1;
  - [Lvh/w3;
  - [Lvh/w9;
  - [Lvh/x3;
  - [Lvh/y5;
  - [Lvh/z0;
  - [Lwk/f0;
  - [Lwk/j0;
  - [Lxh/f;
  - [Lz0/l1;
  - [Lz0/z1;
  - [Lz9/c;
  - [Lz9/i;
  - expectedPaymentMethodType
  - r8-map-id-359377189dc673b1cd212f22a505cfe1fcbce82f7a9388b1da50416927a7ecec
  - ~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":23,"pg-map-id":"359377189dc673b1cd212f22a505cfe1fcbce82f7a9388b1da50416927a7ecec","r8-mode":"full","version":"8.13.19"}
  

TYPES:

   old   │ new   │ diff          
  ───────┼───────┼───────────────
   14270 │ 14281 │ +11 (+67 -56) 
  
  + Lem/m5;
  + Lnh/u0;
  + Lnh/v0;
  + Lvh/na;
  + Lvh/oa;
  + Lvh/pa;
  + Lvh/qa;
  + Lxh/j;
  + Lz0/u5;
  + [Lem/b2;
  + [Lem/d4;
  + [Lem/q2;
  + [Lem/v3;
  + [Lem/y3;
  + [Lnh/r;
  + [Lvh/a1;
  + [Lvh/a2;
  + [Lvh/b4;
  + [Lvh/da;
  + [Lvh/e1;
  + [Lvh/ea;
  + [Lvh/f1;
  + [Lvh/f5;
  + [Lvh/f8;
  + [Lvh/fa;
  + [Lvh/g2;
  + [Lvh/g4;
  + [Lvh/h4;
  + [Lvh/i6;
  + [Lvh/j7;
  + [Lvh/j9;
  + [Lvh/k5;
  + [Lvh/l2;
  + [Lvh/l3;
  + [Lvh/m1;
  + [Lvh/m8;
  + [Lvh/n1;
  + [Lvh/n2;
  + [Lvh/na;
  + [Lvh/o4;
  + [Lvh/oa;
  + [Lvh/p6;
  + [Lvh/pa;
  + [Lvh/q5;
  + [Lvh/qa;
  + [Lvh/r3;
  + [Lvh/s2;
  + [Lvh/s9;
  + [Lvh/t3;
  + [Lvh/u2;
  + [Lvh/w0;
  + [Lvh/w4;
  + [Lvh/x4;
  + [Lvh/x7;
  + [Lvh/y1;
  + [Lvh/y4;
  + [Lvh/y9;
  + [Lvh/z1;
  + [Lvh/z3;
  + [Lvh/z4;
  + [Lwk/i0;
  + [Lwk/l0;
  + [Lxh/i;
  + [Lz0/k1;
  + [Lz0/y1;
  + [Lz9/b;
  + [Lz9/h;
  
  - Lz/z1;
  - Lz9/l;
  - [Lem/a2;
  - [Lem/c4;
  - [Lem/o2;
  - [Lem/u3;
  - [Lem/x3;
  - [Lnh/p;
  - [Lvh/b5;
  - [Lvh/c2;
  - [Lvh/c5;
  - [Lvh/d2;
  - [Lvh/d3;
  - [Lvh/d5;
  - [Lvh/e2;
  - [Lvh/e4;
  - [Lvh/e8;
  - [Lvh/ga;
  - [Lvh/ha;
  - [Lvh/i1;
  - [Lvh/i4;
  - [Lvh/i9;
  - [Lvh/ia;
  - [Lvh/j1;
  - [Lvh/j2;
  - [Lvh/j4;
  - [Lvh/j5;
  - [Lvh/j8;
  - [Lvh/ja;
  - [Lvh/k1;
  - [Lvh/k4;
  - [Lvh/m6;
  - [Lvh/n7;
  - [Lvh/o3;
  - [Lvh/o5;
  - [Lvh/q2;
  - [Lvh/q8;
  - [Lvh/r2;
  - [Lvh/s4;
  - [Lvh/s6;
  - [Lvh/t2;
  - [Lvh/v1;
  - [Lvh/v4;
  - [Lvh/w1;
  - [Lvh/w3;
  - [Lvh/w9;
  - [Lvh/x3;
  - [Lvh/y5;
  - [Lvh/z0;
  - [Lwk/f0;
  - [Lwk/j0;
  - [Lxh/f;
  - [Lz0/l1;
  - [Lz0/z1;
  - [Lz9/c;
  - [Lz9/i;
  

METHODS:

   old   │ new   │ diff              
  ───────┼───────┼───────────────────
   63380 │ 63420 │ +40 (+2470 -2430) 
  
  + a.a h(w1, i4, t2, i4, g, boolean, boolean, boolean, boolean, String, boolean, a, s, z, z, z, r, int, int, int)
  + a.a k0(ha)
  + a.a u0(ha, t6)
  +
...✂
ARSC
ENTRIES:

   old  │ new  │ diff       
  ──────┼──────┼────────────
   7476 │ 7478 │ +2 (+2 -0) 
  + string/stripe_paymentsheet_manage_payment_method
  + string/stripe_paymentsheet_unknown_details_cannot_be_changed

@jeans-stripe
jeans-stripe force-pushed the jeans/add-unknown-payment-methods-to-link branch 2 times, most recently from a567484 to e2c6312 Compare May 21, 2026 17:21
@jeans-stripe
jeans-stripe marked this pull request as ready for review May 21, 2026 17:22
@jeans-stripe
jeans-stripe requested review from a team as code owners May 21, 2026 17:22

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Parcelize
data class Unknown(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be a huge lift to find all and replace this to Generic instead of Unknown? I'm going to change iOS to Generic and it'd be nice to get that change in ahead of merging this one.

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.

Do we want to update just LinkPaymentDetails.Unknown or ConsumerPaymentDetails.Unknown as well?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jeans-stripe Both please!

get() = when (this) {
is Card -> makeCardDisplayName(nickname, funding, brand)
is BankAccount -> bankName?.resolvableString ?: "••••$last4".resolvableString
is LinkPaymentDetails.Unknown -> label?.resolvableString ?: sublabel?.resolvableString ?: "".resolvableString

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] do we need the LinkPaymentDetails prefix here? Same with line 31

sublabel = "0x••••22Dd",
icon = ConsentUi.Icon(default = "https://example.com/crypto-icon.png")
),
nextActionTypes = listOf("STRIPE_REDIRECT")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] can you update both usages of STRIPE_REDIRECT -> redirect_to_url? Just to make it closer to what is sent by the server.

val nickname: String?,
val label: String?,
val sublabel: String?,
val icon: ConsentUi.Icon?,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] Should this be a ConsentUi icon? I'm not sure about the context of ConsentUi so maybe it's used in most places we parse URLs -> Images in stripe-android, but it seems out of place

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.

As suggested by Till, added a new Icon data class instead of reusing ContentUi.Icon

private const val FIELD_LABEL = "label"
private const val FIELD_SUBLABEL = "sublabel"
private const val FIELD_ICON = "icon"
private const val FIELD_DEFAULT = "default"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nit] A rename may be helpful here, since a reader could confuse this with FIELD_IS_DEFAULT. Maybe FIELD_DEFAULT_ICON?

open val nickname: String?,
open val billingAddress: BillingAddress?,
open val billingEmailAddress: String?,
open val display: DisplayMetadata? = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if we can make this non-null? Since we're filtering out payment method types that don't have a display, we shouldn't be setting this field to null at any point, right?

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.

It has to be nullable in PaymentDetails since we eventually want to add display to Card, Bank, and Passthrough (right? or am I misremembering this?) but aren't parsing display for those types right now. I'll update Unknown/Generic to non-null display to reduce null checks.

} ?: false
is ConsumerPaymentDetails.BankAccount -> allowedCountries.contains(CountryCode.US.value)
is ConsumerPaymentDetails.Unknown -> billingAddress?.countryCode?.let {
allowedCountries.contains(it.value.uppercase()) && display != null && !display?.label.isNullOrBlank()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why include the display conditions here?

is ConsumerPaymentDetails.Passthrough -> ConsumerPaymentDetails.Card.TYPE
is ConsumerPaymentDetails.Card,
is ConsumerPaymentDetails.Passthrough,
is ConsumerPaymentDetails.Unknown -> ConsumerPaymentDetails.Card.TYPE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is probably correct, but @tillh-stripe does this seem ok?

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.

Updated expectedPaymentMethodType to be nullable which matches how iOS handles generics: Made expectedPaymentMethodType nullable to match iOS

LinkController.PaymentMethodType.BankAccount
}
is ConsumerPaymentDetails.Unknown -> {
LinkController.PaymentMethodType.Card

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels off, but we may not have another option? How does this LinkController.PaymentMethodPreview get used?

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.

PaymentMethodPreview.Type is only being used for settlement speed related logic in the OnrampViewModel. If we add a Generic type to LinkController.PaymentMethodType then the user will be able to filter using it if LinkController is made public #13145 so I'm not too sure how else to handle this case. Card and GooglePay have instant settlement speed in the OnrampViewModel and I assumed Generics were going to be instant as well.

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.

Returning a type of Card for non-card payment methods doesn't seem right. It feels to me like we need to introduce a separate type if we want to:
a) return a realistic type as part of PaymentMethodPreview, and
b) allow filtering for cards and bank accounts in the upcoming Presenter.present method in #13145.

@jeans-stripe Can you incorporate this idea in the API review?

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.

Done! Added a Generic type to LinkController.PaymentMethodType. I've updated present() to accept a nullable list of PaymentMethodType in #13145 so I'll need to rebase this PR once that is merged fix merge conflicts and enable filtering for Generic

shouldShowDefaultBadge = true,
),
),
PaymentOptionsItem.SavedPaymentMethod(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to another preview payment option here?

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.

No, sorry this was left in from some testing. Removed!

@tillh-stripe tillh-stripe left a comment

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.

Added a few comments about the code.

The main thing we still need to do is update SupportedPaymentMethodTypes.kt so that we no longer send "types": ["card", "bank_account"], which effectively excludes all unknown payment method types from being included in the response.

val nickname: String?,
val label: String?,
val sublabel: String?,
val icon: ConsentUi.Icon?,

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.

Let's introduce an Icon class inside DisplayMetadata (which we should just rename to Display or DisplayInfo) instead of something from the OAuth flow.

val rawType: String,
override val display: DisplayMetadata?,
val nextActionTypes: List<String>,
) : PaymentDetails(

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.

Suggested change
) : PaymentDetails(
) : PaymentDetails(

modifier = modifier,
title = paymentDetails.displayName.resolve(),
subtitle = paymentDetails.display?.sublabel,
icon = { UnknownIcon(iconUrl = paymentDetails.display?.icon?.default?.takeIf { it.isNotBlank() }) }

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: If we're already checking for a blank URL during parsing, we don't need to do it at the call-sites.

get() = when (this) {
is Card -> makeCardDisplayName(nickname, funding, brand)
is BankAccount -> bankName?.resolvableString ?: "••••$last4".resolvableString
is LinkPaymentDetails.Unknown -> label?.resolvableString ?: sublabel?.resolvableString ?: "".resolvableString

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.

label being nullable makes this tricky. Inside PaymentMethodWithLinkDetailsJsonParser, can we just reject unknown payment method types that don't have a display hash?

is ConsumerPaymentDetails.Passthrough -> {
"•••• $last4".resolvableString
}
is ConsumerPaymentDetails.Unknown -> display?.label?.resolvableString ?: "".resolvableString

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.

This would also be solved that way.

import com.stripe.android.uicore.image.StripeImage

@Composable
fun UnknownIcon(

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: We can remove the Unknown prefix here.

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.

Should we call it LoadableIcon or StripeIcon to make it distinct from material Icon?

val items = remember(paymentDetails) {
buildList {
if (!paymentDetails.isDefault) {
if (!paymentDetails.isDefault && paymentDetails !is ConsumerPaymentDetails.Unknown) {

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.

Let's make this a canBeSetAsDefault on ConsumerPaymentDetails with exhaustive matching.

@jeans-stripe

Copy link
Copy Markdown
Contributor Author

Added a few comments about the code.

The main thing we still need to do is update SupportedPaymentMethodTypes.kt so that we no longer send "types": ["card", "bank_account"], which effectively excludes all unknown payment method types from being included in the response.

Yes, the filtering is removed in #13141. The link sheet closing flow isn't properly animated yet and I also update that to make the full flow functional.

Support displaying and interacting with payment methods the SDK doesn't explicitly recognize by introducing ConsumerPaymentDetails.Unknown and LinkPaymentDetails.Unknown. Previously, unrecognized types were silently dropped — now they render using server-provided display metadata (label, sublabel, icon URL).

Key changes:
- Add Unknown data class with DisplayMetadata for server-driven rendering
- Parse unrecognized payment_details types as Unknown instead of null
- Thread Unknown through wallet UI, menus, confirmation, and billing utils
- Add UnknownIcon composable for loading remote icon URLs
- Make listPaymentDetails types filter nullable to retrieve all types
- Add screenshot and unit tests for Unknown in wallet flows
- Renaming Unknown to Generic
- Renaming DisplayMetadata to Display
- Removing unused code
- Generic payment methods now map to null expectedPaymentMethodType which is passed to backend which already accepts null. This matches the iOS logic for unknown/generic payment methods.
- Added `Generic` LinkPaymentMethodFilter
- `PaymentMethodPreview` now returns `Generic` for `Generic` types
@jeans-stripe
jeans-stripe force-pushed the jeans/add-unknown-payment-methods-to-link branch from 2fc7505 to 33ede93 Compare June 2, 2026 14:32
PaymentMethodDisplayData.Type.BankAccount

LinkController.PaymentMethodType.Generic ->
PaymentMethodDisplayData.Type.Card

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added to this ticket to update when settlement times are server-driven

import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod
import com.stripe.android.paymentsheet.PaymentOptionsItem
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.SavedPaymentMethod

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need this import?

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.

Removed!

jkelle-stripe
jkelle-stripe previously approved these changes Jun 3, 2026
@jeans-stripe
jeans-stripe merged commit 0c9c3b3 into master Jun 10, 2026
69 checks passed
@jeans-stripe
jeans-stripe deleted the jeans/add-unknown-payment-methods-to-link branch June 10, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants