From 261fa5b646869389be34256226d25f2fba5bc5cb Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 25 Aug 2026 17:08:22 +0530 Subject: [PATCH] fix(billing): stop AddMembersDrawer crashing when members are absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AddMembersDrawer.tsx:45` fed `getBillingTeamMembers`' result straight to `Object.values`, throwing "Cannot convert undefined or null to object" and blanking the whole billing page via the react-router error boundary. It was the only consumer that didn't guard the value — `BillingTeamMembers`, `OtherBillingTeamDetails` and `AddMembersTableActions` all already do. The selector returns `undefined` whenever the members for a team have not been dispatched. `useBillingTeamsListener` only dispatches on a truthy fetch result, and `getBillingTeamMembersProfile` yields a falsy value on a callable error, on `success: false`, and — the case seen in production — when `billing-getMembersProfile` answers `success: true` with an undefined `billingTeamMembers` payload because the billing team document was not found. Guard at the call site, matching what every other consumer does, and encode the absence in the selector's return type so a future caller has to handle it. Deliberately NOT defaulting the selector to `{}`, which was the first attempt here and is wrong two ways: - `BillingTeamMembers/index.tsx:392` and `OtherBillingTeamDetails/index.tsx:210` both drive an antd ``. A truthy empty object makes that `false`, silently replacing the load spinner with an empty-state until members arrive. - `?? {}` allocates a new object per call, and react-redux 8's `useSelector` compares by reference, so the component would re-render on every dispatched action for as long as members are absent. Fixes WEB-APP-21NJ Co-Authored-By: Claude Opus 5 (1M context) --- .../AddMembersDrawer/AddMembersDrawer.tsx | 21 +++++++++---------- app/src/store/features/billing/selectors.ts | 15 ++++++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/src/features/settings/components/BillingTeam/components/BillingDetails/MyBillingTeamDetails/components/AddMembersDrawer/AddMembersDrawer.tsx b/app/src/features/settings/components/BillingTeam/components/BillingDetails/MyBillingTeamDetails/components/AddMembersDrawer/AddMembersDrawer.tsx index 784cb3ed8a..d383e710b9 100644 --- a/app/src/features/settings/components/BillingTeam/components/BillingDetails/MyBillingTeamDetails/components/AddMembersDrawer/AddMembersDrawer.tsx +++ b/app/src/features/settings/components/BillingTeam/components/BillingDetails/MyBillingTeamDetails/components/AddMembersDrawer/AddMembersDrawer.tsx @@ -41,17 +41,16 @@ export const AppMembersDrawer: React.FC = ({ isOpen, onCl 3. Pending members */ - const externalDomainMembers = - Object.values(billingTeamMembers) - .filter((member) => { - return !billingTeamDetails?.ownerDomains?.includes(getDomainFromEmail(member.email)); - }) - .map((member) => { - return { - email: member.email, - domain: member.domain, - }; - }) || []; + const externalDomainMembers = Object.values(billingTeamMembers ?? {}) + .filter((member) => { + return !billingTeamDetails?.ownerDomains?.includes(getDomainFromEmail(member.email)); + }) + .map((member) => { + return { + email: member.email, + domain: member.domain, + }; + }); const orgMembers = organizationMembers || []; diff --git a/app/src/store/features/billing/selectors.ts b/app/src/store/features/billing/selectors.ts index 4c5a0ddf54..ca8704aba4 100644 --- a/app/src/store/features/billing/selectors.ts +++ b/app/src/store/features/billing/selectors.ts @@ -15,7 +15,20 @@ export const getBillingTeamById = (id: string | undefined) => (state: RootState) return allAvailableBillingTeams.find((billingTeam) => billingTeam.id === id); }; -export const getBillingTeamMembers = (billingId: string | undefined) => (state: RootState): Record => { +/** + * Returns `undefined` when the members for `billingId` have not been fetched (or the fetch failed) — + * callers must handle it. Note the `!billingId` branch below predates this and returns `{}` instead; + * that inconsistency is left alone rather than widened here. + * + * Deliberately NOT defaulted to `{}`, because BillingTeamMembers and OtherBillingTeamDetails both + * drive an antd `
`, and a truthy empty object silently swaps + * those spinners for an empty-state. A fresh `{}` per call would additionally re-render both on every + * dispatched action while members are absent; that part is avoidable with a shared constant, the + * loading-state regression is not. + */ +export const getBillingTeamMembers = (billingId: string | undefined) => ( + state: RootState +): Record | undefined => { if (!billingId) { return {}; }