From 7d833c2348ba3d02831bff8f0dab1fc0525a012c Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Wed, 29 Oct 2025 01:32:19 +0000 Subject: [PATCH] feat: simplify billing ux --- .../components/general/org-menu-switcher.tsx | 24 ++-- .../general/settings-nav-desktop.tsx | 36 ++--- .../general/settings-nav-mobile.tsx | 36 ++--- .../user-billing-organisations-table.tsx | 136 ++++++++++++++++++ .../_dynamic_personal_routes+/billing.tsx | 5 - .../_authenticated+/settings+/billing.tsx | 27 ++++ 6 files changed, 215 insertions(+), 49 deletions(-) create mode 100644 apps/remix/app/components/tables/user-billing-organisations-table.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing.tsx create mode 100644 apps/remix/app/routes/_authenticated+/settings+/billing.tsx diff --git a/apps/remix/app/components/general/org-menu-switcher.tsx b/apps/remix/app/components/general/org-menu-switcher.tsx index c6c7c0040..06c76b3fb 100644 --- a/apps/remix/app/components/general/org-menu-switcher.tsx +++ b/apps/remix/app/components/general/org-menu-switcher.tsx @@ -282,18 +282,6 @@ export const OrgMenuSwitcher = () => { )} - - - Personal Inbox - - - - - - Account - - - {currentOrganisation && canExecuteOrganisationAction( 'MANAGE_ORGANISATION', @@ -306,6 +294,18 @@ export const OrgMenuSwitcher = () => { )} + + + Personal Inbox + + + + + + Account + + + {currentTeam && canExecuteTeamAction('MANAGE_TEAM', currentTeam.currentTeamRole) && ( diff --git a/apps/remix/app/components/general/settings-nav-desktop.tsx b/apps/remix/app/components/general/settings-nav-desktop.tsx index 7be14fe23..0f42a9eb7 100644 --- a/apps/remix/app/components/general/settings-nav-desktop.tsx +++ b/apps/remix/app/components/general/settings-nav-desktop.tsx @@ -16,7 +16,7 @@ import { Link } from 'react-router'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; -import { isPersonalLayout } from '@documenso/lib/utils/organisations'; +import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -29,6 +29,10 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr const isPersonalLayoutMode = isPersonalLayout(organisations); + const hasManageableBillingOrgs = organisations.some((org) => + canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole), + ); + return (
@@ -127,21 +131,6 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr Webhooks - - {IS_BILLING_ENABLED() && ( - - - - )} )} @@ -158,6 +147,21 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr + {IS_BILLING_ENABLED() && hasManageableBillingOrgs && ( + + + + )} + - - {IS_BILLING_ENABLED() && ( - - - - )} )} @@ -158,6 +147,21 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp + {IS_BILLING_ENABLED() && hasManageableBillingOrgs && ( + + + + )} + + ); +}; + +export const UserBillingOrganisationsTable = () => { + const { _ } = useLingui(); + const { organisations } = useSession(); + + const billingOrganisations = useMemo(() => { + return organisations.filter((org) => + canExecuteOrganisationAction('MANAGE_BILLING', org.currentOrganisationRole), + ); + }, [organisations]); + + const getSubscriptionStatusDisplay = (status: SubscriptionStatus | undefined) => { + return match(status) + .with(SubscriptionStatus.ACTIVE, () => ({ + label: _(msg`Active`), + variant: 'default' as const, + })) + .with(SubscriptionStatus.PAST_DUE, () => ({ + label: _(msg`Past Due`), + variant: 'warning' as const, + })) + .with(SubscriptionStatus.INACTIVE, () => ({ + label: _(msg`Inactive`), + variant: 'neutral' as const, + })) + .otherwise(() => ({ + label: _(msg`Free`), + variant: 'neutral' as const, + })); + }; + + const columns = useMemo(() => { + return [ + { + header: _(msg`Organisation`), + accessorKey: 'name', + cell: ({ row }) => ( + + {row.original.name} + } + secondaryText={`${NEXT_PUBLIC_WEBAPP_URL()}/o/${row.original.url}`} + /> + + ), + }, + { + header: _(msg`Subscription Status`), + accessorKey: 'subscription', + cell: ({ row }) => { + const subscription = row.original.subscription; + const status = subscription?.status; + const { label, variant } = getSubscriptionStatusDisplay(status); + + return {label}; + }, + }, + { + header: _(msg`Actions`), + id: 'actions', + cell: ({ row }) => , + }, + ] satisfies DataTableColumnDef<(typeof billingOrganisations)[number]>[]; + }, [_, billingOrganisations]); + + if (billingOrganisations.length === 0) { + return ( +
+

+ You don't manage billing for any organisations. +

+
+ ); + } + + return ( + + ); +}; diff --git a/apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing.tsx b/apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing.tsx deleted file mode 100644 index 037034421..000000000 --- a/apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import BillingPage, { meta } from '../../o.$orgUrl.settings.billing'; - -export { meta }; - -export default BillingPage; diff --git a/apps/remix/app/routes/_authenticated+/settings+/billing.tsx b/apps/remix/app/routes/_authenticated+/settings+/billing.tsx new file mode 100644 index 000000000..0f5f00889 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/settings+/billing.tsx @@ -0,0 +1,27 @@ +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; + +import { SettingsHeader } from '~/components/general/settings-header'; +import { UserBillingOrganisationsTable } from '~/components/tables/user-billing-organisations-table'; +import { appMetaTags } from '~/utils/meta'; + +export function meta() { + return appMetaTags('Billing'); +} + +export default function SettingsBilling() { + const { _ } = useLingui(); + + return ( +
+ + + +
+ ); +}