Fix: Prevent dead-end when closing account with outstanding balance#82905
Conversation
…xpensify#82200) Keep Subscription page and menu item accessible when amountOwed > 0, even after all workspaces are deleted. Add pre-deletion guard on last paid workspace to redirect users to subscription settings to settle their outstanding balance first. Co-Authored-By: Claude Opus 4.6 <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 |
|
All contributors have signed the CLA ✍️ ✅ |
|
@carlosmiceli Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
| @@ -544,6 +570,18 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa | |||
| shouldShowCancelButton={false} | |||
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This ConfirmModal block (including its state isOutstandingBalanceModalOpen, the setIsOutstandingBalanceModalOpen setter, and the onConfirm/onCancel handlers) is duplicated nearly identically in WorkspacesListPage.tsx. Both instances share the same title, prompt, confirmText, cancelText, and navigation logic.
Consider extracting this into a shared custom hook (e.g., useOutstandingBalanceGuard) that encapsulates the modal state, the guard check (hasAmountOwed() && ownerPolicies.length === 1), and returns the modal component or its props. This would eliminate the duplication and ensure both pages stay in sync if the behavior changes.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
PR Review: Fix outstanding balance dead-endKey concerns:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: edf21ee925
ℹ️ 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".
| setPolicyIDToDelete(item.policyID); | ||
| setPolicyNameToDelete(item.title); | ||
|
|
||
| if (hasAmountOwed() && getOwnedPaidPolicies(policies, currentUserPersonalDetails?.accountID).length === 1) { |
There was a problem hiding this comment.
Restrict outstanding-balance guard to paid workspace deletions
This guard blocks deletion whenever the user has exactly one owned paid workspace, but it does not verify that the workspace being deleted is that paid workspace. In production, a user with one paid workspace (carrying the balance) plus any free workspace will be prevented from deleting the free workspace and incorrectly sent to Subscription, even though they are not deleting their last paid workspace. The same predicate pattern is also used in WorkspaceOverviewPage, so both delete entry points over-block.
Useful? React with 👍 / 👎.
|
I have read the CLA Document and I hereby sign the CLA |
Extract duplicated ConfirmModal state, guard check, and modal component from WorkspaceOverviewPage and WorkspacesListPage into a shared useOutstandingBalanceGuard hook to keep both pages in sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove formatting noise: only meaningful changes, no Prettier reformatting - Use consistent reactive pattern: useOnyx for amountOwed in both InitialSettingsPage and SubscriptionSettingsPage (instead of getAmountOwed) - Use consistent ownerPoliciesSelector in both WorkspaceOverviewPage and WorkspacesListPage (instead of getOwnedPaidPolicies inline) - Guard ordering verified: invoicify check runs first, then outstanding balance check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…standing-balance-dead-end
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Good for product
|
Shouldn't @Ollyws have been assigned as reviewer here? |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ore showing guard modal The OutstandingBalanceModal was created via useCallback as a component, causing unmount/remount issues where the modal never appeared on native platforms. Changed to return a JSX element via useMemo instead. Also added wouldBlockDeletion boolean so popover/dropdown menus close before the guard modal shows — otherwise the open popover blocks the ConfirmModal on native. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hi @carlosmiceli @joekaufmanexpensify @Ollyws , the PR is fully ready for review. |
…outstanding-balance-dead-end
|
@luacmartins please review again. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a1d259022
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@imgyf ESLint check is failing. Maybe you can merge main to fix it? |
…outstanding-balance-dead-end
|
@luacmartins merged main |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 @luacmartins 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! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/luacmartins in version: 9.3.36-0 🚀
|
|
Hi @imgyf. QA team is not able to enable the 'Outstanding balance' workspace when running the Onyx code on the web and opening the account on mobile. Does this PR apply to Native apps? |
|
Hi @IuliiaHerets, yes the PR applies to Native apps too. You can see my videos in the PR description. For Native apps testing, I did not run Onyx code on the web but instead added useEffect to simulate the condition. |
|
@imgyf I don't think Applause can'add useEffect in native aops |
|
@kavimuru sorry what's Applause? is there a way where you can actually get to the condition without using Onyx? |
|
@imgyf we don't have access to the console in staging builds, so we need different steps to test this |
|
@luacmartins @kavimuru @IuliiaHerets Is testing on staging web where browser console is available acceptable? Because the UI logic is shared across all platforms, so web verification covers the fix. If it is not acceptable, then QA would need to create an actual outstanding balance by:
|
|
@imgyf we have to test on all platforms, including ios/android native |
|
@luacmartins can we have QA create an actual outstanding balance by having a paid workspace with a billing method that fails/gets removed? This would naturally set |
|
🚀 Deployed to production by https://github.com/luacmartins in version: 9.3.36-10 🚀
|
Explanation of Change
When a user deletes their last paid workspace while having an outstanding billing balance, the Subscription menu item and page become inaccessible (because
useSubscriptionPlanreturns null with no owned paid policies). This leaves the user in a dead-end state where they cannot settle their balance or close their account.This PR:
amountOwed > 0, even after all workspaces are deleteduseOutstandingBalanceGuardhook used by both WorkspaceOverviewPage and WorkspacesListPageFixed Issues
$ #82200
PROPOSAL: #82200 (comment)
Tests
Onyx.set('nvp_private_amountOwed', 100)to simulate an outstanding balanceOnyx.set('nvp_private_amountOwed', 100)again in consoleOffline tests
amountOwed > 0and no workspaces, verify the Subscription menu item is still visible in SettingsQA Steps
Same as Tests above, performed on staging environment.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, 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) N/A — no generic components were modifiedDesignlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page. N/A — no new pages were addedmainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
android-native.mp4
Android: mWeb Chrome
mweb-chrome.mp4
iOS: Native
ios-native.mov
iOS: mWeb Safari
mweb-safari.mov
MacOS: Chrome / Safari
bug-fixed.mov