40 remove self billing feature flag (#4379)

* Define quantity at checkout

* Remove billing submenu when not isBillingEnabled

* Remove feature flag

* Log warning when missing subscription active workspace add or remove member

* Display subscribe cta for free usage of twenty

* Authorize all settings when subscription canceled or unpaid

* Display subscribe cta for workspace with canceled subscription

* Replace OneToOne by OneToMany

* Add a currentBillingSubscriptionField

* Handle multiple subscriptions by workspace

* Fix redirection

* Fix test

* Fix billingState
This commit is contained in:
martmull
2024-03-12 18:10:27 +01:00
committed by GitHub
parent 4476f5215b
commit 62d414ee66
23 changed files with 292 additions and 247 deletions

View File

@ -1,47 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { billingState } from '@/client-config/states/billingState';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(8)};
`;
export const PlanRequired = () => {
const billing = useRecoilValue(billingState());
const handleButtonClick = () => {
billing?.billingUrl && window.location.replace(billing.billingUrl);
};
useScopedHotkeys('enter', handleButtonClick, PageHotkeyScope.PlanRequired, [
handleButtonClick,
]);
return (
<>
<AnimatedEaseIn>
<Logo />
</AnimatedEaseIn>
<Title>Plan required</Title>
<SubTitle>
Please select a subscription plan before proceeding to sign in.
</SubTitle>
<StyledButtonContainer>
<MainButton
title="Get started"
onClick={handleButtonClick}
width={200}
/>
</StyledButtonContainer>
</>
);
};

View File

@ -1,54 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { PlanRequired } from '../PlanRequired';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/PlanRequired',
component: PlanRequired,
decorators: [PageDecorator],
args: { routePath: AppPath.PlanRequired },
parameters: {
msw: {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: {
...mockedOnboardingUsersData[0],
defaultWorkspace: {
...mockedOnboardingUsersData[0].defaultWorkspace,
subscriptionStatus: 'incomplete',
},
},
},
});
}),
graphqlMocks.handlers,
],
},
},
};
export default meta;
export type Story = StoryObj<typeof PlanRequired>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByRole('button', { name: 'Get started' });
},
};

View File

@ -1,15 +1,13 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus.ts';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState.ts';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus.ts';
import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage.tsx';
import { supportChatState } from '@/client-config/states/supportChatState.ts';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SupportChat } from '@/support/components/SupportChat.tsx';
import { AppPath } from '@/types/AppPath.ts';
import { IconCreditCard, IconCurrencyDollar } from '@/ui/display/icon';
import { Info } from '@/ui/display/info/components/Info.tsx';
import { H1Title } from '@/ui/display/typography/components/H1Title.tsx';
@ -29,9 +27,8 @@ const StyledInvisibleChat = styled.div`
`;
export const SettingsBilling = () => {
const navigate = useNavigate();
const onboardingStatus = useOnboardingStatus();
const supportChat = useRecoilValue(supportChatState());
const currentWorkspace = useRecoilValue(currentWorkspaceState());
const { data, loading } = useBillingPortalSessionQuery({
variables: {
returnUrlPath: '/settings/billing',
@ -45,22 +42,17 @@ export const SettingsBilling = () => {
const displaySubscriptionCanceledInfo =
onboardingStatus === OnboardingStatus.Canceled;
const displaySubscribeInfo =
onboardingStatus === OnboardingStatus.CompletedWithoutSubscription;
const openBillingPortal = () => {
if (isDefined(data)) {
window.location.replace(data.billingPortalSession.url);
}
};
const openChat = () => {
if (isNonEmptyString(supportChat.supportDriver)) {
window.FrontChat?.('show');
} else {
window.location.href =
'mailto:felix@twenty.com?' +
`subject=Subscription Recovery for workspace ${currentWorkspace?.id}&` +
'body=Hey,%0D%0A%0D%0AMy subscription is canceled and I would like to subscribe a new one.' +
'Can you help me?%0D%0A%0D%0ACheers';
}
const redirectToSubscribePage = () => {
navigate(AppPath.PlanRequired);
};
return (
@ -68,14 +60,6 @@ export const SettingsBilling = () => {
<SettingsPageContainer>
<StyledH1Title title="Billing" />
<SettingsBillingCoverImage />
{displaySubscriptionCanceledInfo && (
<Info
text={'Subscription canceled. Please contact us to start a new one'}
buttonTitle={'Contact Us'}
accent={'danger'}
onClick={openChat}
/>
)}
{displayPaymentFailInfo && (
<Info
text={'Last payment failed. Please update your billing details.'}
@ -84,19 +68,37 @@ export const SettingsBilling = () => {
onClick={openBillingPortal}
/>
)}
<Section>
<H2Title
title="Manage your subscription"
description="Edit payment method, see your invoices and more"
{displaySubscriptionCanceledInfo && (
<Info
text={'Subscription canceled. Please start a new one'}
buttonTitle={'Subscribe'}
accent={'danger'}
onClick={redirectToSubscribePage}
/>
<Button
Icon={IconCreditCard}
title="View billing details"
variant="secondary"
onClick={openBillingPortal}
disabled={loading}
)}
{displaySubscribeInfo && (
<Info
text={'Your workspace does not have an active subscription'}
buttonTitle={'Subscribe'}
accent={'danger'}
onClick={redirectToSubscribePage}
/>
</Section>
)}
{!displaySubscribeInfo && (
<Section>
<H2Title
title="Manage your subscription"
description="Edit payment method, see your invoices and more"
/>
<Button
Icon={IconCreditCard}
title="View billing details"
variant="secondary"
onClick={openBillingPortal}
disabled={loading}
/>
</Section>
)}
</SettingsPageContainer>
<StyledInvisibleChat>
<SupportChat />