Skip to content

[Due for payment 2025-08-20] Physical Expensify Card isn't visible in NewDot #68033

Closed

Description

@joekaufmanexpensify

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: v9.1.89-19
Reproducible in staging?: Y
Reproducible in production?: Y
If this was caught during regression testing, add the test name, ID and link from TestRail: N/A
Email or phone of affected tester (no customers): daniel@expensify.com
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL: N/A
Issue reported by: @danielrvidal
Slack conversation: #convert

Action Performed:

  1. Login to an account that has both a physical and virtual Expensify combo card (meaning, the card was originally issued in Expensify Classic).
  2. Make sure the physical card was replaced at some point, meaning the physical card's cardID in Expensify is > than the virtual card's cardID.
  3. Go to Settings > Wallet.
  4. Tap the Expensify Card row.

Note: If you're trying to reproduce this and have a combo Expensify Card, but haven't replaced the physical card at some point (meaning you don't meet the conditions to see this bug), you can also simulate the page where we don't show the physical card by manually replacing the cardID in this URL with your virtual cardID: https://staging.new.expensify.com/settings/wallet/card/%cardID%.

Expected Result:

You should see both the physical and virtual card that together make up the combo card on the Expensify Card page.

Actual Result:

If the physical card has a cardID higher than the virtual card, we incorrectly only show the virtual card on this page. One suggested solution to this from @mountiny is to update this grouping logic here to make sure we always keep the physical card ID.

Workaround:

If you manually reload https://staging.new.expensify.com/settings/wallet/card/%cardID% with your physical cardID, then we show both cards on the Expensify Card page.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari
  • MacOS: Desktop

Screenshots/Videos

Expected combo Expensify Card page

Image

Actual combo Expensify Card page because of this bug

Image

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @
Issue OwnerCurrent Issue Owner: @muttmuure

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Aug 6, 2025
  2. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Aug 6, 2025
  3. melvin-bot commented on Aug 6, 2025

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @s77rt (External)

  4. melvin-bot commented on Aug 6, 2025

    @melvin-bot

    Triggered auto assignment to @muttmuure (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

  5. elizhabiboullah commented on Aug 6, 2025

    @elizhabiboullah

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    when a user has a physical Expensify card that was replaced after the virtual card was issued. In this scenario, the physical card (with a higher cardID) is not displayed on the Expensify Card page, even though both the physical and virtual cards should be visible as part of the combo card.

    What is the root cause of that problem?

    The issue is in the card filtering logic, the current implementation filters out cards based on their issuedBy and isVirtual status, which cause physical cards with higher cardIDs to be excluded

    const cardsToShow = useMemo(() => {
    if (shouldDisplayCardDomain) {
    return getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy || !card?.nameValuePairs?.isVirtual) ?? [];
    }
    return [cardList?.[cardID]];
    }, [shouldDisplayCardDomain, cardList, cardID, domain]);

    What changes do you think we should make in order to solve the problem?

    fix the filtering logic to ensure both physical and virtual cards are shown when they're part of the same combo card

    const cardsToShow = useMemo(() => {
        if (!shouldDisplayCardDomain) {
            return [cardList?.[cardID]];
        }
        
        const domainCards = getDomainCards(cardList)[domain] ?? [];
        const virtualCards = domainCards.filter(card => card?.nameValuePairs?.isVirtual);
        const physicalCards = domainCards.filter(card => !card?.nameValuePairs?.isVirtual);
        
        // If we have both virtual and physical cards, combine them
        if (virtualCards.length > 0 && physicalCards.length > 0) {
            return [...virtualCards, ...physicalCards];
        }
        
        return domainCards;
    }, [shouldDisplayCardDomain, cardList, cardID, domain]);

    What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

    N/A

  6. cosmicvulpes commented on Aug 6, 2025

    @cosmicvulpes
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When a workspace has a combo Expensify Card (one physical + one virtual) and the physical card was replaced after the virtual card was created, the Assigned Cards → Expensify Card row in Wallet navigates to a page that only shows the virtual card. The physical card – even though active – is missing from the details screen.

    What is the root cause of that problem?

    PaymentMethodList builds the list of assigned-card rows by

    1. sorting the raw cardList, then
    2. walking that array once, keeping the first card it meets for each domain and merging the rest.

    The original sort ordered cards by “Is this an Expensify card?” only, ignoring whether it is physical or virtual. Since the virtual card’s cardID is always lower than the replacement physical card’s, it becomes the “first” card for the domain. Consequently, the row’s navigation URL contains the virtual cardID, and the destination screen filters out the physical card.

    What changes do you think we should make in order to solve the problem?

    Update the comparator used in PaymentMethodList.

    const assignedCards = Object.values(isLoadingCardList ? {} : (cardList ?? {}))
    // Filter by active cards associated with a domain
    .filter((card) => !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0));
    const assignedCardsSorted = lodashSortBy(assignedCards, (card) => !isExpensifyCard(card.cardID));
    const assignedCardsGrouped: PaymentMethodItem[] = [];
    assignedCardsSorted.forEach((card) => {
    const isDisabled = card.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !!card.errors;

    const assignedCardsSorted = lodashSortBy(assignedCards, card => {
        if (!isExpensifyCard(card.cardID)) {
            return 2;              // Non-Expensify cards last
        }
        // Within Expensify cards put physical before virtual
        return card.nameValuePairs?.isVirtual ? 1 : 0;
    });

    This guarantees that, the physical Expensify card is visited first and its cardID is used for navigation.

    What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

    None

    What alternative solutions did you explore? (Optional)

    None

  7. self-assigned this
    on Aug 6, 2025
  8. s77rt commented on Aug 7, 2025

    @s77rt
    Member

    @elizhabiboullah @cosmicvulpes Were you able to reproduce the bug?

    @mountiny Do you know how I can assign myself a combo card? Or how I can add a policy domain?

    Image
  9. elizhabiboullah commented on Aug 7, 2025

    @elizhabiboullah

    I couldn't reproduce the bug as I don’t have a physical card yet, and the workaround doesn’t work for me

    @elizhabiboullah @cosmicvulpes Were you able to reproduce the bug?

  10. 36 remaining items

  11. garrettmknight commented on Aug 26, 2025

    @garrettmknight
    Contributor

    $250 approved for @s77rt

  12. melvin-bot commented on Aug 27, 2025

    @melvin-bot

    @muttmuure Whoops! This issue is 2 days overdue. Let's get this updated quick!

  13. muttmuure commented on Aug 29, 2025

    @muttmuure
    Contributor

    Offer sent @cosmicvulpes

  14. melvin-bot commented on Sep 1, 2025

    @melvin-bot

    @muttmuure Whoops! This issue is 2 days overdue. Let's get this updated quick!

  15. melvin-bot commented on Sep 3, 2025

    @melvin-bot

    @muttmuure Huh... This is 4 days overdue. Who can take care of this?

  16. melvin-bot commented on Sep 5, 2025

    @melvin-bot

    @muttmuure 6 days overdue. This is scarier than being forced to listen to Vogon poetry!

  17. melvin-bot commented on Sep 9, 2025

    @melvin-bot

    @muttmuure 10 days overdue. I'm getting more depressed than Marvin.

  18. muttmuure commented on Sep 11, 2025

    @muttmuure
    Contributor

    Paid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributors

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions