Skip to content

v6 - Card brands - US debit cards (restricted cards)#2756

Open
araratthehero wants to merge 6 commits into
chore/v6-bin-lookup-alignmentfrom
chore/v6-bin-lookup-restricted-card-brands
Open

v6 - Card brands - US debit cards (restricted cards)#2756
araratthehero wants to merge 6 commits into
chore/v6-bin-lookup-alignmentfrom
chore/v6-bin-lookup-restricted-card-brands

Conversation

@araratthehero
Copy link
Copy Markdown
Contributor

@araratthehero araratthehero commented May 15, 2026

Description

Restricted-brand handling alignment with Web/iOS.

Restricted brands (accel, pulse, star, nyce — US ATM/debit network rails) were stripped from supportedCardBrands client-side before the BIN lookup request was built. That caused the backend to return them as supported=false, the state handler to drop them, and merchants to never know the card had a restricted rail.

This PR moves restricted-brand handling from a client-side filter to the data-to-state classification boundary, matching Web/iOS behavior:

  • CardComponentParamsMapper.removeRestrictedCards() is removed. Restricted brands flow through to the BIN lookup request.
  • RestrictedCardType moves from internal.ui.model to internal.helper and is simplified from an enum to a Set<String> with a top-level isRestrictedCardType() function.
  • Two new internal CardBrandState variants:
    • RestrictedBrand — only restricted brand(s) detected. UI mirrors NoBrandsDetected. No brand sent in /payments.
    • SingleReliableWithRestrictedBrand(cardBrandData) — single supported brand alongside restricted brand(s) (e.g. [visa, accel]). Only the supported logo is shown; restricted logos are never rendered. No brand sent in /payments since the rail is ambiguous.
  • Restricted brands are filtered from the supported card brands grid in CardViewStateProducer, so they never appear in the UI logo list.

6 commits, all on internal code.

Behavior matrix

Scenario Detected Logos shown brand in payload State
Standard single [visa] visa yes SingleReliableBrand (unchanged)
Standard dual [visa, mc] both yes DualBrand (unchanged)
Standard dual w/ selection [visa, cartebancaire] both yes (selected) DualBrandWithShopperSelection (unchanged)
Dual with restricted [visa, accel] visa only no SingleReliableWithRestrictedBrand (new)
3 brands with restricted [visa, cartebancaire, accel] visa + cartebancaire yes (selected) DualBrandWithShopperSelection (existing path)
Restricted only [accel] placeholder + grid no RestrictedBrand (new)
Supported + unsupported restricted [visa(supported), accel(unsupported)] visa yes SingleReliableBrand (unsupported restricted ignored)
All unsupported [discover] (merchant=[visa]) error n/a UnsupportedBrand (unchanged)
No detection [] grid n/a NoBrandsDetected (unchanged)

Checklist

  • Code is unit tested
  • Changes are tested manually
  • Aligned public API changes with other platforms (if applicable)

Ticket Number

COSDK-1204

Restricted brands must flow through to the BIN lookup request,
matching Web/iOS behavior. Filtering is moved to the view layer
in the next commit.

COSDK-1204
Restricted brands (accel, pulse, star, nyce) are now filtered out
in CardViewStateProducer.produce() so they don't appear in the
logo grid. The unfiltered list remains on CardComponentState for
the BIN lookup service.

COSDK-1204
Add RestrictedBrand and DualBrandWithRestrictedBrand to CardBrandState.
Update CardBrandIntentsHandler to detect restricted brands in network
and cached sources, classifying them into the new states.
Update all consumers: cardBrand(), validator, and view state producer.

COSDK-1204
Add test confirming restricted brands pass through to the
BinLookupData callback output unchanged. Fix line length.

COSDK-1204
@araratthehero araratthehero requested a review from a team as a code owner May 15, 2026 08:24
@araratthehero araratthehero added the Chore [PRs only] Indicates any task that does not need to be mentioned in the public release notes label May 15, 2026
@araratthehero araratthehero marked this pull request as draft May 15, 2026 08:24
@github-actions
Copy link
Copy Markdown
Contributor

