feat: stripe connect atom (#16190)
* update stripe service * init endpoints for stripe connect atom * update stripe controller * update stripe service * add generic function to create app credential * update stripe controller * update stripe service * add handler to construct redirectUrl * restructure files for better readibility * better naming * replace STRIPE_PRIVATE_KEY with STRIPE_API_KEY * fix naming * remove unused query param * frontend for stripe connect atom * custom hooks for stripe connect atom * abstract response schema into separate file * fixup * input dto for stripe controller * update stripe module * update endpoints module to include stripe module * updatte stripe module * fixups and add check endpoint to stripe controller * add method to check stripe account * add helper fn to get on error return value * update stripe connect atom * custom hook to check user stripe credentials * add stripe connect to atom exports * update stripe connect styling * add qs stringify package * translations for stripe connect atom * updaet typing * update output dtos for stripe endpoints * add error message * fix merge conflicts * add stripe connect atom to atom exports * add query param for error rediect link * add handler for onCheck success and error redirect link * update index value * fix merge conflicts * fixup * resolve merge conflicts * war with merge conflicts * update examples app --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
"next-auth": "^4.22.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"qs-stringify": "^1.2.1",
|
||||
"querystring": "^0.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { App } from "@prisma/client";
|
||||
import { App, Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class AppsRepository {
|
||||
@@ -10,4 +10,15 @@ export class AppsRepository {
|
||||
async getAppBySlug(slug: string): Promise<App | null> {
|
||||
return await this.dbRead.prisma.app.findUnique({ where: { slug } });
|
||||
}
|
||||
|
||||
async createAppCredential(type: string, key: Prisma.InputJsonValue, userId: number, appId: string) {
|
||||
return this.dbWrite.prisma.credential.create({
|
||||
data: {
|
||||
type: type,
|
||||
key: key,
|
||||
userId: userId,
|
||||
appId: appId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AtomsModule } from "@/modules/atoms/atoms.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -20,6 +21,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
WebhooksModule,
|
||||
DestinationCalendarsModule,
|
||||
AtomsModule,
|
||||
StripeModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import {
|
||||
StripConnectOutputDto,
|
||||
StripConnectOutputResponseDto,
|
||||
StripCredentialsCheckOutputResponseDto,
|
||||
StripCredentialsSaveOutputResponseDto,
|
||||
} from "@/modules/stripe/outputs/stripe.output";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { getOnErrorReturnToValueFromQueryState } from "@/modules/stripe/utils/getReturnToValueFromQueryState";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Query,
|
||||
UseGuards,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Redirect,
|
||||
Req,
|
||||
BadRequestException,
|
||||
Headers,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/stripe",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Stripe")
|
||||
export class StripeController {
|
||||
constructor(private readonly stripeService: StripeService) {}
|
||||
|
||||
@Get("/connect")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Query("redir") redir?: string | null,
|
||||
@Query("errorRedir") errorRedir?: string | null
|
||||
): Promise<StripConnectOutputResponseDto> {
|
||||
const origin = req.headers.origin;
|
||||
const accessToken = authorization.replace("Bearer ", "");
|
||||
|
||||
const state = {
|
||||
onErrorReturnTo: !!errorRedir ? errorRedir : origin,
|
||||
fromApp: false,
|
||||
returnTo: !!redir ? redir : origin,
|
||||
accessToken,
|
||||
};
|
||||
|
||||
const stripeRedirectUrl = await this.stripeService.getStripeRedirectUrl(
|
||||
JSON.stringify(state),
|
||||
user.email,
|
||||
user.name
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(StripConnectOutputDto, { authUrl: stripeRedirectUrl }, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/save")
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Query("code") code: string,
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<StripCredentialsSaveOutputResponseDto> {
|
||||
const accessToken = JSON.parse(state).accessToken;
|
||||
|
||||
// user cancels flow
|
||||
if (error === "access_denied") {
|
||||
return { url: getOnErrorReturnToValueFromQueryState(state) };
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw new BadRequestException(stringify({ error, error_description }));
|
||||
}
|
||||
|
||||
return await this.stripeService.saveStripeAccount(state, code, accessToken);
|
||||
}
|
||||
|
||||
@Get("/check")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async check(@GetUser() user: UserWithProfile): Promise<StripCredentialsCheckOutputResponseDto> {
|
||||
return await this.stripeService.checkIfStripeAccountConnected(user.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsString, IsOptional } from "class-validator";
|
||||
|
||||
export class StripeConnectQueryParamsInputDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
readonly redir?: string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class StripConnectOutputDto {
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly authUrl!: string;
|
||||
}
|
||||
|
||||
export class StripConnectOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => StripConnectOutputDto)
|
||||
data!: StripConnectOutputDto;
|
||||
}
|
||||
|
||||
export class StripCredentialsCheckOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS })
|
||||
status!: typeof SUCCESS_STATUS;
|
||||
}
|
||||
|
||||
export class StripCredentialsSaveOutputResponseDto {
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly url!: string;
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeController } from "@/modules/stripe/controllers/stripe.controller";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
imports: [ConfigModule, PrismaModule],
|
||||
exports: [StripeService],
|
||||
providers: [StripeService],
|
||||
providers: [StripeService, AppsRepository, CredentialsRepository, TokensRepository],
|
||||
controllers: [StripeController],
|
||||
})
|
||||
export class StripeModule {}
|
||||
|
||||
@@ -1,15 +1,130 @@
|
||||
import { AppConfig } from "@/config/type";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { getReturnToValueFromQueryState } from "@/modules/stripe/utils/getReturnToValueFromQueryState";
|
||||
import { stripeInstance } from "@/modules/stripe/utils/newStripeInstance";
|
||||
import { StripeData } from "@/modules/stripe/utils/stripeDataSchemas";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Injectable, NotFoundException, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { stripeKeysResponseSchema } from "./utils/stripeDataSchemas";
|
||||
|
||||
import stringify = require("qs-stringify");
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
public stripe: Stripe;
|
||||
private redirectUri = `${this.config.get("api.url")}/stripe/save`;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
constructor(
|
||||
configService: ConfigService<AppConfig>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly appsRepository: AppsRepository,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly tokensRepository: TokensRepository
|
||||
) {
|
||||
this.stripe = new Stripe(configService.get("stripe.apiKey", { infer: true }) ?? "", {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
||||
}
|
||||
|
||||
async getStripeRedirectUrl(state: string, userEmail?: string, userName?: string | null) {
|
||||
const { client_id } = await this.getStripeAppKeys();
|
||||
|
||||
const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = {
|
||||
client_id,
|
||||
scope: "read_write",
|
||||
response_type: "code",
|
||||
stripe_user: {
|
||||
email: userEmail,
|
||||
first_name: userName || undefined,
|
||||
/** We need this so E2E don't fail for international users */
|
||||
country: process.env.NEXT_PUBLIC_IS_E2E ? "US" : undefined,
|
||||
},
|
||||
redirect_uri: this.redirectUri,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const params = z.record(z.any()).parse(stripeConnectParams);
|
||||
const query = stringify(params);
|
||||
const url = `https://connect.stripe.com/oauth/authorize?${query}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async getStripeAppKeys() {
|
||||
const app = await this.appsRepository.getAppBySlug("stripe");
|
||||
|
||||
const { client_id, client_secret } = stripeKeysResponseSchema.parse(app?.keys);
|
||||
|
||||
if (!client_id) {
|
||||
throw new NotFoundException("Stripe app not found");
|
||||
}
|
||||
|
||||
if (!client_secret) {
|
||||
throw new NotFoundException("Stripe app not found");
|
||||
}
|
||||
|
||||
return { client_id, client_secret };
|
||||
}
|
||||
|
||||
async saveStripeAccount(state: string, code: string, accessToken: string): Promise<{ url: string }> {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(accessToken);
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException("Invalid Access token.");
|
||||
}
|
||||
|
||||
const response = await stripeInstance.oauth.token({
|
||||
grant_type: "authorization_code",
|
||||
code: code?.toString(),
|
||||
});
|
||||
|
||||
const data: StripeData = { ...response, default_currency: "" };
|
||||
if (response["stripe_user_id"]) {
|
||||
const account = await stripeInstance.accounts.retrieve(response["stripe_user_id"]);
|
||||
data["default_currency"] = account.default_currency;
|
||||
}
|
||||
|
||||
await this.appsRepository.createAppCredential(
|
||||
"stripe_payment",
|
||||
data as unknown as Prisma.InputJsonObject,
|
||||
userId,
|
||||
"stripe"
|
||||
);
|
||||
|
||||
return { url: getReturnToValueFromQueryState(state) };
|
||||
}
|
||||
|
||||
async checkIfStripeAccountConnected(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> {
|
||||
const stripeCredentials = await this.credentialRepository.getByTypeAndUserId("stripe_payment", userId);
|
||||
|
||||
if (!stripeCredentials) {
|
||||
throw new NotFoundException("Credentials for stripe not found.");
|
||||
}
|
||||
|
||||
if (stripeCredentials.invalid) {
|
||||
throw new BadRequestException("Invalid stripe credentials.");
|
||||
}
|
||||
|
||||
const stripeKey = JSON.stringify(stripeCredentials.key);
|
||||
const stripeKeyObject = JSON.parse(stripeKey);
|
||||
|
||||
const stripeAccount = await stripeInstance.accounts.retrieve(stripeKeyObject?.stripe_user_id);
|
||||
|
||||
// both of these should be true for an account to be fully active
|
||||
if (!stripeAccount.payouts_enabled || !stripeAccount.charges_enabled) {
|
||||
throw new BadRequestException("Stripe account is not an active account");
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export const getReturnToValueFromQueryState = (queryState: string | string[] | undefined) => {
|
||||
let returnTo = "";
|
||||
try {
|
||||
returnTo = JSON.parse(`${queryState}`).returnTo;
|
||||
} catch (error) {
|
||||
console.info("No 'returnTo' in req.query.state");
|
||||
}
|
||||
return returnTo;
|
||||
};
|
||||
|
||||
export const getOnErrorReturnToValueFromQueryState = (queryState: string | string[] | undefined) => {
|
||||
let returnTo = "";
|
||||
try {
|
||||
returnTo = JSON.parse(`${queryState}`).onErrorReturnTo;
|
||||
} catch (error) {
|
||||
console.info("No 'onErrorReturnTo' in req.query.state");
|
||||
}
|
||||
return returnTo;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
const stripeApiKey = process.env.STRIPE_API_KEY || "";
|
||||
export const stripeInstance = new Stripe(stripeApiKey, {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const stripeOAuthTokenSchema = z.object({
|
||||
access_token: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
livemode: z.boolean().optional(),
|
||||
token_type: z.literal("bearer").optional(),
|
||||
refresh_token: z.string().optional(),
|
||||
stripe_user_id: z.string().optional(),
|
||||
stripe_publishable_key: z.string().optional(),
|
||||
});
|
||||
|
||||
export const stripeDataSchema = stripeOAuthTokenSchema.extend({
|
||||
default_currency: z.string(),
|
||||
});
|
||||
|
||||
export type StripeData = z.infer<typeof stripeDataSchema>;
|
||||
|
||||
export const stripeKeysResponseSchema = z.object({
|
||||
client_id: z.string().startsWith("ca_").min(1),
|
||||
client_secret: z.string().startsWith("sk_").min(1),
|
||||
public_key: z.string().startsWith("pk_").min(1),
|
||||
webhook_secret: z.string().startsWith("whsec_").min(1),
|
||||
});
|
||||
@@ -2605,6 +2605,9 @@
|
||||
"outlook_connect_atom_label": "Connect Outlook Calendar",
|
||||
"outlook_connect_atom_already_connected_label": "Connected Outlook Calendar",
|
||||
"outlook_connect_atom_loading_label": "Checking Outlook Calendar",
|
||||
"stripe_connect_atom_label": "Connect to Stripe",
|
||||
"stripe_connect_atom_already_connected_label": "Connected Stripe",
|
||||
"stripe_connect_atom_loading_label": "Checking Stripe credentials",
|
||||
"booking_question_response_variables": "Booking question response variables",
|
||||
"managed_by_teamAdmins": "Managed by {{teamAdmins}}",
|
||||
"number_of_options":"{{count}} options",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import type { OnCheckErrorType, UseCheckProps } from "../../hooks/connect/useCheck";
|
||||
import { useCheck } from "../../hooks/stripe/useCheck";
|
||||
import { useConnect } from "../../hooks/stripe/useConnect";
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { cn } from "../../src/lib/utils";
|
||||
|
||||
type StripeConnectProps = {
|
||||
className?: string;
|
||||
label: string;
|
||||
alreadyConnectedLabel: string;
|
||||
loadingLabel: string;
|
||||
onCheckError?: OnCheckErrorType;
|
||||
redir?: string;
|
||||
errorRedir?: string;
|
||||
initialData: UseCheckProps["initialData"];
|
||||
onCheckSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const StripeConnect: FC<Partial<StripeConnectProps>> = ({
|
||||
label,
|
||||
className,
|
||||
loadingLabel,
|
||||
alreadyConnectedLabel,
|
||||
redir,
|
||||
errorRedir,
|
||||
onCheckError,
|
||||
initialData,
|
||||
onCheckSuccess,
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { connect } = useConnect(redir, errorRedir);
|
||||
const { allowConnect, checked } = useCheck({
|
||||
onCheckError,
|
||||
onCheckSuccess,
|
||||
initialData,
|
||||
});
|
||||
|
||||
let displayedLabel = label || t("stripe_connect_atom_label");
|
||||
|
||||
const isChecking = !checked;
|
||||
const isDisabled = isChecking || !allowConnect;
|
||||
|
||||
if (isChecking) {
|
||||
displayedLabel = loadingLabel || t("stripe_connect_atom_loading_label");
|
||||
} else if (!allowConnect) {
|
||||
displayedLabel = alreadyConnectedLabel || t("stripe_connect_atom_already_connected_label");
|
||||
}
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<Button
|
||||
StartIcon="calendar"
|
||||
color="primary"
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"",
|
||||
className,
|
||||
isChecking && "animate-pulse",
|
||||
isDisabled && "cursor-not-allowed",
|
||||
!isDisabled && "cursor-pointer"
|
||||
)}
|
||||
onClick={() => connect()}>
|
||||
{displayedLabel}
|
||||
</Button>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type { CALENDARS } from "@calcom/platform-constants";
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiErrorResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../lib/http";
|
||||
import { useAtomsContext } from "../useAtomsContext";
|
||||
|
||||
export interface UseCheckProps {
|
||||
onCheckError?: OnCheckErrorType;
|
||||
onCheckSuccess?: () => void;
|
||||
initialData?: {
|
||||
status: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
data: {
|
||||
allowConnect: boolean;
|
||||
checked: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
const stripeQueryKey = ["get-stripe-check"];
|
||||
export type OnCheckErrorType = (err: ApiErrorResponse) => void;
|
||||
export const getQueryKey = (calendar: (typeof CALENDARS)[number]) => [`get-${calendar}-check`];
|
||||
|
||||
export const useCheck = ({ onCheckError, initialData, onCheckSuccess }: UseCheckProps) => {
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: check, refetch } = useQuery({
|
||||
queryKey: stripeQueryKey,
|
||||
staleTime: 6000,
|
||||
enabled: isInit && !!accessToken,
|
||||
queryFn: () => {
|
||||
return http
|
||||
?.get<ApiResponse<{ checked: boolean; allowConnect: boolean }>>(`/stripe/check`)
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.status === SUCCESS_STATUS) {
|
||||
onCheckSuccess();
|
||||
return { status: SUCCESS_STATUS, data: { allowConnect: false, checked: true } };
|
||||
}
|
||||
onCheckError?.(responseBody);
|
||||
return { status: ERROR_STATUS, data: { allowConnect: true, checked: true } };
|
||||
})
|
||||
.catch((err) => {
|
||||
onCheckError?.(err);
|
||||
return { status: ERROR_STATUS, data: { allowConnect: true, checked: true } };
|
||||
});
|
||||
},
|
||||
initialData,
|
||||
});
|
||||
return {
|
||||
allowConnect: check?.data?.allowConnect ?? false,
|
||||
checked: check?.data?.checked ?? false,
|
||||
refetch: () => {
|
||||
queryClient.setQueryData(stripeQueryKey, {
|
||||
status: SUCCESS_STATUS,
|
||||
data: { allowConnect: false, checked: false },
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../lib/http";
|
||||
|
||||
export const useGetRedirectUrl = (redir?: string, errorRedir?: string) => {
|
||||
const authUrl = useQuery({
|
||||
queryKey: ["get-stripe-connect-redirect-uri"],
|
||||
staleTime: Infinity,
|
||||
enabled: false,
|
||||
queryFn: () => {
|
||||
return http
|
||||
?.get<ApiResponse<{ authUrl: string }>>(
|
||||
`/stripe/connect${redir ? `?redir=${encodeURIComponent(redir)}` : "?redir="}${
|
||||
errorRedir ? `&errorRedir=${encodeURIComponent(errorRedir)}` : ""
|
||||
}`
|
||||
)
|
||||
.then(({ data: responseBody }) => {
|
||||
if (responseBody.status === SUCCESS_STATUS) {
|
||||
return responseBody.data.authUrl;
|
||||
}
|
||||
if (responseBody.status === ERROR_STATUS) throw new Error(responseBody.error.message);
|
||||
return "";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return authUrl;
|
||||
};
|
||||
|
||||
export const useConnect = (redir?: string, errorRedir?: string) => {
|
||||
const { refetch } = useGetRedirectUrl(redir, errorRedir);
|
||||
|
||||
const connect = async () => {
|
||||
const redirectUri = await refetch();
|
||||
|
||||
if (redirectUri.data) {
|
||||
window.location.href = redirectUri.data;
|
||||
}
|
||||
};
|
||||
|
||||
return { connect };
|
||||
};
|
||||
@@ -25,3 +25,4 @@ export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettin
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
|
||||
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
|
||||
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
|
||||
export { StripeConnect } from "./connect/stripe/StripeConnect";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
|
||||
import { GcalConnect, Connect } from "@calcom/atoms";
|
||||
import { Connect, StripeConnect } from "@calcom/atoms";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const poppins = Poppins({ subsets: ["latin"], weight: ["400", "800"] });
|
||||
@@ -20,7 +20,7 @@ export default function Home(props: { calUsername: string; calEmail: string }) {
|
||||
To get started, connect your google calendar.
|
||||
</p>
|
||||
<div className="flex flex-row gap-4">
|
||||
<GcalConnect
|
||||
<Connect.GoogleCalendar
|
||||
redir="http://localhost:4321/calendars"
|
||||
className="h-[40px] bg-gradient-to-r from-[#8A2387] via-[#E94057] to-[#F27121] text-center text-base font-semibold text-transparent text-white hover:bg-orange-700"
|
||||
/>
|
||||
@@ -33,6 +33,13 @@ export default function Home(props: { calUsername: string; calEmail: string }) {
|
||||
isMultiCalendar={true}
|
||||
className="h-[40px] bg-gradient-to-r from-[#8A2387] via-[#E94057] to-[#F27121] text-center text-base font-semibold text-transparent text-white hover:bg-orange-700"
|
||||
/>
|
||||
<StripeConnect
|
||||
className="h-[40px] bg-gradient-to-r from-[#E94057] via-[#E94057] to-[#E94057] text-center text-base font-semibold text-transparent text-white hover:bg-orange-700"
|
||||
errorRedir="http://localhost:4321/availability"
|
||||
onCheckSuccess={() => {
|
||||
console.log("stripe account connected successfully".toLocaleUpperCase());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
|
||||
Reference in New Issue
Block a user