From 6a9746569f0d90a5b1a020fb060157552f8bc95e Mon Sep 17 00:00:00 2001 From: rrader2890 Date: Tue, 16 Jun 2026 19:34:09 -0400 Subject: [PATCH 1/2] feat(customer-portal): block portal until white-label MSA is signed When customerPortalUser.mustSignAgreement is true, the portal renders a blocking AgreementGate (link to the hosted signing page) instead of any billing sections. Reuses the existing gate page as the signing surface. NOTE: requires `pnpm codegen` against a lago-api carrying the new mustSignAgreement / agreementSigningUrl fields before this typechecks/builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../customerPortal/common/AgreementGate.tsx | 43 +++++++++++++++++++ .../common/CustomerPortalSections.tsx | 10 +++++ .../common/hooks/useCustomerPortalData.ts | 2 + 3 files changed, 55 insertions(+) create mode 100644 src/components/customerPortal/common/AgreementGate.tsx diff --git a/src/components/customerPortal/common/AgreementGate.tsx b/src/components/customerPortal/common/AgreementGate.tsx new file mode 100644 index 0000000000..8dcfdf045d --- /dev/null +++ b/src/components/customerPortal/common/AgreementGate.tsx @@ -0,0 +1,43 @@ +import { Button } from '~/components/designSystem/Button' +import { Typography } from '~/components/designSystem/Typography' + +interface AgreementGateProps { + signingUrl?: string | null +} + +export const AGREEMENT_GATE_TEST_ID = 'customer-portal-agreement-gate' + +// Blocking gate shown in the customer portal until the white-label / SDK MSA is +// signed. Signing happens on the hosted gate page (signingUrl), which renders +// the agreement, records the signature, and collects a payment method. +const AgreementGate = ({ signingUrl }: AgreementGateProps) => { + return ( +
+ + Action required: sign your agreement + + + Before you can access billing, invoices, and usage, please review and accept your + Master Services & White-Label / SDK Agreement. It only takes a minute and no + login is required. + + +
+ ) +} + +export default AgreementGate diff --git a/src/components/customerPortal/common/CustomerPortalSections.tsx b/src/components/customerPortal/common/CustomerPortalSections.tsx index da5198c4a5..b1fee3840e 100644 --- a/src/components/customerPortal/common/CustomerPortalSections.tsx +++ b/src/components/customerPortal/common/CustomerPortalSections.tsx @@ -1,3 +1,4 @@ +import AgreementGate from '~/components/customerPortal/common/AgreementGate' import { useCustomerPortalData } from '~/components/customerPortal/common/hooks/useCustomerPortalData' import useCustomerPortalNavigation from '~/components/customerPortal/common/hooks/useCustomerPortalNavigation' import useCustomerPortalTranslate from '~/components/customerPortal/common/useCustomerPortalTranslate' @@ -25,6 +26,15 @@ const CustomerPortalSections = () => { PremiumIntegrationTypeEnum.RemoveBrandingWatermark, ) + // White-label / SDK customers must sign the MSA before anything else is shown. + if (portalData?.customerPortalUser?.mustSignAgreement) { + return ( +
+ +
+ ) + } + return (
diff --git a/src/components/customerPortal/common/hooks/useCustomerPortalData.ts b/src/components/customerPortal/common/hooks/useCustomerPortalData.ts index 8c93cedcf6..13fd5a8ca7 100644 --- a/src/components/customerPortal/common/hooks/useCustomerPortalData.ts +++ b/src/components/customerPortal/common/hooks/useCustomerPortalData.ts @@ -18,6 +18,8 @@ gql` } applicableTimezone premium + mustSignAgreement + agreementSigningUrl customerType name firstname From 03bd38ed5a28f7797bcd7a15ebb837bade4a98f8 Mon Sep 17 00:00:00 2001 From: rrader2890 Date: Tue, 16 Jun 2026 19:38:22 -0400 Subject: [PATCH 2/2] refactor(customer-portal): fetch MSA gating via raw Apollo (no codegen dependency) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated useGetCustomerPortalDataQuery uses a pre-generated document, so the new white-label fields would be inert until `pnpm codegen` re-runs. Switch to a dedicated raw useQuery hook (useCustomerPortalAgreement) — same precedent as the create-org change — so this builds and works without regenerating types. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../common/CustomerPortalSections.tsx | 7 ++-- .../hooks/useCustomerPortalAgreement.ts | 34 +++++++++++++++++++ .../common/hooks/useCustomerPortalData.ts | 2 -- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/components/customerPortal/common/hooks/useCustomerPortalAgreement.ts diff --git a/src/components/customerPortal/common/CustomerPortalSections.tsx b/src/components/customerPortal/common/CustomerPortalSections.tsx index b1fee3840e..e5cdfd252c 100644 --- a/src/components/customerPortal/common/CustomerPortalSections.tsx +++ b/src/components/customerPortal/common/CustomerPortalSections.tsx @@ -1,4 +1,5 @@ import AgreementGate from '~/components/customerPortal/common/AgreementGate' +import { useCustomerPortalAgreement } from '~/components/customerPortal/common/hooks/useCustomerPortalAgreement' import { useCustomerPortalData } from '~/components/customerPortal/common/hooks/useCustomerPortalData' import useCustomerPortalNavigation from '~/components/customerPortal/common/hooks/useCustomerPortalNavigation' import useCustomerPortalTranslate from '~/components/customerPortal/common/useCustomerPortalTranslate' @@ -19,6 +20,8 @@ const CustomerPortalSections = () => { const { data: portalData } = useCustomerPortalData() + const { mustSignAgreement, agreementSigningUrl } = useCustomerPortalAgreement() + const { viewWallet, viewSubscription, viewEditInformation, viewPlans } = useCustomerPortalNavigation() @@ -27,10 +30,10 @@ const CustomerPortalSections = () => { ) // White-label / SDK customers must sign the MSA before anything else is shown. - if (portalData?.customerPortalUser?.mustSignAgreement) { + if (mustSignAgreement) { return (
- +
) } diff --git a/src/components/customerPortal/common/hooks/useCustomerPortalAgreement.ts b/src/components/customerPortal/common/hooks/useCustomerPortalAgreement.ts new file mode 100644 index 0000000000..5e6ae6c816 --- /dev/null +++ b/src/components/customerPortal/common/hooks/useCustomerPortalAgreement.ts @@ -0,0 +1,34 @@ +import { gql, useQuery } from '@apollo/client' +import { useParams } from 'react-router-dom' + +import { useIsAuthenticated } from '~/hooks/auth/useIsAuthenticated' + +// Raw Apollo query (no codegen dependency, matching the create-org precedent) so +// the new white-label fields are fetched at runtime without regenerating types. +const GET_CUSTOMER_PORTAL_AGREEMENT = gql` + query getCustomerPortalAgreement { + customerPortalUser { + id + mustSignAgreement + agreementSigningUrl + } + } +` + +export const useCustomerPortalAgreement = () => { + const { token } = useParams() + const { isPortalAuthenticated } = useIsAuthenticated() + + const { data, loading } = useQuery(GET_CUSTOMER_PORTAL_AGREEMENT, { + fetchPolicy: 'cache-first', + skip: !isPortalAuthenticated || !token, + }) + + const user = data?.customerPortalUser + + return { + loading, + mustSignAgreement: Boolean(user?.mustSignAgreement), + agreementSigningUrl: (user?.agreementSigningUrl ?? null) as string | null, + } +} diff --git a/src/components/customerPortal/common/hooks/useCustomerPortalData.ts b/src/components/customerPortal/common/hooks/useCustomerPortalData.ts index 13fd5a8ca7..8c93cedcf6 100644 --- a/src/components/customerPortal/common/hooks/useCustomerPortalData.ts +++ b/src/components/customerPortal/common/hooks/useCustomerPortalData.ts @@ -18,8 +18,6 @@ gql` } applicableTimezone premium - mustSignAgreement - agreementSigningUrl customerType name firstname