diff --git a/libs/domains/organizations/feature/src/lib/free-trial-banner/free-trial-banner.tsx b/libs/domains/organizations/feature/src/lib/free-trial-banner/free-trial-banner.tsx index b1dac3e7fa0..4fc0dbca2f1 100644 --- a/libs/domains/organizations/feature/src/lib/free-trial-banner/free-trial-banner.tsx +++ b/libs/domains/organizations/feature/src/lib/free-trial-banner/free-trial-banner.tsx @@ -1,20 +1,25 @@ import { useMemo } from 'react' import { useLocation, useParams } from 'react-router-dom' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' import { SETTINGS_BILLING_SUMMARY_URL, SETTINGS_URL } from '@qovery/shared/routes' import { Banner } from '@qovery/shared/ui' import { useSupportChat } from '@qovery/shared/util-hooks' import { pluralize } from '@qovery/shared/util-js' import useClusterCreationRestriction from '../hooks/use-cluster-creation-restriction/use-cluster-creation-restriction' +const FREE_TRIAL_ADD_CREDIT_CARD_MESSAGE = + 'You are on a free trial. Add a credit card to unlock managed cluster creation. If you need help, please contact us.' + export function FreeTrialBanner() { const { organizationId = '' } = useParams() const { pathname } = useLocation() - const { data: userSignUp } = useUserSignUp() - const hasDxAuth = Boolean(userSignUp?.dx_auth) - const { isInActiveFreeTrial, remainingTrialDays } = useClusterCreationRestriction({ + const { + isClusterCreationRestricted: hasRestriction, + isNoCreditCardRestriction, + isInActiveFreeTrial, + remainingTrialDays, + hasNoCreditCard, + } = useClusterCreationRestriction({ organizationId, - dxAuth: hasDxAuth, }) const { showChat } = useSupportChat() @@ -22,18 +27,31 @@ export function FreeTrialBanner() { SETTINGS_URL(organizationId) + SETTINGS_BILLING_SUMMARY_URL ) + // Show the banner when there is any billing restriction or an active free trial + const shouldShowBanner = hasRestriction || isInActiveFreeTrial + const shouldHideBanner = useMemo( - () => !isInActiveFreeTrial || isOnOrganizationBillingSummaryPage || hasDxAuth, - [isInActiveFreeTrial, isOnOrganizationBillingSummaryPage, hasDxAuth] + () => !shouldShowBanner || isOnOrganizationBillingSummaryPage, + [shouldShowBanner, isOnOrganizationBillingSummaryPage] ) if (shouldHideBanner) { return null } - // Add + 1 because Chargebee return 0 when the trial is ending today + // Generic restriction (not NO_CREDIT_CARD): deployments are blocked + if (hasRestriction && !isNoCreditCardRestriction) { + return ( + showChat()}> + Deployments are restricted on your organization. Please contact support to resolve this issue. + + ) + } + + // Free trial: ask to add card only when billing restricts cluster creation (NO_CREDIT_CARD), otherwise show expiry countdown const days = (remainingTrialDays ?? 0) + 1 - const message = `Your free trial plan expires ${days} ${pluralize(days, 'day')} from now. If you need help, please contact us.` + const expiryMessage = `Your free trial plan expires ${days} ${pluralize(days, 'day')} from now. If you need help, please contact us.` + const message = hasNoCreditCard && isNoCreditCardRestriction ? FREE_TRIAL_ADD_CREDIT_CARD_MESSAGE : expiryMessage return ( showChat()}> diff --git a/libs/domains/organizations/feature/src/lib/hooks/use-add-credit-card/use-add-credit-card.ts b/libs/domains/organizations/feature/src/lib/hooks/use-add-credit-card/use-add-credit-card.ts index 78c953c6a86..75731cee1af 100644 --- a/libs/domains/organizations/feature/src/lib/hooks/use-add-credit-card/use-add-credit-card.ts +++ b/libs/domains/organizations/feature/src/lib/hooks/use-add-credit-card/use-add-credit-card.ts @@ -10,6 +10,9 @@ export function useAddCreditCard() { queryClient.invalidateQueries({ queryKey: queries.organizations.creditCards({ organizationId }).queryKey, }) + queryClient.invalidateQueries({ + queryKey: queries.organizations.details({ organizationId }).queryKey, + }) }, meta: { notifyOnSuccess: { diff --git a/libs/domains/organizations/feature/src/lib/hooks/use-cluster-creation-restriction/use-cluster-creation-restriction.ts b/libs/domains/organizations/feature/src/lib/hooks/use-cluster-creation-restriction/use-cluster-creation-restriction.ts index 5c45b1eb357..a66d38acd95 100644 --- a/libs/domains/organizations/feature/src/lib/hooks/use-cluster-creation-restriction/use-cluster-creation-restriction.ts +++ b/libs/domains/organizations/feature/src/lib/hooks/use-cluster-creation-restriction/use-cluster-creation-restriction.ts @@ -1,60 +1,55 @@ import { useMemo } from 'react' import useCreditCards from '../use-credit-cards/use-credit-cards' import useCurrentCost from '../use-current-cost/use-current-cost' +import useOrganization from '../use-organization/use-organization' export interface UseClusterCreationRestrictionProps { organizationId: string - /** When true, cluster creation is never restricted (e.g. DX auth users). */ - dxAuth?: boolean } /** * Hook to determine if cluster creation should be restricted. * - * Clusters (except demo) are restricted when: - * - User is not dxAuth (dxAuth users are never restricted) - * - AND user is in an active free trial (inverse of the free-trial-banner hide condition) - * - AND user has no credit card registered - * - * @see https://qovery.slack.com/archives/C02P3MA2NKT/p1768564947277349 + * Uses the backend-provided `billing_deployment_restriction` field on the organization: + * - null → no restriction + * - 'NO_CREDIT_CARD' → free trial restriction (blocks managed cluster creation, allows demo) + * - any other string → blocks all deployments */ -export function useClusterCreationRestriction({ organizationId, dxAuth }: UseClusterCreationRestrictionProps) { +export function useClusterCreationRestriction({ organizationId }: UseClusterCreationRestrictionProps) { + const { data: organization, isFetched: isFetchedOrganization } = useOrganization({ organizationId }) const { data: currentCost, isFetched: isFetchedCurrentCost } = useCurrentCost({ organizationId }) - const { data: creditCards, isFetched: isFetchedCreditCards } = useCreditCards({ organizationId }) + const { data: creditCards = [], isFetched: isFetchedCreditCards } = useCreditCards({ organizationId }) const remainingTrialDays = currentCost?.remaining_trial_day - // Check if user is in active free trial - // This is the inverse of the condition in free-trial-banner.tsx that hides the banner - // Original condition (to hide banner): - // remainingTrialDays === undefined || remainingTrialDays <= 0 || remainingTrialDays > 90 || !isFetchedCurrentCost - // Inverse (user is in active trial): - // remainingTrialDays is defined AND > 0 AND <= 90 AND data is fetched + const billingDeploymentRestriction = organization?.billing_deployment_restriction + + // Check if user is in active free trial (used by free-trial-banner) const isInActiveFreeTrial = useMemo( () => isFetchedCurrentCost && remainingTrialDays !== undefined && remainingTrialDays > 0 && remainingTrialDays <= 90, [isFetchedCurrentCost, remainingTrialDays] ) - // Check if user has no credit card - const hasNoCreditCard = useMemo( - () => isFetchedCreditCards && (!creditCards || creditCards.length === 0), - [isFetchedCreditCards, creditCards] - ) - - // Do not restrict when dxAuth is true (e.g. DX auth users bypass trial/credit-card rules) + // Cluster creation is restricted when the backend sets a billing deployment restriction const isClusterCreationRestricted = useMemo( - () => !dxAuth && isInActiveFreeTrial && hasNoCreditCard, - [dxAuth, isInActiveFreeTrial, hasNoCreditCard] + () => isFetchedOrganization && billingDeploymentRestriction != null, + [isFetchedOrganization, billingDeploymentRestriction] ) - const isLoading = !isFetchedCurrentCost || !isFetchedCreditCards + const isNoCreditCardRestriction = billingDeploymentRestriction === 'NO_CREDIT_CARD' + + const hasNoCreditCard = isFetchedCreditCards && creditCards.length === 0 + + const isLoading = !isFetchedOrganization || !isFetchedCurrentCost return { isClusterCreationRestricted, + isNoCreditCardRestriction, + hasNoCreditCard, isLoading, isInActiveFreeTrial, - hasNoCreditCard, + billingDeploymentRestriction, remainingTrialDays, } } diff --git a/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx b/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx index 86b119a3f5e..59b98fa7642 100644 --- a/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx +++ b/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx @@ -13,7 +13,6 @@ import { NavLink, useParams } from 'react-router-dom' import { match } from 'ts-pattern' import { ClusterInstallationGuideModal } from '@qovery/domains/clusters/feature' import { useClusterCreationRestriction } from '@qovery/domains/organizations/feature' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' import { AddCreditCardModalFeature } from '@qovery/shared/console-shared' import { CLUSTERS_TEMPLATE_CREATION_URL, CLUSTERS_URL, SETTINGS_BILLING_URL, SETTINGS_URL } from '@qovery/shared/routes' import { Button, Callout, Heading, Icon, Link, Section, useModal } from '@qovery/shared/ui' @@ -391,12 +390,8 @@ export function PageNewFeature() { const { organizationId = '' } = useParams() useDocumentTitle('Create new cluster - Qovery') const { openModal, closeModal } = useModal() - const { data: userSignUp } = useUserSignUp() - const hasDxAuth = Boolean(userSignUp?.dx_auth) - - const { isClusterCreationRestricted } = useClusterCreationRestriction({ + const { isClusterCreationRestricted, isNoCreditCardRestriction } = useClusterCreationRestriction({ organizationId, - dxAuth: hasDxAuth, }) const openInstallationGuideModal = ({ isDemo = false }: { isDemo?: boolean } = {}) => @@ -608,38 +603,52 @@ export function PageNewFeature() { Or choose your hosting mode

Manage your infrastructure across different hosting mode.

- {isClusterCreationRestricted && ( - - - - - - Add a credit card to create a cluster - - You need to add a credit card to your account before creating a cluster on a cloud provider. You won’t - be charged until your trial ends. -
- - Add credit card - - -
-
-
- )} + {isClusterCreationRestricted && + (isNoCreditCardRestriction ? ( + + + + + + Add a credit card to create a cluster + + You need to add a credit card to your account before creating a cluster on a cloud provider. You + won't be charged until your trial ends. +
+ + Add credit card + + +
+
+
+ ) : ( + + + + + + Cluster creation is restricted + + Your organization has a billing restriction that prevents cluster creation. Please contact support + to resolve this issue. + + + + ))}
{cloudProviders.slice(1).map((props, index) => ( ))} diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx index 80d8f88be4e..7e1ac405c32 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx +++ b/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx @@ -54,10 +54,8 @@ export function PageOrganizationBillingSummary(props: PageOrganizationBillingSum // It's not so accurate, but it's a good enough approximation for now const billingRecurrence = getBillingRecurrenceStr(props.currentCost?.renewal_at) const remainingTrialDay = props.currentCost?.remaining_trial_day ?? 0 - const hasDxAuth = Boolean(userSignUp?.dx_auth) - const showTrialCallout = - remainingTrialDay !== undefined && remainingTrialDay > 0 && !props.creditCardLoading && !hasDxAuth - const showErrorCallout = (props.hasCreditCard ?? Boolean(props.creditCard)) || hasDxAuth + const showTrialCallout = remainingTrialDay !== undefined && remainingTrialDay > 0 && !props.creditCardLoading + const hasNoCreditCard = !(props.hasCreditCard ?? Boolean(props.creditCard)) && !userSignUp?.dx_auth // This function is used to get the trial start date based on the remaining trial days from the API const trialStartDate = useMemo(() => { @@ -83,31 +81,31 @@ export function PageOrganizationBillingSummary(props: PageOrganizationBillingSum
{showTrialCallout && ( - + {/* Add + 1 because Chargebee return 0 when the trial is ending today */} - {showErrorCallout - ? `Your free trial plan expires ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')} from now` - : `No credit card registered, your account will be blocked at the end your trial in ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')}`} + {hasNoCreditCard + ? `No credit card registered, your account will be blocked at the end your trial in ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')}` + : `Your free trial plan expires ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')} from now`} - {showErrorCallout ? ( + {hasNoCreditCard ? ( + <>Add a payment method to avoid service interruption at the end of your trial. + ) : ( <> You have contracted a free 14-days trial on{' '} {trialStartDate ? format(trialStartDate, 'MMMM d, yyyy') : '...'}. At the end of this plan your user subscription will start. You cancel your trial by deleting your organization. - ) : ( - <>Add a payment method to avoid service interruption at the end of your trial. )} )} diff --git a/package.json b/package.json index a0b02548859..46b07a6b60d 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "mermaid": "^11.6.0", "monaco-editor": "0.53.0", "posthog-js": "^1.260.1", - "qovery-typescript-axios": "1.1.830", + "qovery-typescript-axios": "1.1.832", "react": "18.3.1", "react-country-flag": "^3.0.2", "react-datepicker": "^4.12.0", diff --git a/yarn.lock b/yarn.lock index a0ea77b43fe..140729fc9e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5451,7 +5451,7 @@ __metadata: prettier: ^3.2.5 prettier-plugin-tailwindcss: ^0.5.14 pretty-quick: ^4.0.0 - qovery-typescript-axios: 1.1.830 + qovery-typescript-axios: 1.1.832 qovery-ws-typescript-axios: ^0.1.420 react: 18.3.1 react-country-flag: ^3.0.2 @@ -24269,12 +24269,12 @@ __metadata: languageName: node linkType: hard -"qovery-typescript-axios@npm:1.1.830": - version: 1.1.830 - resolution: "qovery-typescript-axios@npm:1.1.830" +"qovery-typescript-axios@npm:1.1.832": + version: 1.1.832 + resolution: "qovery-typescript-axios@npm:1.1.832" dependencies: axios: 1.12.2 - checksum: 7545153eac1d9b3e8192c2ef6288fee203714ec81404581e1b6871a69937335d03c7820d5c2c8934f73eed96f0ac0e5bda4c7149d88ee23df26a56f8822e3128 + checksum: 1337d725e15ff6c5ed82f183a2e0b2bd0cbfa86ff0826df327da8a1778f05ff363279b0dd74e9a9f450eb9f09fd63fc35988c9928960466fbc7767b9d4639ddd languageName: node linkType: hard