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..e5cdfd252c 100644 --- a/src/components/customerPortal/common/CustomerPortalSections.tsx +++ b/src/components/customerPortal/common/CustomerPortalSections.tsx @@ -1,3 +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' @@ -18,6 +20,8 @@ const CustomerPortalSections = () => { const { data: portalData } = useCustomerPortalData() + const { mustSignAgreement, agreementSigningUrl } = useCustomerPortalAgreement() + const { viewWallet, viewSubscription, viewEditInformation, viewPlans } = useCustomerPortalNavigation() @@ -25,6 +29,15 @@ const CustomerPortalSections = () => { PremiumIntegrationTypeEnum.RemoveBrandingWatermark, ) + // White-label / SDK customers must sign the MSA before anything else is shown. + if (mustSignAgreement) { + return ( +
+ +
+ ) + } + 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, + } +}