mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-25 13:02:15 +03:00
42 add billing portal endpoint (#4315)
* Add create billing portal session endpoint * Rename checkout to checkoutSession * Code review returns
This commit is contained in:
parent
1f00af286b
commit
28a093d495
@ -70,11 +70,6 @@ export type BooleanFieldComparison = {
|
||||
isNot?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type CheckoutEntity = {
|
||||
__typename?: 'CheckoutEntity';
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
__typename?: 'ClientConfig';
|
||||
authProviders: AuthProviders;
|
||||
@ -226,7 +221,7 @@ export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkspace: Workspace;
|
||||
challenge: LoginToken;
|
||||
checkout: CheckoutEntity;
|
||||
checkoutSession: SessionEntity;
|
||||
createEvent: Analytics;
|
||||
createOneObject: Object;
|
||||
createOneRefreshToken: RefreshToken;
|
||||
@ -262,7 +257,7 @@ export type MutationChallengeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCheckoutArgs = {
|
||||
export type MutationCheckoutSessionArgs = {
|
||||
recurringInterval: Scalars['String'];
|
||||
successUrlPath?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@ -397,6 +392,7 @@ export type ProductPricesEntity = {
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
billingPortalSession: SessionEntity;
|
||||
checkUserExists: UserExists;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
@ -412,6 +408,11 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryBillingPortalSessionArgs = {
|
||||
returnUrlPath?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryCheckUserExistsArgs = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
@ -500,6 +501,11 @@ export type Sentry = {
|
||||
dsn?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SessionEntity = {
|
||||
__typename?: 'SessionEntity';
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Sort Directions */
|
||||
export enum SortDirection {
|
||||
Asc = 'ASC',
|
||||
@ -883,13 +889,13 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{
|
||||
|
||||
export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
|
||||
|
||||
export type CheckoutMutationVariables = Exact<{
|
||||
export type CheckoutSessionMutationVariables = Exact<{
|
||||
recurringInterval: Scalars['String'];
|
||||
successUrlPath?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CheckoutMutation = { __typename?: 'Mutation', checkout: { __typename?: 'CheckoutEntity', url: string } };
|
||||
export type CheckoutSessionMutation = { __typename?: 'Mutation', checkoutSession: { __typename?: 'SessionEntity', url: string } };
|
||||
|
||||
export type GetProductPricesQueryVariables = Exact<{
|
||||
product: Scalars['String'];
|
||||
@ -1582,40 +1588,43 @@ 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 CheckoutDocument = gql`
|
||||
mutation Checkout($recurringInterval: String!, $successUrlPath: String) {
|
||||
checkout(recurringInterval: $recurringInterval, successUrlPath: $successUrlPath) {
|
||||
export const CheckoutSessionDocument = gql`
|
||||
mutation CheckoutSession($recurringInterval: String!, $successUrlPath: String) {
|
||||
checkoutSession(
|
||||
recurringInterval: $recurringInterval
|
||||
successUrlPath: $successUrlPath
|
||||
) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CheckoutMutationFn = Apollo.MutationFunction<CheckoutMutation, CheckoutMutationVariables>;
|
||||
export type CheckoutSessionMutationFn = Apollo.MutationFunction<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCheckoutMutation__
|
||||
* __useCheckoutSessionMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCheckoutMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCheckoutMutation` returns a tuple that includes:
|
||||
* To run a mutation, you first call `useCheckoutSessionMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCheckoutSessionMutation` 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 [checkoutMutation, { data, loading, error }] = useCheckoutMutation({
|
||||
* const [checkoutSessionMutation, { data, loading, error }] = useCheckoutSessionMutation({
|
||||
* variables: {
|
||||
* recurringInterval: // value for 'recurringInterval'
|
||||
* successUrlPath: // value for 'successUrlPath'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCheckoutMutation(baseOptions?: Apollo.MutationHookOptions<CheckoutMutation, CheckoutMutationVariables>) {
|
||||
export function useCheckoutSessionMutation(baseOptions?: Apollo.MutationHookOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CheckoutMutation, CheckoutMutationVariables>(CheckoutDocument, options);
|
||||
return Apollo.useMutation<CheckoutSessionMutation, CheckoutSessionMutationVariables>(CheckoutSessionDocument, options);
|
||||
}
|
||||
export type CheckoutMutationHookResult = ReturnType<typeof useCheckoutMutation>;
|
||||
export type CheckoutMutationResult = Apollo.MutationResult<CheckoutMutation>;
|
||||
export type CheckoutMutationOptions = Apollo.BaseMutationOptions<CheckoutMutation, CheckoutMutationVariables>;
|
||||
export type CheckoutSessionMutationHookResult = ReturnType<typeof useCheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationResult = Apollo.MutationResult<CheckoutSessionMutation>;
|
||||
export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
|
||||
export const GetProductPricesDocument = gql`
|
||||
query GetProductPrices($product: String!) {
|
||||
getProductPrices(product: $product) {
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CHECKOUT = gql`
|
||||
mutation Checkout($recurringInterval: String!, $successUrlPath: String) {
|
||||
checkout(
|
||||
export const CHECKOUT_SESSION = gql`
|
||||
mutation CheckoutSession(
|
||||
$recurringInterval: String!
|
||||
$successUrlPath: String
|
||||
) {
|
||||
checkoutSession(
|
||||
recurringInterval: $recurringInterval
|
||||
successUrlPath: $successUrlPath
|
||||
) {
|
@ -14,7 +14,7 @@ import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
|
||||
import { CardPicker } from '@/ui/input/components/CardPicker.tsx';
|
||||
import {
|
||||
ProductPriceEntity,
|
||||
useCheckoutMutation,
|
||||
useCheckoutSessionMutation,
|
||||
useGetProductPricesQuery,
|
||||
} from '~/generated/graphql.tsx';
|
||||
|
||||
@ -53,7 +53,7 @@ export const ChooseYourPlan = () => {
|
||||
variables: { product: 'base-plan' },
|
||||
});
|
||||
|
||||
const [checkout] = useCheckoutMutation();
|
||||
const [checkoutSession] = useCheckoutSessionMutation();
|
||||
|
||||
const handlePlanChange = (type?: string) => {
|
||||
return () => {
|
||||
@ -81,14 +81,14 @@ export const ChooseYourPlan = () => {
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
setIsSubmitting(true);
|
||||
const { data } = await checkout({
|
||||
const { data } = await checkoutSession({
|
||||
variables: {
|
||||
recurringInterval: planSelected,
|
||||
successUrlPath: AppPath.PlanRequiredSuccess,
|
||||
},
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
if (!data?.checkout.url) {
|
||||
if (!data?.checkoutSession.url) {
|
||||
enqueueSnackBar(
|
||||
'Checkout session error. Please retry or contact Twenty team',
|
||||
{
|
||||
@ -97,7 +97,7 @@ export const ChooseYourPlan = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.location.replace(data.checkout.url);
|
||||
window.location.replace(data.checkoutSession.url);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -11,8 +11,9 @@ import { ProductPricesEntity } from 'src/core/billing/dto/product-prices.entity'
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { AuthUser } from 'src/decorators/auth/auth-user.decorator';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { CheckoutInput } from 'src/core/billing/dto/checkout.input';
|
||||
import { CheckoutEntity } from 'src/core/billing/dto/checkout.entity';
|
||||
import { CheckoutSessionInput } from 'src/core/billing/dto/checkout-session.input';
|
||||
import { SessionEntity } from 'src/core/billing/dto/session.entity';
|
||||
import { BillingSessionInput } from 'src/core/billing/dto/billing-session.input';
|
||||
|
||||
@Resolver()
|
||||
export class BillingResolver {
|
||||
@ -38,11 +39,25 @@ export class BillingResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => CheckoutEntity)
|
||||
@Query(() => SessionEntity)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async checkout(
|
||||
async billingPortalSession(
|
||||
@AuthUser() user: User,
|
||||
@Args() { recurringInterval, successUrlPath }: CheckoutInput,
|
||||
@Args() { returnUrlPath }: BillingSessionInput,
|
||||
) {
|
||||
return {
|
||||
url: await this.billingService.computeBillingPortalSessionURL(
|
||||
user.defaultWorkspaceId,
|
||||
returnUrlPath,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => SessionEntity)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async checkoutSession(
|
||||
@AuthUser() user: User,
|
||||
@Args() { recurringInterval, successUrlPath }: CheckoutSessionInput,
|
||||
) {
|
||||
const stripeProductId = this.billingService.getProductStripeId(
|
||||
AvailableProduct.BasePlan,
|
||||
@ -66,7 +81,7 @@ export class BillingResolver {
|
||||
);
|
||||
|
||||
return {
|
||||
url: await this.billingService.checkout(
|
||||
url: await this.billingService.computeCheckoutSessionURL(
|
||||
user,
|
||||
stripePriceId,
|
||||
successUrlPath,
|
||||
|
@ -11,6 +11,7 @@ import { BillingSubscriptionItem } from 'src/core/billing/entities/billing-subsc
|
||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||
import { ProductPriceEntity } from 'src/core/billing/dto/product-price.entity';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export enum AvailableProduct {
|
||||
BasePlan = 'base-plan',
|
||||
@ -101,18 +102,45 @@ export class BillingService {
|
||||
return billingSubscriptionItem;
|
||||
}
|
||||
|
||||
async checkout(user: User, priceId: string, successUrlPath?: string) {
|
||||
async computeBillingPortalSessionURL(
|
||||
workspaceId: string,
|
||||
returnUrlPath?: string,
|
||||
) {
|
||||
const billingSubscription =
|
||||
await this.billingSubscriptionRepository.findOneOrFail({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
const session = await this.stripeService.createBillingPortalSession(
|
||||
billingSubscription.stripeCustomerId,
|
||||
returnUrlPath,
|
||||
);
|
||||
|
||||
assert(session.url, 'Error: missing billingPortal.session.url');
|
||||
|
||||
return session.url;
|
||||
}
|
||||
|
||||
async computeCheckoutSessionURL(
|
||||
user: User,
|
||||
priceId: string,
|
||||
successUrlPath?: string,
|
||||
): Promise<string> {
|
||||
const frontBaseUrl = this.environmentService.getFrontBaseUrl();
|
||||
const successUrl = successUrlPath
|
||||
? frontBaseUrl + successUrlPath
|
||||
: frontBaseUrl;
|
||||
|
||||
return await this.stripeService.createCheckoutSession(
|
||||
const session = await this.stripeService.createCheckoutSession(
|
||||
user,
|
||||
priceId,
|
||||
successUrl,
|
||||
frontBaseUrl,
|
||||
);
|
||||
|
||||
assert(session.url, 'Error: missing checkout.session.url');
|
||||
|
||||
return session.url;
|
||||
}
|
||||
|
||||
async deleteSubscription(workspaceId: string) {
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class BillingSessionInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
returnUrlPath?: string;
|
||||
}
|
@ -4,7 +4,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
@ArgsType()
|
||||
export class CheckoutInput {
|
||||
export class CheckoutSessionInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class CheckoutEntity {
|
||||
export class SessionEntity {
|
||||
@Field(() => String)
|
||||
url: string;
|
||||
}
|
@ -4,7 +4,6 @@ import Stripe from 'stripe';
|
||||
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import { User } from 'src/core/user/user.entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
@ -43,13 +42,23 @@ export class StripeService {
|
||||
await this.stripe.subscriptions.cancel(stripeSubscriptionId);
|
||||
}
|
||||
|
||||
async createBillingPortalSession(
|
||||
stripeCustomerId: string,
|
||||
returnUrlPath?: string,
|
||||
): Promise<Stripe.BillingPortal.Session> {
|
||||
return await this.stripe.billingPortal.sessions.create({
|
||||
customer: stripeCustomerId,
|
||||
return_url: returnUrlPath ?? this.environmentService.getFrontBaseUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
async createCheckoutSession(
|
||||
user: User,
|
||||
priceId: string,
|
||||
successUrl?: string,
|
||||
cancelUrl?: string,
|
||||
) {
|
||||
const session = await this.stripe.checkout.sessions.create({
|
||||
): Promise<Stripe.Checkout.Session> {
|
||||
return await this.stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
@ -70,11 +79,5 @@ export class StripeService {
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
});
|
||||
|
||||
assert(session.url, 'Error: missing checkout.session.url');
|
||||
|
||||
this.logger.log(`Stripe Checkout Session Url Redirection: ${session.url}`);
|
||||
|
||||
return session.url;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user