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