add trial period ending banner + server logic (#11389)
Solution : - if user reaches his workflow usage cap + in trial period > display banner - end trial period if user has payment method (if not, not possible) <img width="941" alt="Screenshot 2025-04-04 at 10 27 32" src="https://github.com/user-attachments/assets/d7a1d5f7-9b12-4a92-a7c7-15ef8847c790" />
This commit is contained in:
@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const END_SUBSCRIPTION_TRIAL_PERIOD = gql`
|
||||
mutation EndSubscriptionTrialPeriod {
|
||||
endSubscriptionTrialPeriod {
|
||||
status
|
||||
hasPaymentMethod
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,71 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useEndSubscriptionTrialPeriodMutation } from '~/generated/graphql';
|
||||
|
||||
export const useEndSubscriptionTrialPeriod = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [endSubscriptionTrialPeriod] = useEndSubscriptionTrialPeriodMutation();
|
||||
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
|
||||
currentWorkspaceState,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const endTrialPeriod = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const { data } = await endSubscriptionTrialPeriod();
|
||||
const endTrialPeriodOutput = data?.endSubscriptionTrialPeriod;
|
||||
|
||||
const hasPaymentMethod = endTrialPeriodOutput?.hasPaymentMethod;
|
||||
|
||||
if (isDefined(hasPaymentMethod) && hasPaymentMethod === false) {
|
||||
enqueueSnackBar(
|
||||
t`No payment method found. Please update your billing details.`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSubscriptionStatus = endTrialPeriodOutput?.status;
|
||||
if (
|
||||
isDefined(updatedSubscriptionStatus) &&
|
||||
isDefined(currentWorkspace?.currentBillingSubscription)
|
||||
) {
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
currentBillingSubscription: {
|
||||
...currentWorkspace?.currentBillingSubscription,
|
||||
status: updatedSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
enqueueSnackBar(t`Subscription activated.`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} catch {
|
||||
enqueueSnackBar(
|
||||
t`Error while ending trial period. Please contact Twenty team.`,
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
endTrialPeriod,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,10 @@
|
||||
import { InformationBannerBillingSubscriptionPaused } from '@/information-banner/components/billing/InformationBannerBillingSubscriptionPaused';
|
||||
import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod';
|
||||
import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo';
|
||||
import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription';
|
||||
import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases';
|
||||
import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions';
|
||||
import { useIsSomeMeteredProductCapReached } from '@/workspace/hooks/useIsSomeMeteredProductCapReached';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
@ -24,6 +26,7 @@ export const InformationBannerWrapper = () => {
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
const isSomeMeteredProductCapReached = useIsSomeMeteredProductCapReached();
|
||||
|
||||
const displayBillingSubscriptionPausedBanner =
|
||||
isWorkspaceSuspended && subscriptionStatus === SubscriptionStatus.Paused;
|
||||
@ -35,6 +38,10 @@ export const InformationBannerWrapper = () => {
|
||||
subscriptionStatus === SubscriptionStatus.PastDue ||
|
||||
subscriptionStatus === SubscriptionStatus.Unpaid;
|
||||
|
||||
const displayEndTrialPeriodBanner =
|
||||
isSomeMeteredProductCapReached &&
|
||||
subscriptionStatus === SubscriptionStatus.Trialing;
|
||||
|
||||
return (
|
||||
<StyledInformationBannerWrapper>
|
||||
<InformationBannerReconnectAccountInsufficientPermissions />
|
||||
@ -46,6 +53,7 @@ export const InformationBannerWrapper = () => {
|
||||
<InformationBannerNoBillingSubscription />
|
||||
)}
|
||||
{displayFailPaymentInfoBanner && <InformationBannerFailPaymentInfo />}
|
||||
{displayEndTrialPeriodBanner && <InformationBannerEndTrialPeriod />}
|
||||
</StyledInformationBannerWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const InformationBannerEndTrialPeriod = () => {
|
||||
const { endTrialPeriod, isLoading } = useEndSubscriptionTrialPeriod();
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<InformationBanner
|
||||
variant="danger"
|
||||
message={t`No free workflow executions left. End trial period and activate your billing to continue.`}
|
||||
buttonTitle={t`Activate`}
|
||||
buttonOnClick={async () => await endTrialPeriod()}
|
||||
isButtonDisabled={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -53,6 +53,19 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
id
|
||||
status
|
||||
interval
|
||||
billingSubscriptionItems {
|
||||
id
|
||||
hasReachedCurrentPeriodCap
|
||||
billingProduct {
|
||||
name
|
||||
description
|
||||
metadata {
|
||||
planKey
|
||||
priceUsageBased
|
||||
productKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
billingSubscriptions {
|
||||
id
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { BillingProductKey } from '~/generated/graphql';
|
||||
|
||||
export const useIsSomeMeteredProductCapReached = (): boolean => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const meteredProductKeys = Object.values(BillingProductKey).filter(
|
||||
(productKey) => productKey !== BillingProductKey.BASE_PRODUCT,
|
||||
);
|
||||
|
||||
return meteredProductKeys.some((productKey) => {
|
||||
return (
|
||||
currentWorkspace?.currentBillingSubscription?.billingSubscriptionItems?.find(
|
||||
(item) => item.billingProduct?.metadata.productKey === productKey,
|
||||
)?.hasReachedCurrentPeriodCap ?? false
|
||||
);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user