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:
@ -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`,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user