[HOLD] Show balance and remaining limit for Plaid company card feeds - #94096
[HOLD] Show balance and remaining limit for Plaid company card feeds#94096ishpaul777 wants to merge 13 commits into
Conversation
Display a balance block (current balance + remaining limit) above the company cards table for Plaid-connected feeds, mirroring the Expensify Card page. Each stat has an info tooltip with the last-updated timestamp reported by the bank. The block is hidden for non-Plaid feeds and when no balance data is available, and shows "Not available" for a field the bank did not report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
|
draft WIP for now will open soon |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| <Text style={styles.shortTermsHeadline}>{displayValue}</Text> | ||
| <Popover | ||
| onClose={() => setVisible(false)} | ||
| isVisible={isVisible} |
There was a problem hiding this comment.
❌ UI-3 (docs)
innerContainerStyle={!shouldUseNarrowLayout ? {maxWidth: variables.modalContentMaxWidth} : undefined} passes an inline style object literal to a *Style prop. {maxWidth: variables.modalContentMaxWidth} is a fully static value that belongs in the shared style system; the inline literal creates a new object every render and bypasses the styles helpers.
Add a named style (e.g. styles.popoverMaxWidth returning {maxWidth: variables.modalContentMaxWidth}) in the style sheet and reference it: innerContainerStyle={!shouldUseNarrowLayout ? styles.popoverMaxWidth : undefined}.
Reviewed at: a0a6c8a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Done — replaced the inline {maxWidth: variables.modalContentMaxWidth} with a named styles.cardLabelTooltipContainer in the shared component.
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3151435db5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }, [isVisible, windowWidth]); | ||
|
|
||
| const displayValue = value === undefined ? translate('workspace.companyCards.balance.notAvailable') : convertToDisplayString(value, currency); | ||
| const formattedLastUpdated = lastUpdated ? format(new Date(lastUpdated.replace(' ', 'T')), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING) : undefined; |
There was a problem hiding this comment.
Use UTC-aware formatting for balance timestamps
When the backend sends balanceTimestamp in the same DB format used elsewhere (yyyy-MM-dd HH:mm:ss, serialized as UTC without a Z), this parses it as a local device time because the replacement does not add a timezone. In non-UTC timezones the tooltip will show a shifted “last updated” time compared with existing company-card timestamps that go through getLocalDateFromDatetime, so admins can see incorrect balance freshness.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed — switched to getLocalDateFromDatetime, which treats the backend datetime as UTC (it appends Z) and converts to the user's selected timezone, so the tooltip no longer shows a shifted last-updated time.
|
Switching @joekaufmanexpensify on for this one. |
Move the duplicated header, value, anchor-positioning and info tooltip out of WorkspaceCardsListLabel into a reusable WorkspaceCardLabel, and replace the inline popover max-width with a named style.
Render the company card balance stats via WorkspaceCardLabel and format the last-updated time with getLocalDateFromDatetime so it honors the user's locale and timezone.
Shrink labels to content width and align with feed header using ph5/mt2, with gap24 spacing between them.
|
@MelvinBot fix conflicts |
|
Let me know if you have videos/screenshots for us to review! |
|
looks like we have conflicts .. lets also cover the screenshots |
…mpany-card-balance-frontend # Conflicts: # src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
|
resolved conflicts and screenshots in PR description.@shawnborton @abzokhattab @joekaufmanexpensify you might want to test this with a real connection in adhoc build |
Yep, I will 👍 |
|
🚧 joekaufmanexpensify has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
@shawnborton increased margin bottom to 24px
|
|
that looks better, thanks! |
|
reviewing today from my side as well |
|
🚧 joekaufmanexpensify has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
backend is down for few hours couldnt test this, will take a look tomorrow https://expensify.slack.com/archives/C01GTK53T8Q/p1783539221593109 |
|
looks like we have conflicts @ishpaul777 |
…mpany-card-balance-frontend # Conflicts: # src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
|
When Amex is a "no preset limit" card - there's no fixed ceiling, so the bank doesn't send us a limit at all. In that case Amex reports the leftover amount as 0, and we were taking that literally and showing $0, which is misleading. The signal we can rely on is exactly that: if the bank doesn't give us a limit for the card, there's no meaningful "remaining limit" to show. So I've updated the backend to show "Not available" in that situation instead of $0. Cards that do have a real limit (like your other credit card) are unaffected and still show the correct remaining amount - and a card that's genuinely maxed out will still correctly show $0. This is a backend change [PR here] it should not block this PR |
|
Nice, thanks. That sounds like a great fix. What do you think about this comment @ishpaul777? |
Sorry for missing this comment earlier - I wasn't sure about the exact code path at the time, but I've dug into it now. You're right on all counts, and I agree we should decouple this. Here's the approach I'm landing on: Today the balance only refreshes inside the card scrape, and only for feeds that have an assigned card being scraped. So with no cards assigned the block never shows, and even with cards it only appears after the first scrape (up to ~24h later). That's not the behavior we want.
One catch worth calling out: this "create the job on connect" logic only covers feeds connected after it ships. Feeds that are already connected today (like yours) won't have a job yet, so we'll need a one-time backfill to create the recurring job for every existing feed. I am not quite sure on how we do it in general i'll bring this slack for engineering help. |
|
Sounds great. Thanks for bringing this up in Slack! |
|
@ishpaul777 gentle reminder on the above |
|
Thanks! its on my radar working on PR here https://github.com/Expensify/Web-Expensify/pull/54603 |
|
gentle reminder on this one @ishpaul777 |
Backend PR was stalled, i pinged reviewer again putting this on HOLD for now |






Explanation of Change
Displays a balance block (Current balance + Remaining limit) above the company cards table for Plaid-connected (direct) feeds, mirroring the pattern on the Expensify Card page.
currentBalance,remainingLimit, andbalanceTimestamp(all in cents / datetime) to the company card feed type (CustomCardFeedData). These are populated by the backend from Plaid (see backend PR below).WorkspaceCompanyCardsBalanceLabelsrenders the two stats above the table; each stat (WorkspaceCompanyCardsBalanceLabel) has an info tooltip showing the last-updated timestamp reported by the bank.isDirectFeed).Depends on backend PR: https://github.com/Expensify/Web-Expensify/pull/53882 — that PR fetches the balance from Plaid and stores it on the feed (in cents). This PR is display-only and is safe to merge after it.
Note: amounts are displayed in the policy's output currency (fallback USD), consistent with the rest of the company cards page.
Fixed Issues
$ #93198
PROPOSAL: N/A (internal)
Tests
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps./** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Screenshots/Videos