mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-24 06:48:42 +03:00
43 add billing portal link (#4318)
* Add create billing portal session endpoint * Rename checkout to checkoutSession * Add billig portal query in twenty-front * Add billing menu item * WIP: add menu page * Code review returns * Rename request files * Unwip: add menu page * Add billing cover image * Fix icon imports * Rename parameter * Add feature flag soon pill
This commit is contained in:
parent
9fc421876f
commit
0b889ef089
@ -40,6 +40,7 @@ import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/we
|
||||
import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew';
|
||||
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
|
||||
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
|
||||
import { SettingsBilling } from '~/pages/settings/SettingsBilling.tsx';
|
||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||
@ -114,6 +115,10 @@ export const App = () => {
|
||||
path={SettingsPath.AccountsEmailsInboxSettings}
|
||||
element={<SettingsAccountsEmailsInboxSettings />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.Billing}
|
||||
element={<SettingsBilling />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.WorkspaceMembersPage}
|
||||
element={<SettingsWorkspaceMembers />}
|
||||
|
@ -889,6 +889,13 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{
|
||||
|
||||
export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
|
||||
|
||||
export type BillingPortalSessionQueryVariables = Exact<{
|
||||
returnUrlPath?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url: string } };
|
||||
|
||||
export type CheckoutSessionMutationVariables = Exact<{
|
||||
recurringInterval: Scalars['String'];
|
||||
successUrlPath?: InputMaybe<Scalars['String']>;
|
||||
@ -1588,6 +1595,41 @@ export function useValidatePasswordResetTokenLazyQuery(baseOptions?: Apollo.Lazy
|
||||
export type ValidatePasswordResetTokenQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenQuery>;
|
||||
export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenLazyQuery>;
|
||||
export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult<ValidatePasswordResetTokenQuery, ValidatePasswordResetTokenQueryVariables>;
|
||||
export const BillingPortalSessionDocument = gql`
|
||||
query BillingPortalSession($returnUrlPath: String) {
|
||||
billingPortalSession(returnUrlPath: $returnUrlPath) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useBillingPortalSessionQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useBillingPortalSessionQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useBillingPortalSessionQuery` 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 } = useBillingPortalSessionQuery({
|
||||
* variables: {
|
||||
* returnUrlPath: // value for 'returnUrlPath'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useBillingPortalSessionQuery(baseOptions?: Apollo.QueryHookOptions<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>(BillingPortalSessionDocument, options);
|
||||
}
|
||||
export function useBillingPortalSessionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>(BillingPortalSessionDocument, options);
|
||||
}
|
||||
export type BillingPortalSessionQueryHookResult = ReturnType<typeof useBillingPortalSessionQuery>;
|
||||
export type BillingPortalSessionLazyQueryHookResult = ReturnType<typeof useBillingPortalSessionLazyQuery>;
|
||||
export type BillingPortalSessionQueryResult = Apollo.QueryResult<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>;
|
||||
export const CheckoutSessionDocument = gql`
|
||||
mutation CheckoutSession($recurringInterval: String!, $successUrlPath: String) {
|
||||
checkoutSession(
|
||||
|
BIN
packages/twenty-front/src/modules/billing/assets/cover-dark.png
Normal file
BIN
packages/twenty-front/src/modules/billing/assets/cover-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 KiB |
BIN
packages/twenty-front/src/modules/billing/assets/cover-light.png
Normal file
BIN
packages/twenty-front/src/modules/billing/assets/cover-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 365 KiB |
@ -0,0 +1,24 @@
|
||||
import { IconCreditCard } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { useBillingPortalSessionQuery } from '~/generated/graphql.tsx';
|
||||
export const ManageYourSubscription = () => {
|
||||
const { data, loading } = useBillingPortalSessionQuery({
|
||||
variables: {
|
||||
returnUrlPath: '/settings/billing',
|
||||
},
|
||||
});
|
||||
const handleButtonClick = () => {
|
||||
if (data) {
|
||||
window.location.replace(data.billingPortalSession.url);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
Icon={IconCreditCard}
|
||||
title="View billing details"
|
||||
variant="secondary"
|
||||
onClick={handleButtonClick}
|
||||
disabled={loading}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
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 />;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const BILLING_PORTAL_SESSION = gql`
|
||||
query BillingPortalSession($returnUrlPath: String) {
|
||||
billingPortalSession(returnUrlPath: $returnUrlPath) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
@ -11,6 +11,7 @@ import {
|
||||
IconCalendarEvent,
|
||||
IconCode,
|
||||
IconColorSwatch,
|
||||
IconCurrencyDollar,
|
||||
IconDoorEnter,
|
||||
IconHierarchy2,
|
||||
IconMail,
|
||||
@ -34,6 +35,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
}, [signOut, navigate]);
|
||||
|
||||
const isCalendarEnabled = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
|
||||
const isSelfBillingEnabled = useIsFeatureEnabled('IS_SELF_BILLING_ENABLED');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -86,6 +88,12 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
path={SettingsPath.WorkspaceMembersPage}
|
||||
Icon={IconUsers}
|
||||
/>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Billing"
|
||||
path={SettingsPath.Billing}
|
||||
Icon={IconCurrencyDollar}
|
||||
soon={!isSelfBillingEnabled}
|
||||
/>
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Data model"
|
||||
path={SettingsPath.Objects}
|
||||
|
@ -7,6 +7,7 @@ export enum SettingsPath {
|
||||
AccountsCalendarsSettings = 'accounts/calendars/:accountUuid',
|
||||
AccountsEmails = 'accounts/emails',
|
||||
AccountsEmailsInboxSettings = 'accounts/emails/:accountUuid',
|
||||
Billing = 'billing',
|
||||
Objects = 'objects',
|
||||
ObjectDetail = 'objects/:objectSlug',
|
||||
ObjectEdit = 'objects/:objectSlug/edit',
|
||||
|
@ -42,6 +42,7 @@ export {
|
||||
IconColorSwatch,
|
||||
IconMessageCircle as IconComment,
|
||||
IconCopy,
|
||||
IconCreditCard,
|
||||
IconCurrencyDollar,
|
||||
IconCurrencyEuro,
|
||||
IconCurrencyFrank,
|
||||
|
30
packages/twenty-front/src/pages/settings/SettingsBilling.tsx
Normal file
30
packages/twenty-front/src/pages/settings/SettingsBilling.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ManageYourSubscription } from '@/billing/components/ManageYourSubscription.tsx';
|
||||
import { SettingsBillingCoverImage } from '@/billing/components/SettingsBillingCoverImage.tsx';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { IconCurrencyDollar } from '@/ui/display/icon';
|
||||
import { H1Title } from '@/ui/display/typography/components/H1Title.tsx';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title.tsx';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section.tsx';
|
||||
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export const SettingsBilling = () => (
|
||||
<SubMenuTopBarContainer Icon={IconCurrencyDollar} title="Billing">
|
||||
<SettingsPageContainer>
|
||||
<StyledH1Title title="Billing" />
|
||||
<SettingsBillingCoverImage />
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Manage your subscription"
|
||||
description="Edit payment method, see your invoices and more"
|
||||
/>
|
||||
<ManageYourSubscription />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
@ -111,9 +111,14 @@ export class BillingService {
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
const frontBaseUrl = this.environmentService.getFrontBaseUrl();
|
||||
const returnUrl = returnUrlPath
|
||||
? frontBaseUrl + returnUrlPath
|
||||
: frontBaseUrl;
|
||||
|
||||
const session = await this.stripeService.createBillingPortalSession(
|
||||
billingSubscription.stripeCustomerId,
|
||||
returnUrlPath,
|
||||
returnUrl,
|
||||
);
|
||||
|
||||
assert(session.url, 'Error: missing billingPortal.session.url');
|
||||
|
@ -44,11 +44,11 @@ export class StripeService {
|
||||
|
||||
async createBillingPortalSession(
|
||||
stripeCustomerId: string,
|
||||
returnUrlPath?: string,
|
||||
returnUrl?: string,
|
||||
): Promise<Stripe.BillingPortal.Session> {
|
||||
return await this.stripe.billingPortal.sessions.create({
|
||||
customer: stripeCustomerId,
|
||||
return_url: returnUrlPath ?? this.environmentService.getFrontBaseUrl(),
|
||||
return_url: returnUrl ?? this.environmentService.getFrontBaseUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user