✅ No public API changes

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the handling of restricted card brands (accel, pulse, star, nyce) by moving the logic to a dedicated helper and updating the card detection flow. It introduces new states, RestrictedBrand and SingleReliableWithRestrictedBrand, within CardBrandState to better manage scenarios where restricted brands are detected alongside or instead of supported brands. Additionally, the CardViewStateProducer now filters restricted brands from the supported brands list, and extensive unit tests have been added to ensure correct behavior. I have no feedback to provide as there were no review comments to assess.

@sonarqubecloud
Copy link
Copy Markdown

@araratthehero araratthehero changed the title [WIP] v6 - Card brands - US debit cards (restricted cards) v6 - Card brands - US debit cards (restricted cards) May 15, 2026
@araratthehero araratthehero marked this pull request as ready for review May 15, 2026 08:30
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.

Do you think using hidden instead of restricted could make this flow clearer?

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.

I agree. Will make the change.

DetectedCardTypeList.Source.NETWORK,
DetectedCardTypeList.Source.CACHED -> {
val nonRestrictedSupportedBrands = supportedDetectedCardTypes.filterNot {
isRestrictedCardType(it.cardBrand.txVariant)
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.

What do you think of adding a boolean inside DetectedCardType that indicates if the brand should be hidden/restricted? IMO it achieves two things:

  • It aligns with other similar values such as isSupported and isShopperSelectionAllowedInDualBranded
  • When this value starts coming from the backend, it will be easy to migrate the logic, only inside NetworkCardBrandDetectionService (same will happen for isShopperSelectionAllowedInDualBranded)

// network detection + no detected brands
detectedCardTypes.isEmpty() -> CardBrandState.NoBrandsDetected

// network detection + only restricted brands are supported
Copy link
Copy Markdown
Collaborator

@jreij jreij May 15, 2026

Choose a reason for hiding this comment

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

I think this when block is a bit difficult to read now, I had a similar issue when I was writing it in my PR and here's what I can suggest:

when {
    // network detection + no detected brands
    detectedCardTypes.isEmpty() -> CardBrandState.NoBrandsDetected

    nonRestrictedSupportedBrands.isEmpty() -> {
        if (anyRestrictedBrandDetected) {
            // network detection + only restricted brands are supported
            CardBrandState.RestrictedBrand
        } else {
            // network detection + detected brands but no supported brands
            CardBrandState.UnsupportedBrand
        }
    }

    nonRestrictedSupportedBrands.size == 1 -> {
        if (anyRestrictedBrandDetected) {
            // network detection + 1 non-restricted supported brand + restricted brand(s)
            CardBrandState.SingleReliableWithRestrictedBrand(
                cardBrandData = nonRestrictedSupportedBrands.first().toCardBrandData(),
            )
        } else {
            // network detection + 1 non-restricted supported brand
            CardBrandState.SingleReliableBrand(
                cardBrandData = nonRestrictedSupportedBrands.first().toCardBrandData(),
            )
        }
    }

    // network detection + multiple non-restricted supported brands
    else -> {
        getDualBrandedCardBrandState(currentState.cardBrandState, nonRestrictedSupportedBrands)
    }
}

Basically you make it a when with 4 cases:

  • No brands at all
  • No brands that are supported and not restricted
  • 1 brand that is supported and not restricted
  • More than 1 brand supported and not restricted

Then inside the blocks you check if anything is restricted. Let me know if you find this easier to read.

val shopperSelectedCardBrandData: CardBrandData,
) : CardBrandState()

data object RestrictedBrand : CardBrandState()
Copy link
Copy Markdown
Collaborator

@jreij jreij May 15, 2026

Choose a reason for hiding this comment

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

Nit: for readability consider ordering the values of this sealed class by: no brands - single brand - dual brand instead of putting the cases with restricted at the bottom.

@jreij jreij self-assigned this May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Chore [PRs only] Indicates any task that does not need to be mentioned in the public release notes size:medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants