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

@ -145,6 +145,17 @@ export type BillingEndTrialPeriodOutput = {
status?: Maybe<SubscriptionStatus>;
};
export type BillingMeteredProductUsageOutput = {
__typename?: 'BillingMeteredProductUsageOutput';
includedFreeQuantity: Scalars['Float'];
periodEnd: Scalars['DateTime'];
periodStart: Scalars['DateTime'];
productKey: BillingProductKey;
totalCostCents: Scalars['Float'];
unitPriceCents: Scalars['Float'];
usageQuantity: Scalars['Float'];
};
/** The different billing plans available */
export enum BillingPlanKey {
ENTERPRISE = 'ENTERPRISE',
@ -876,8 +887,8 @@ export type Mutation = {
signUpInNewWorkspace: SignUpOutput;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
submitFormStep: Scalars['Boolean'];
switchToYearlyInterval: BillingUpdateOutput;
track: Analytics;
updateBillingSubscription: BillingUpdateOutput;
updateLabPublicFeatureFlag: FeatureFlag;
updateOneField: Field;
updateOneObject: Object;
@ -1419,6 +1430,7 @@ export type Query = {
getAvailablePackages: Scalars['JSON'];
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
getIndicatorHealthStatus: AdminPanelHealthServiceData;
getMeteredProductsUsage: Array<BillingMeteredProductUsageOutput>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getQueueMetrics: QueueMetricsData;
@ -2566,10 +2578,15 @@ export type EndSubscriptionTrialPeriodMutationVariables = Exact<{ [key: string]:
export type EndSubscriptionTrialPeriodMutation = { __typename?: 'Mutation', endSubscriptionTrialPeriod: { __typename?: 'BillingEndTrialPeriodOutput', status?: SubscriptionStatus | null, hasPaymentMethod: boolean } };
export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>;
export type GetMeteredProductsUsageQueryVariables = Exact<{ [key: string]: never; }>;
export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updateBillingSubscription: { __typename?: 'BillingUpdateOutput', success: boolean } };
export type GetMeteredProductsUsageQuery = { __typename?: 'Query', getMeteredProductsUsage: Array<{ __typename?: 'BillingMeteredProductUsageOutput', productKey: BillingProductKey, usageQuantity: number, includedFreeQuantity: number, unitPriceCents: number, totalCostCents: number }> };
export type SwitchSubscriptionToYearlyIntervalMutationVariables = Exact<{ [key: string]: never; }>;
export type SwitchSubscriptionToYearlyIntervalMutation = { __typename?: 'Mutation', switchToYearlyInterval: { __typename?: 'BillingUpdateOutput', success: boolean } };
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
@ -4226,38 +4243,76 @@ export function useEndSubscriptionTrialPeriodMutation(baseOptions?: Apollo.Mutat
export type EndSubscriptionTrialPeriodMutationHookResult = ReturnType<typeof useEndSubscriptionTrialPeriodMutation>;
export type EndSubscriptionTrialPeriodMutationResult = Apollo.MutationResult<EndSubscriptionTrialPeriodMutation>;
export type EndSubscriptionTrialPeriodMutationOptions = Apollo.BaseMutationOptions<EndSubscriptionTrialPeriodMutation, EndSubscriptionTrialPeriodMutationVariables>;
export const UpdateBillingSubscriptionDocument = gql`
mutation UpdateBillingSubscription {
updateBillingSubscription {
export const GetMeteredProductsUsageDocument = gql`
query GetMeteredProductsUsage {
getMeteredProductsUsage {
productKey
usageQuantity
includedFreeQuantity
unitPriceCents
totalCostCents
}
}
`;
/**
* __useGetMeteredProductsUsageQuery__
*
* To run a query within a React component, call `useGetMeteredProductsUsageQuery` and pass it any options that fit your needs.
* When your component renders, `useGetMeteredProductsUsageQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetMeteredProductsUsageQuery({
* variables: {
* },
* });
*/
export function useGetMeteredProductsUsageQuery(baseOptions?: Apollo.QueryHookOptions<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>(GetMeteredProductsUsageDocument, options);
}
export function useGetMeteredProductsUsageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>(GetMeteredProductsUsageDocument, options);
}
export type GetMeteredProductsUsageQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageQuery>;
export type GetMeteredProductsUsageLazyQueryHookResult = ReturnType<typeof useGetMeteredProductsUsageLazyQuery>;
export type GetMeteredProductsUsageQueryResult = Apollo.QueryResult<GetMeteredProductsUsageQuery, GetMeteredProductsUsageQueryVariables>;
export const SwitchSubscriptionToYearlyIntervalDocument = gql`
mutation SwitchSubscriptionToYearlyInterval {
switchToYearlyInterval {
success
}
}
`;
export type UpdateBillingSubscriptionMutationFn = Apollo.MutationFunction<UpdateBillingSubscriptionMutation, UpdateBillingSubscriptionMutationVariables>;
export type SwitchSubscriptionToYearlyIntervalMutationFn = Apollo.MutationFunction<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
/**
* __useUpdateBillingSubscriptionMutation__
* __useSwitchSubscriptionToYearlyIntervalMutation__
*
* To run a mutation, you first call `useUpdateBillingSubscriptionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateBillingSubscriptionMutation` returns a tuple that includes:
* To run a mutation, you first call `useSwitchSubscriptionToYearlyIntervalMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSwitchSubscriptionToYearlyIntervalMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateBillingSubscriptionMutation, { data, loading, error }] = useUpdateBillingSubscriptionMutation({
* const [switchSubscriptionToYearlyIntervalMutation, { data, loading, error }] = useSwitchSubscriptionToYearlyIntervalMutation({
* variables: {
* },
* });
*/
export function useUpdateBillingSubscriptionMutation(baseOptions?: Apollo.MutationHookOptions<UpdateBillingSubscriptionMutation, UpdateBillingSubscriptionMutationVariables>) {
export function useSwitchSubscriptionToYearlyIntervalMutation(baseOptions?: Apollo.MutationHookOptions<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateBillingSubscriptionMutation, UpdateBillingSubscriptionMutationVariables>(UpdateBillingSubscriptionDocument, options);
return Apollo.useMutation<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>(SwitchSubscriptionToYearlyIntervalDocument, options);
}
export type UpdateBillingSubscriptionMutationHookResult = ReturnType<typeof useUpdateBillingSubscriptionMutation>;
export type UpdateBillingSubscriptionMutationResult = Apollo.MutationResult<UpdateBillingSubscriptionMutation>;
export type UpdateBillingSubscriptionMutationOptions = Apollo.BaseMutationOptions<UpdateBillingSubscriptionMutation, UpdateBillingSubscriptionMutationVariables>;
export type SwitchSubscriptionToYearlyIntervalMutationHookResult = ReturnType<typeof useSwitchSubscriptionToYearlyIntervalMutation>;
export type SwitchSubscriptionToYearlyIntervalMutationResult = Apollo.MutationResult<SwitchSubscriptionToYearlyIntervalMutation>;
export type SwitchSubscriptionToYearlyIntervalMutationOptions = Apollo.BaseMutationOptions<SwitchSubscriptionToYearlyIntervalMutation, SwitchSubscriptionToYearlyIntervalMutationVariables>;
export const GetClientConfigDocument = gql`
query GetClientConfig {
clientConfig {

View File

@ -1,22 +0,0 @@
import styled from '@emotion/styled';
import DarkCoverImage from '@/billing/assets/cover-dark.png';
import LightCoverImage from '@/billing/assets/cover-light.png';
const StyledCoverImageContainer = styled.div`
align-items: center;
background-image: ${({ theme }) =>
theme.name === 'light'
? `url('${LightCoverImage.toString()}')`
: `url('${DarkCoverImage.toString()}')`};
background-size: contain;
background-repeat: no-repeat;
box-sizing: border-box;
display: flex;
height: 162px;
justify-content: center;
position: relative;
`;
export const SettingsBillingCoverImage = () => {
return <StyledCoverImageContainer />;
};

View File

@ -0,0 +1,40 @@
import styled from '@emotion/styled';
type SettingsBillingLabelValueItemProps = {
label: string;
value: string;
isValueInPrimaryColor?: boolean;
};
const StyledContainer = styled.div`
display: flex;
justify-content: space-between;
`;
const StyledLabelSpan = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledValueSpan = styled.span<{ isPrimaryColor: boolean }>`
color: ${({ theme, isPrimaryColor }) =>
isPrimaryColor ? theme.font.color.primary : theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
export const SettingsBillingLabelValueItem = ({
label,
value,
isValueInPrimaryColor = false,
}: SettingsBillingLabelValueItemProps) => {
return (
<StyledContainer>
<StyledLabelSpan>{label}</StyledLabelSpan>
<StyledValueSpan isPrimaryColor={isValueInPrimaryColor}>
{value}
</StyledValueSpan>
</StyledContainer>
);
};

View File

@ -0,0 +1,94 @@
import { SettingsBillingLabelValueItem } from '@/billing/components/SettingsBillingLabelValueItem';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { H2Title } from 'twenty-ui/display';
import { ProgressBar } from 'twenty-ui/feedback';
import { Section } from 'twenty-ui/layout';
import { BACKGROUND_LIGHT, COLOR } from 'twenty-ui/theme';
import { SubscriptionStatus } from '~/generated/graphql';
import { formatAmount } from '~/utils/format/formatAmount';
import { formatNumber } from '~/utils/format/number';
const StyledMonthlyCreditsContainer = styled.div`
border-radius: ${({ theme }) => theme.border.radius.md};
border: 1px solid ${({ theme }) => theme.border.color.medium};
background-color: ${({ theme }) => theme.background.secondary};
padding: ${({ theme }) => theme.spacing(3)};
width: 100%;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
`;
const StyledLineSeparator = styled.div`
width: 100%;
height: 1px;
background-color: ${({ theme }) => theme.background.tertiary};
`;
export const SettingsBillingMonthlyCreditsSection = () => {
const subscriptionStatus = useSubscriptionStatus();
const isTrialing = subscriptionStatus === SubscriptionStatus.Trialing;
const {
freeUsageQuantity,
includedFreeQuantity,
paidUsageQuantity,
unitPriceCents,
totalCostCents,
} = useGetWorkflowNodeExecutionUsage();
const progressBarValue =
freeUsageQuantity === includedFreeQuantity
? 0
: (freeUsageQuantity / includedFreeQuantity) * 100;
const formattedFreeUsageQuantity =
freeUsageQuantity === includedFreeQuantity
? formatAmount(freeUsageQuantity)
: formatNumber(freeUsageQuantity);
return (
<Section>
<H2Title
title={t`Monthly Credits`}
description={t`Track your monthly workflow credit consumption.`}
/>
<StyledMonthlyCreditsContainer>
<SettingsBillingLabelValueItem
label={t`Free Credits Used`}
value={`${formattedFreeUsageQuantity}/${formatAmount(includedFreeQuantity)}`}
/>
<ProgressBar
value={progressBarValue}
barColor={COLOR.blue}
backgroundColor={BACKGROUND_LIGHT.tertiary}
withBorderRadius={true}
/>
<StyledLineSeparator />
{!isTrialing && (
<SettingsBillingLabelValueItem
label={t`Extra Credits Used`}
value={`${formatNumber(paidUsageQuantity)}`}
/>
)}
<SettingsBillingLabelValueItem
label={t`Cost per 1k Extra Credits`}
value={`$${formatNumber((unitPriceCents / 100) * 1000, 2)}`}
/>
{!isTrialing && (
<SettingsBillingLabelValueItem
label={t`Cost`}
isValueInPrimaryColor={true}
value={`$${formatNumber(totalCostCents / 100, 2)}`}
/>
)}
</StyledMonthlyCreditsContainer>
</Section>
);
};

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_METERED_PRODUCTS_USAGE = gql`
query GetMeteredProductsUsage {
getMeteredProductsUsage {
productKey
usageQuantity
includedFreeQuantity
unitPriceCents
totalCostCents
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const SWITCH_SUBSCRIPTION_TO_YEARLY_INTERVAL = gql`
mutation SwitchSubscriptionToYearlyInterval {
switchToYearlyInterval {
success
}
}
`;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const UPDATE_BILLING_SUBSCRIPTION = gql`
mutation UpdateBillingSubscription {
updateBillingSubscription {
success
}
}
`;

View File

@ -0,0 +1,39 @@
import {
BillingProductKey,
useGetMeteredProductsUsageQuery,
} from '~/generated/graphql';
export const useGetWorkflowNodeExecutionUsage = () => {
const { data, loading } = useGetMeteredProductsUsageQuery();
const workflowUsage = data?.getMeteredProductsUsage.find(
(productUsage) =>
productUsage.productKey === BillingProductKey.WORKFLOW_NODE_EXECUTION,
);
if (loading === true || !workflowUsage) {
return {
usageQuantity: 0,
freeUsageQuantity: 0,
includedFreeQuantity: 0,
paidUsageQuantity: 0,
unitPriceCents: 0,
totalCostCents: 0,
};
}
return {
usageQuantity: workflowUsage.usageQuantity,
freeUsageQuantity:
workflowUsage.usageQuantity > workflowUsage.includedFreeQuantity
? workflowUsage.includedFreeQuantity
: workflowUsage.usageQuantity,
includedFreeQuantity: workflowUsage.includedFreeQuantity,
paidUsageQuantity:
workflowUsage.usageQuantity > workflowUsage.includedFreeQuantity
? workflowUsage.usageQuantity - workflowUsage.includedFreeQuantity
: 0,
unitPriceCents: workflowUsage.unitPriceCents,
totalCostCents: workflowUsage.totalCostCents,
};
};

View File

@ -11,9 +11,9 @@ import {
IconSquareRoundedCheck,
IconX,
} from 'twenty-ui/display';
import { ProgressBar, useProgressAnimation } from 'twenty-ui/feedback';
import { LightButton, LightIconButton } from 'twenty-ui/input';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { ProgressBar, useProgressAnimation } from 'twenty-ui/feedback';
export enum SnackBarVariant {
Default = 'default',
@ -205,7 +205,7 @@ export const SnackBar = ({
{...{ className, id, role, variant }}
>
<StyledProgressBar
color={theme.snackBar[variant].backgroundColor}
barColor={theme.snackBar[variant].backgroundColor}
value={progressValue}
/>
<StyledHeader>

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>