From 95c089d676511c9a0ff97462e4342ceea36aa7e4 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Wed, 18 Feb 2026 14:28:37 +0100 Subject: [PATCH 1/4] feat(QOV-1600): use backend billing_deployment_restriction for cluster creation blocking Replace client-side heuristic (trial days + credit card check) with the backend-provided billing_deployment_restriction field from the organization API. - Hook: restriction driven by billing_deployment_restriction != null - FreeTrialBanner: shows for NO_CREDIT_CARD (trial message) and for other restrictions (generic deployment blocked message, red banner) - Cluster creation page: NO_CREDIT_CARD shows "add credit card" callout with modal, other restrictions show a generic blocked callout without credit card action --- .../free-trial-banner/free-trial-banner.tsx | 29 ++++++-- .../use-add-credit-card.ts | 3 + .../use-cluster-creation-restriction.ts | 42 +++++------ .../page-new-feature/page-new-feature.tsx | 70 ++++++++++++------- 4 files changed, 89 insertions(+), 55 deletions(-) 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..d46e450136e 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 @@ -7,12 +7,14 @@ 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 NO_CREDIT_CARD = 'NO_CREDIT_CARD' + export function FreeTrialBanner() { const { organizationId = '' } = useParams() const { pathname } = useLocation() const { data: userSignUp } = useUserSignUp() const hasDxAuth = Boolean(userSignUp?.dx_auth) - const { isInActiveFreeTrial, remainingTrialDays } = useClusterCreationRestriction({ + const { billingDeploymentRestriction, isInActiveFreeTrial, remainingTrialDays } = useClusterCreationRestriction({ organizationId, dxAuth: hasDxAuth, }) @@ -22,18 +24,37 @@ export function FreeTrialBanner() { SETTINGS_URL(organizationId) + SETTINGS_BILLING_SUMMARY_URL ) + const hasRestriction = billingDeploymentRestriction != null + const isNoCreditCardRestriction = billingDeploymentRestriction === NO_CREDIT_CARD + + // 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 || hasDxAuth, + [shouldShowBanner, isOnOrganizationBillingSummaryPage, hasDxAuth] ) if (shouldHideBanner) { return null } + // 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 (NO_CREDIT_CARD or client-side detection) // Add + 1 because Chargebee return 0 when the trial is ending today 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 hasTrialDaysInfo = remainingTrialDays !== undefined && remainingTrialDays > 0 + const message = hasTrialDaysInfo + ? `Your free trial plan expires ${days} ${pluralize(days, 'day')} from now. If you need help, please contact us.` + : 'You are on a free trial. Add a credit card to unlock managed cluster creation. If you need help, please contact us.' 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..a77f56e1cac 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,6 +1,6 @@ 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 @@ -11,50 +11,44 @@ export interface UseClusterCreationRestrictionProps { /** * 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 + * 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 * - * @see https://qovery.slack.com/archives/C02P3MA2NKT/p1768564947277349 + * DX auth users bypass all restrictions. */ export function useClusterCreationRestriction({ organizationId, dxAuth }: UseClusterCreationRestrictionProps) { + const { data: organization, isFetched: isFetchedOrganization } = useOrganization({ organizationId }) const { data: currentCost, isFetched: isFetchedCurrentCost } = useCurrentCost({ 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 + // TODO: Remove cast once qovery-typescript-axios SDK is regenerated with billing_deployment_restriction field + const billingDeploymentRestriction = (organization as { billing_deployment_restriction?: string | null } | undefined) + ?.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 + // DX auth users bypass all restrictions const isClusterCreationRestricted = useMemo( - () => !dxAuth && isInActiveFreeTrial && hasNoCreditCard, - [dxAuth, isInActiveFreeTrial, hasNoCreditCard] + () => !dxAuth && isFetchedOrganization && billingDeploymentRestriction != null, + [dxAuth, isFetchedOrganization, billingDeploymentRestriction] ) - const isLoading = !isFetchedCurrentCost || !isFetchedCreditCards + const isLoading = !isFetchedOrganization || !isFetchedCurrentCost return { isClusterCreationRestricted, 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..8bc965fef80 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 @@ -394,11 +394,13 @@ export function PageNewFeature() { const { data: userSignUp } = useUserSignUp() const hasDxAuth = Boolean(userSignUp?.dx_auth) - const { isClusterCreationRestricted } = useClusterCreationRestriction({ + const { isClusterCreationRestricted, billingDeploymentRestriction } = useClusterCreationRestriction({ organizationId, dxAuth: hasDxAuth, }) + const isNoCreditCardRestriction = billingDeploymentRestriction === 'NO_CREDIT_CARD' + const openInstallationGuideModal = ({ isDemo = false }: { isDemo?: boolean } = {}) => openModal({ options: { @@ -608,38 +610,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) => ( ))} From d719bab1d10a87daddba504d7e6891535720d1e7 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Thu, 19 Feb 2026 09:29:39 +0100 Subject: [PATCH 2/4] refactor: remove dxAuth bypass workaround from billing restriction logic The dxAuth client-side bypass (PR #2396) was a temporary workaround before the backend billing_deployment_restriction solution. Now that restrictions are driven by the backend, the frontend no longer needs to exempt dxAuth users. --- .../src/lib/free-trial-banner/free-trial-banner.tsx | 8 ++------ .../use-cluster-creation-restriction.ts | 10 +++------- .../lib/feature/page-new-feature/page-new-feature.tsx | 5 ----- .../page-organization-billing-summary.tsx | 6 ++---- 4 files changed, 7 insertions(+), 22 deletions(-) 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 d46e450136e..96668b25507 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,6 +1,5 @@ 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' @@ -12,11 +11,8 @@ const NO_CREDIT_CARD = 'NO_CREDIT_CARD' export function FreeTrialBanner() { const { organizationId = '' } = useParams() const { pathname } = useLocation() - const { data: userSignUp } = useUserSignUp() - const hasDxAuth = Boolean(userSignUp?.dx_auth) const { billingDeploymentRestriction, isInActiveFreeTrial, remainingTrialDays } = useClusterCreationRestriction({ organizationId, - dxAuth: hasDxAuth, }) const { showChat } = useSupportChat() @@ -31,8 +27,8 @@ export function FreeTrialBanner() { const shouldShowBanner = hasRestriction || isInActiveFreeTrial const shouldHideBanner = useMemo( - () => !shouldShowBanner || isOnOrganizationBillingSummaryPage || hasDxAuth, - [shouldShowBanner, isOnOrganizationBillingSummaryPage, hasDxAuth] + () => !shouldShowBanner || isOnOrganizationBillingSummaryPage, + [shouldShowBanner, isOnOrganizationBillingSummaryPage] ) if (shouldHideBanner) { 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 a77f56e1cac..72876a29331 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 @@ -4,8 +4,6 @@ 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 } /** @@ -16,9 +14,8 @@ export interface UseClusterCreationRestrictionProps { * - 'NO_CREDIT_CARD' → free trial restriction (blocks managed cluster creation, allows demo) * - any other string → blocks all deployments * - * DX auth users bypass all restrictions. */ -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 }) @@ -36,10 +33,9 @@ export function useClusterCreationRestriction({ organizationId, dxAuth }: UseClu ) // Cluster creation is restricted when the backend sets a billing deployment restriction - // DX auth users bypass all restrictions const isClusterCreationRestricted = useMemo( - () => !dxAuth && isFetchedOrganization && billingDeploymentRestriction != null, - [dxAuth, isFetchedOrganization, billingDeploymentRestriction] + () => isFetchedOrganization && billingDeploymentRestriction != null, + [isFetchedOrganization, billingDeploymentRestriction] ) const isLoading = !isFetchedOrganization || !isFetchedCurrentCost 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 8bc965fef80..3cdbcd100b1 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, billingDeploymentRestriction } = useClusterCreationRestriction({ organizationId, - dxAuth: hasDxAuth, }) const isNoCreditCardRestriction = billingDeploymentRestriction === 'NO_CREDIT_CARD' 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..2902bde7347 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 showErrorCallout = (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(() => { From b546542fd3a7bdc6569637b620f18ad751e2824a Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Thu, 19 Feb 2026 10:16:48 +0100 Subject: [PATCH 3/4] chore: update qovery-typescript-axios to 1.1.832 and remove type cast The SDK now includes the billing_deployment_restriction field on OrganizationResponse, so the manual type cast is no longer needed. --- .../use-cluster-creation-restriction.ts | 4 +--- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 9 deletions(-) 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 72876a29331..b7f0bdd283f 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 @@ -21,9 +21,7 @@ export function useClusterCreationRestriction({ organizationId }: UseClusterCrea const remainingTrialDays = currentCost?.remaining_trial_day - // TODO: Remove cast once qovery-typescript-axios SDK is regenerated with billing_deployment_restriction field - const billingDeploymentRestriction = (organization as { billing_deployment_restriction?: string | null } | undefined) - ?.billing_deployment_restriction + const billingDeploymentRestriction = organization?.billing_deployment_restriction // Check if user is in active free trial (used by free-trial-banner) const isInActiveFreeTrial = useMemo( 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 From c2ddc65cde429d282892aeefcc52f08c4e754df5 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 19 Feb 2026 15:39:08 +0100 Subject: [PATCH 4/4] feat(free-trial): enhance FreeTrialBanner and restriction logic for credit card handling --- .../free-trial-banner/free-trial-banner.tsx | 23 ++++++++++--------- .../use-cluster-creation-restriction.ts | 9 +++++++- .../page-new-feature/page-new-feature.tsx | 4 +--- .../page-organization-billing-summary.tsx | 22 +++++++++--------- 4 files changed, 32 insertions(+), 26 deletions(-) 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 96668b25507..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 @@ -6,12 +6,19 @@ 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 NO_CREDIT_CARD = 'NO_CREDIT_CARD' +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 { billingDeploymentRestriction, isInActiveFreeTrial, remainingTrialDays } = useClusterCreationRestriction({ + const { + isClusterCreationRestricted: hasRestriction, + isNoCreditCardRestriction, + isInActiveFreeTrial, + remainingTrialDays, + hasNoCreditCard, + } = useClusterCreationRestriction({ organizationId, }) const { showChat } = useSupportChat() @@ -20,9 +27,6 @@ export function FreeTrialBanner() { SETTINGS_URL(organizationId) + SETTINGS_BILLING_SUMMARY_URL ) - const hasRestriction = billingDeploymentRestriction != null - const isNoCreditCardRestriction = billingDeploymentRestriction === NO_CREDIT_CARD - // Show the banner when there is any billing restriction or an active free trial const shouldShowBanner = hasRestriction || isInActiveFreeTrial @@ -44,13 +48,10 @@ export function FreeTrialBanner() { ) } - // Free trial (NO_CREDIT_CARD or client-side detection) - // Add + 1 because Chargebee return 0 when the trial is ending today + // 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 hasTrialDaysInfo = remainingTrialDays !== undefined && remainingTrialDays > 0 - const message = hasTrialDaysInfo - ? `Your free trial plan expires ${days} ${pluralize(days, 'day')} from now. If you need help, please contact us.` - : 'You are on a free trial. Add a credit card to unlock managed cluster creation. 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-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 b7f0bdd283f..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,4 +1,5 @@ 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' @@ -13,11 +14,11 @@ export interface UseClusterCreationRestrictionProps { * - 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 }: UseClusterCreationRestrictionProps) { const { data: organization, isFetched: isFetchedOrganization } = useOrganization({ organizationId }) const { data: currentCost, isFetched: isFetchedCurrentCost } = useCurrentCost({ organizationId }) + const { data: creditCards = [], isFetched: isFetchedCreditCards } = useCreditCards({ organizationId }) const remainingTrialDays = currentCost?.remaining_trial_day @@ -36,10 +37,16 @@ export function useClusterCreationRestriction({ organizationId }: UseClusterCrea [isFetchedOrganization, billingDeploymentRestriction] ) + const isNoCreditCardRestriction = billingDeploymentRestriction === 'NO_CREDIT_CARD' + + const hasNoCreditCard = isFetchedCreditCards && creditCards.length === 0 + const isLoading = !isFetchedOrganization || !isFetchedCurrentCost return { isClusterCreationRestricted, + isNoCreditCardRestriction, + hasNoCreditCard, isLoading, isInActiveFreeTrial, billingDeploymentRestriction, 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 3cdbcd100b1..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 @@ -390,12 +390,10 @@ export function PageNewFeature() { const { organizationId = '' } = useParams() useDocumentTitle('Create new cluster - Qovery') const { openModal, closeModal } = useModal() - const { isClusterCreationRestricted, billingDeploymentRestriction } = useClusterCreationRestriction({ + const { isClusterCreationRestricted, isNoCreditCardRestriction } = useClusterCreationRestriction({ organizationId, }) - const isNoCreditCardRestriction = billingDeploymentRestriction === 'NO_CREDIT_CARD' - const openInstallationGuideModal = ({ isDemo = false }: { isDemo?: boolean } = {}) => openModal({ options: { 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 2902bde7347..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 @@ -55,7 +55,7 @@ export function PageOrganizationBillingSummary(props: PageOrganizationBillingSum const billingRecurrence = getBillingRecurrenceStr(props.currentCost?.renewal_at) const remainingTrialDay = props.currentCost?.remaining_trial_day ?? 0 const showTrialCallout = remainingTrialDay !== undefined && remainingTrialDay > 0 && !props.creditCardLoading - const showErrorCallout = (props.hasCreditCard ?? Boolean(props.creditCard)) || userSignUp?.dx_auth + 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(() => { @@ -81,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. )} )}