add metered products usage (#11452)

- add metered products usage module on settings/billing page
- add new resolver + logic with meter event data fetching from Stripe

<img width="590" alt="Screenshot 2025-04-08 at 16 34 07"
src="https://github.com/user-attachments/assets/34327af1-3482-4d61-91a6-e2dbaeb017ab"
/>
<img width="570" alt="Screenshot 2025-04-08 at 16 31 58"
src="https://github.com/user-attachments/assets/55aa221a-925f-48bf-88c4-f20713c79962"
/>

- bonus : disable subscription switch from yearly to monthly

closes https://github.com/twentyhq/core-team-issues/issues/681
This commit is contained in:
Etienne
2025-04-09 11:26:49 +02:00
committed by GitHub
parent b25ee28c12
commit 11fb8e0284
23 changed files with 570 additions and 139 deletions

View File

@ -104,7 +104,7 @@ export const ChooseYourPlan = () => {
t`Email integration`,
t`Custom objects`,
t`API & Webhooks`,
t`20 000 workflow node executions`,
t`20,000 workflow node executions`,
t`SSO (SAML / OIDC)`,
];
}
@ -115,7 +115,7 @@ export const ChooseYourPlan = () => {
t`Email integration`,
t`Custom objects`,
t`API & Webhooks`,
t`10 000 workflow node executions`,
t`10,000 workflow node executions`,
];
};

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage';
import { SettingsBillingMonthlyCreditsSection } from '@/billing/components/SettingsBillingMonthlyCreditsSection';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
@ -11,55 +11,31 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import {
SubscriptionInterval,
SubscriptionStatus,
useBillingPortalSessionQuery,
useUpdateBillingSubscriptionMutation,
} from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { isDefined } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
import {
H2Title,
IconCalendarEvent,
IconCircleX,
IconCreditCard,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
type SwitchInfo = {
newInterval: SubscriptionInterval;
to: string;
from: string;
impact: string;
};
import {
FeatureFlagKey,
SubscriptionInterval,
SubscriptionStatus,
useBillingPortalSessionQuery,
useSwitchSubscriptionToYearlyIntervalMutation,
} from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsBilling = () => {
const { t } = useLingui();
const { redirect } = useRedirect();
const MONTHLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Year,
to: t`to yearly`,
from: t`from monthly to yearly`,
impact: t`You will be charged immediately for the full year.`,
};
const YEARLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Month,
to: t`to monthly`,
from: t`from yearly to monthly`,
impact: t`Your credit balance will be used to pay the monthly bills.`,
};
const SWITCH_INFOS = {
year: YEARLY_SWITCH_INFO,
month: MONTHLY_SWITCH_INFO,
};
const { enqueueSnackBar } = useSnackBar();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
@ -72,14 +48,11 @@ export const SettingsBilling = () => {
subscriptionStatus !== SubscriptionStatus.Canceled;
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const switchingInfo =
currentWorkspace?.currentBillingSubscription?.interval ===
SubscriptionInterval.Year
? SWITCH_INFOS.year
: SWITCH_INFOS.month;
const [isSwitchingIntervalModalOpen, setIsSwitchingIntervalModalOpen] =
useState(false);
const [updateBillingSubscription] = useUpdateBillingSubscriptionMutation();
const [switchToYearlyInterval] =
useSwitchSubscriptionToYearlyIntervalMutation();
const { data, loading } = useBillingPortalSessionQuery({
variables: {
returnUrlPath: '/settings/billing',
@ -100,33 +73,33 @@ export const SettingsBilling = () => {
setIsSwitchingIntervalModalOpen(true);
};
const from = switchingInfo.from;
const to = switchingInfo.to;
const impact = switchingInfo.impact;
const switchInterval = async () => {
try {
await updateBillingSubscription();
await switchToYearlyInterval();
if (isDefined(currentWorkspace?.currentBillingSubscription)) {
const newCurrentWorkspace = {
...currentWorkspace,
currentBillingSubscription: {
...currentWorkspace?.currentBillingSubscription,
interval: switchingInfo.newInterval,
interval: SubscriptionInterval.Year,
},
};
setCurrentWorkspace(newCurrentWorkspace);
}
enqueueSnackBar(t`Subscription has been switched ${to}`, {
enqueueSnackBar(t`Subscription has been switched to yearly.`, {
variant: SnackBarVariant.Success,
});
} catch (error: any) {
enqueueSnackBar(t`Error while switching subscription ${to}.`, {
enqueueSnackBar(t`Error while switching subscription to yearly.`, {
variant: SnackBarVariant.Error,
});
}
};
const isMeteredProductBillingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsMeteredProductBillingEnabled,
);
return (
<SubMenuTopBarContainer
title={t`Billing`}
@ -139,7 +112,9 @@ export const SettingsBilling = () => {
]}
>
<SettingsPageContainer>
<SettingsBillingCoverImage />
{isMeteredProductBillingEnabled && (
<SettingsBillingMonthlyCreditsSection />
)}
<Section>
<H2Title
title={t`Manage your subscription`}
@ -153,19 +128,22 @@ export const SettingsBilling = () => {
disabled={billingPortalButtonDisabled}
/>
</Section>
<Section>
<H2Title
title={t`Edit billing interval`}
description={t`Switch ${from}`}
/>
<Button
Icon={IconCalendarEvent}
title={t`Switch ${to}`}
variant="secondary"
onClick={openSwitchingIntervalModal}
disabled={!hasNotCanceledCurrentSubscription}
/>
</Section>
{currentWorkspace?.currentBillingSubscription?.interval ===
SubscriptionInterval.Month && (
<Section>
<H2Title
title={t`Edit billing interval`}
description={t`Switch from monthly to yearly`}
/>
<Button
Icon={IconCalendarEvent}
title={t`Switch to yearly`}
variant="secondary"
onClick={openSwitchingIntervalModal}
disabled={!hasNotCanceledCurrentSubscription}
/>
</Section>
)}
<Section>
<H2Title
title={t`Cancel your subscription`}
@ -184,13 +162,10 @@ export const SettingsBilling = () => {
<ConfirmationModal
isOpen={isSwitchingIntervalModalOpen}
setIsOpen={setIsSwitchingIntervalModalOpen}
title={t`Switch billing ${to}`}
subtitle={
t`Are you sure that you want to change your billing interval?` +
` ${impact}`
}
title={t`Switch billing to yearly`}
subtitle={t`Are you sure that you want to change your billing interval? You will be charged immediately for the full year.`}
onConfirmClick={switchInterval}
confirmButtonText={t`Change ${to}`}
confirmButtonText={t`Change to yearly`}
confirmButtonAccent={'blue'}
/>
</SubMenuTopBarContainer>