Add enterprise plan in cloud onboarding (#11100)

Adding a way to switch to enterprise plan during onboarding

<img width="409" alt="Screenshot 2025-03-21 at 17 03 19"
src="https://github.com/user-attachments/assets/7a8c9ebd-3d77-4875-a141-c30fa5119eff"
/>
This commit is contained in:
Félix Malfait
2025-03-21 17:38:13 +01:00
committed by GitHub
parent e624e8deee
commit 07bd2486ca
4 changed files with 63 additions and 17 deletions

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -2386,7 +2386,7 @@ export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePa
export type BillingBaseProductPricesQueryVariables = Exact<{ [key: string]: never; }>; export type BillingBaseProductPricesQueryVariables = Exact<{ [key: string]: never; }>;
export type BillingBaseProductPricesQuery = { __typename?: 'Query', plans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, baseProduct: { __typename?: 'BillingProductDTO', prices: Array<{ __typename?: 'BillingPriceLicensedDTO', unitAmount: number, stripePriceId: string, recurringInterval: SubscriptionInterval } | { __typename?: 'BillingPriceMeteredDTO' }> } }> }; export type BillingBaseProductPricesQuery = { __typename?: 'Query', plans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, baseProduct: { __typename?: 'BillingProductDTO', name: string, prices: Array<{ __typename?: 'BillingPriceLicensedDTO', unitAmount: number, stripePriceId: string, recurringInterval: SubscriptionInterval } | { __typename?: 'BillingPriceMeteredDTO' }> } }> };
export type BillingPortalSessionQueryVariables = Exact<{ export type BillingPortalSessionQueryVariables = Exact<{
returnUrlPath?: InputMaybe<Scalars['String']>; returnUrlPath?: InputMaybe<Scalars['String']>;
@ -3859,6 +3859,7 @@ export const BillingBaseProductPricesDocument = gql`
plans { plans {
planKey planKey
baseProduct { baseProduct {
name
prices { prices {
... on BillingPriceLicensedDTO { ... on BillingPriceLicensedDTO {
unitAmount unitAmount

View File

@ -5,6 +5,7 @@ export const BILLING_BASE_PRODUCT_PRICES = gql`
plans { plans {
planKey planKey
baseProduct { baseProduct {
name
prices { prices {
... on BillingPriceLicensedDTO { ... on BillingPriceLicensedDTO {
unitAmount unitAmount

View File

@ -88,20 +88,43 @@ export const ChooseYourPlan = () => {
const billing = useRecoilValue(billingState); const billing = useRecoilValue(billingState);
const { t } = useLingui(); const { t } = useLingui();
const benefits = [ const [billingCheckoutSession, setBillingCheckoutSession] = useRecoilState(
t`Full access`, billingCheckoutSessionState,
t`Unlimited contacts`, );
t`Email integration`,
t`Custom objects`,
t`API & Webhooks`,
t`1 000 workflow node executions`,
];
const { data: plans } = useBillingBaseProductPricesQuery(); const { data: plans } = useBillingBaseProductPricesQuery();
const currentPlan = billingCheckoutSession.plan || BillingPlanKey.PRO;
const getPlanBenefits = (planKey: BillingPlanKey) => {
if (planKey === BillingPlanKey.ENTERPRISE) {
return [
t`Full access`,
t`Unlimited contacts`,
t`Email integration`,
t`Custom objects`,
t`API & Webhooks`,
t`20 000 workflow node executions`,
t`SSO (SAML / OIDC)`,
];
}
return [
t`Full access`,
t`Unlimited contacts`,
t`Email integration`,
t`Custom objects`,
t`API & Webhooks`,
t`10 000 workflow node executions`,
];
};
const benefits = getPlanBenefits(currentPlan);
const baseProduct = plans?.plans.find( const baseProduct = plans?.plans.find(
(plan) => plan.planKey === BillingPlanKey.PRO, (plan) => plan.planKey === currentPlan,
)?.baseProduct; )?.baseProduct;
const baseProductPrice = baseProduct?.prices.find( const baseProductPrice = baseProduct?.prices.find(
(price): price is BillingPriceLicensedDto => (price): price is BillingPriceLicensedDto =>
isBillingPriceLicensed(price) && isBillingPriceLicensed(price) &&
@ -116,10 +139,6 @@ export const ChooseYourPlan = () => {
(trialPeriod) => trialPeriod.isCreditCardRequired, (trialPeriod) => trialPeriod.isCreditCardRequired,
); );
const [billingCheckoutSession, setBillingCheckoutSession] = useRecoilState(
billingCheckoutSessionState,
);
const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({ const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({
recurringInterval: billingCheckoutSession.interval, recurringInterval: billingCheckoutSession.interval,
plan: billingCheckoutSession.plan, plan: billingCheckoutSession.plan,
@ -133,7 +152,7 @@ export const ChooseYourPlan = () => {
billingCheckoutSession.requirePaymentMethod !== withCreditCard billingCheckoutSession.requirePaymentMethod !== withCreditCard
) { ) {
setBillingCheckoutSession({ setBillingCheckoutSession({
plan: billingCheckoutSession.plan, plan: currentPlan,
interval: baseProductPrice.recurringInterval, interval: baseProductPrice.recurringInterval,
requirePaymentMethod: withCreditCard, requirePaymentMethod: withCreditCard,
}); });
@ -141,10 +160,31 @@ export const ChooseYourPlan = () => {
}; };
}; };
const handleSwitchPlan = (planKey: BillingPlanKey) => {
return () => {
if (isDefined(baseProductPrice)) {
setBillingCheckoutSession({
plan: planKey,
interval: baseProductPrice.recurringInterval,
requirePaymentMethod: billingCheckoutSession.requirePaymentMethod,
});
}
};
};
const { signOut } = useAuth(); const { signOut } = useAuth();
const withCreditCardTrialPeriodDuration = withCreditCardTrialPeriod?.duration; const withCreditCardTrialPeriodDuration = withCreditCardTrialPeriod?.duration;
const alternatePlan =
currentPlan === BillingPlanKey.PRO
? BillingPlanKey.ENTERPRISE
: BillingPlanKey.PRO;
const alternatePlanName = plans?.plans.find(
(plan) => plan.planKey === alternatePlan,
)?.baseProduct.name;
return ( return (
isDefined(baseProductPrice) && isDefined(baseProductPrice) &&
isDefined(billing) && ( isDefined(billing) && (
@ -213,6 +253,10 @@ export const ChooseYourPlan = () => {
<Trans>Log out</Trans> <Trans>Log out</Trans>
</ActionLink> </ActionLink>
<span /> <span />
<ActionLink onClick={handleSwitchPlan(alternatePlan)}>
<Trans>Switch to {alternatePlanName}</Trans>
</ActionLink>
<span />
<ActionLink href={CAL_LINK} target="_blank" rel="noreferrer"> <ActionLink href={CAL_LINK} target="_blank" rel="noreferrer">
<Trans>Book a Call</Trans> <Trans>Book a Call</Trans>
</ActionLink> </ActionLink>

View File

@ -85,7 +85,7 @@ export class BillingPlanService {
if (!baseProduct) { if (!baseProduct) {
throw new BillingException( throw new BillingException(
'Base product not found, did you run the billing:sync-products command?', 'Base product not found, did you run the billing:sync-plans-data command?',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND, BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
); );
} }