diff --git a/apps/remix/app/components/general/org-menu-switcher.tsx b/apps/remix/app/components/general/org-menu-switcher.tsx index c6c7c0040..68db67f9b 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', @@ -314,6 +302,18 @@ export const OrgMenuSwitcher = () => { )} + + + Personal Inbox + + + + + + Account + + + setLanguageSwitcherOpen(true)} diff --git a/apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx b/apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx index 7e4e1cb53..c2e927d7d 100644 --- a/apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx +++ b/apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx @@ -3,6 +3,8 @@ import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; +import { useSession } from '@documenso/lib/client-only/providers/session'; +import { isPersonalLayout } from '@documenso/lib/utils/organisations'; import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; @@ -15,6 +17,8 @@ export type OrganisationBillingPortalButtonProps = { export const OrganisationBillingPortalButton = ({ buttonProps, }: OrganisationBillingPortalButtonProps) => { + const { organisations } = useSession(); + const organisation = useCurrentOrganisation(); const { _ } = useLingui(); @@ -30,7 +34,10 @@ export const OrganisationBillingPortalButton = ({ const handleCreatePortal = async () => { try { - const { redirectUrl } = await manageSubscription({ organisationId: organisation.id }); + const { redirectUrl } = await manageSubscription({ + organisationId: organisation.id, + isPersonalLayoutMode: isPersonalLayout(organisations), + }); window.open(redirectUrl, '_blank'); } catch (err) { diff --git a/apps/remix/app/components/general/settings-nav-desktop.tsx b/apps/remix/app/components/general/settings-nav-desktop.tsx index 7be14fe23..20000f6d8 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 && ( + + + + )} + + ), + }, + ] 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-personal.tsx similarity index 100% rename from apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing.tsx rename to apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing-personal.tsx 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..92f666f97 --- /dev/null +++ b/apps/remix/app/routes/_authenticated+/settings+/billing.tsx @@ -0,0 +1,24 @@ +import { useLingui } from '@lingui/react/macro'; + +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 { t } = useLingui(); + + return ( +
+ + + +
+ ); +} diff --git a/packages/trpc/server/enterprise-router/create-subscription.ts b/packages/trpc/server/enterprise-router/create-subscription.ts index ddaf603aa..7a01d824e 100644 --- a/packages/trpc/server/enterprise-router/create-subscription.ts +++ b/packages/trpc/server/enterprise-router/create-subscription.ts @@ -71,7 +71,7 @@ export const createSubscriptionRoute = authenticatedProcedure } const returnUrl = isPersonalLayoutMode - ? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing` + ? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing-personal` : `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`; const redirectUrl = await createCheckoutSession({ diff --git a/packages/trpc/server/enterprise-router/manage-subscription.ts b/packages/trpc/server/enterprise-router/manage-subscription.ts index 2a66ebd9e..8d7e688a9 100644 --- a/packages/trpc/server/enterprise-router/manage-subscription.ts +++ b/packages/trpc/server/enterprise-router/manage-subscription.ts @@ -12,7 +12,7 @@ import { ZManageSubscriptionRequestSchema } from './manage-subscription.types'; export const manageSubscriptionRoute = authenticatedProcedure .input(ZManageSubscriptionRequestSchema) .mutation(async ({ ctx, input }) => { - const { organisationId } = input; + const { organisationId, isPersonalLayoutMode } = input; ctx.logger.info({ input: { @@ -93,9 +93,13 @@ export const manageSubscriptionRoute = authenticatedProcedure }); } + const returnUrl = isPersonalLayoutMode + ? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing-personal` + : `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`; + const redirectUrl = await getPortalSession({ customerId, - returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`, + returnUrl, }); return { diff --git a/packages/trpc/server/enterprise-router/manage-subscription.types.ts b/packages/trpc/server/enterprise-router/manage-subscription.types.ts index 0d77166ef..580639272 100644 --- a/packages/trpc/server/enterprise-router/manage-subscription.types.ts +++ b/packages/trpc/server/enterprise-router/manage-subscription.types.ts @@ -2,4 +2,5 @@ import { z } from 'zod'; export const ZManageSubscriptionRequestSchema = z.object({ organisationId: z.string().describe('The organisation to manage the subscription for'), + isPersonalLayoutMode: z.boolean().optional(), });