chore: [app-router-migration 17]: migrate payments page (#13039)
* chore: migrate payments page * migrate page to WithLayout HOC, replace new ssrInit * fix type error * fix * revert version changes * fix --------- Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
parent
abd90f6af8
commit
a28b9cacd2
|
@ -1,7 +1,6 @@
|
|||
import LegacyPage from "@pages/getting-started/[[...step]]";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
@ -10,10 +9,7 @@ import prisma from "@calcom/prisma";
|
|||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const getData = async (ctx: GetServerSidePropsContext) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
|
||||
const session = await getServerSession({ req });
|
||||
const session = await getServerSession({ req: ctx.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
|
||||
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
// the title does not contain the eventName as in the legacy page
|
||||
(t) => `${t("payment")} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
});
|
||||
|
||||
async function getData(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
await ssr.viewer.me.prefetch();
|
||||
|
||||
const { uid } = querySchema.parse(context.params);
|
||||
const rawPayment = await prisma.payment.findFirst({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
data: true,
|
||||
success: true,
|
||||
uid: true,
|
||||
refunded: true,
|
||||
bookingId: true,
|
||||
appId: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
paymentOption: true,
|
||||
booking: {
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
description: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
location: true,
|
||||
status: true,
|
||||
rejectionReason: true,
|
||||
cancellationReason: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
eventName: true,
|
||||
requiresConfirmation: true,
|
||||
userId: true,
|
||||
metadata: true,
|
||||
users: {
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
price: true,
|
||||
currency: true,
|
||||
successRedirectUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rawPayment) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { data, booking: _booking, ...restPayment } = rawPayment;
|
||||
|
||||
const payment = {
|
||||
...restPayment,
|
||||
data: data as Record<string, unknown>,
|
||||
};
|
||||
|
||||
if (!_booking) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { startTime, endTime, eventType, ...restBooking } = _booking;
|
||||
const booking = {
|
||||
...restBooking,
|
||||
startTime: startTime.toString(),
|
||||
endTime: endTime.toString(),
|
||||
};
|
||||
|
||||
if (!eventType) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (eventType.users.length === 0 && !!!eventType.team) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [user] = eventType?.users.length
|
||||
? eventType.users
|
||||
: [{ name: null, theme: null, hideBranding: null, username: null }];
|
||||
const profile = {
|
||||
name: eventType.team?.name || user?.name || null,
|
||||
theme: (!eventType.team?.name && user?.theme) || null,
|
||||
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
|
||||
};
|
||||
|
||||
if (
|
||||
([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes(
|
||||
booking.status as BookingStatus
|
||||
)
|
||||
) {
|
||||
return redirect(`/booking/${booking.uid}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
eventType: {
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
},
|
||||
booking,
|
||||
dehydratedState: ssr.dehydrate(),
|
||||
payment,
|
||||
clientSecret: getClientSecretFromPayment(payment),
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
export default WithLayout({ getLayout: null, getData, Page: PaymentPage });
|
|
@ -0,0 +1,68 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Payment", () => {
|
||||
test.describe("user", () => {
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
test("should create a mock payment for a user", async ({ context, users, page }) => {
|
||||
test.skip(process.env.MOCK_PAYMENT_APP_ENABLED === undefined, "Skipped as Stripe is not installed");
|
||||
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto("/apps");
|
||||
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("mock");
|
||||
|
||||
await page.getByTestId("install-app-button").click();
|
||||
|
||||
await page.waitForURL((url) => url.pathname.endsWith("/apps/installed/payment"));
|
||||
|
||||
await page.getByRole("link", { name: "Event Types" }).click();
|
||||
|
||||
await page.getByRole("link", { name: /^30 min/ }).click();
|
||||
await page.getByTestId("vertical-tab-apps").click();
|
||||
await page.locator("#event-type-form").getByRole("switch").click();
|
||||
await page.getByPlaceholder("Price").click();
|
||||
await page.getByPlaceholder("Price").fill("1");
|
||||
|
||||
await page.locator("#test-mock-payment-app-currency-id").click();
|
||||
await page.getByTestId("select-option-USD").click();
|
||||
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
await page.goto(`${user.username}/30-min`);
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await page.waitForURL((url) => url.pathname.includes("/payment/"));
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
await page.getByText("Payment", { exact: true }).waitFor();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1046,72 +1046,77 @@ export function mockCalendar(
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const deleteEventCalls: any[] = [];
|
||||
const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata];
|
||||
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({
|
||||
lib: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
CalendarService: function MockCalendarService() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
|
||||
if (calendarData?.creationCrash) {
|
||||
throw new Error("MockCalendarService.createEvent fake error");
|
||||
}
|
||||
const [calEvent, credentialId] = rest;
|
||||
log.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
|
||||
createEventCalls.push(rest);
|
||||
return Promise.resolve({
|
||||
type: app.type,
|
||||
additionalInfo: {},
|
||||
uid: "PROBABLY_UNUSED_UID",
|
||||
// A Calendar is always expected to return an id.
|
||||
id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID",
|
||||
iCalUID: normalizedCalendarData.create?.iCalUID,
|
||||
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
|
||||
password: "MOCK_PASSWORD",
|
||||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
|
||||
if (calendarData?.updationCrash) {
|
||||
throw new Error("MockCalendarService.updateEvent fake error");
|
||||
}
|
||||
const [uid, event, externalCalendarId] = rest;
|
||||
log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
updateEventCalls.push(rest);
|
||||
return Promise.resolve({
|
||||
type: app.type,
|
||||
additionalInfo: {},
|
||||
uid: "PROBABLY_UNUSED_UID",
|
||||
iCalUID: normalizedCalendarData.update?.iCalUID,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
|
||||
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
|
||||
password: "MOCK_PASSWORD",
|
||||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteEvent: async (...rest: any[]) => {
|
||||
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
deleteEventCalls.push(rest);
|
||||
},
|
||||
getAvailability: async (): Promise<EventBusyDate[]> => {
|
||||
if (calendarData?.getAvailabilityCrash) {
|
||||
throw new Error("MockCalendarService.getAvailability fake error");
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolve(calendarData?.busySlots || []);
|
||||
});
|
||||
},
|
||||
};
|
||||
const appMock = appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default];
|
||||
|
||||
appMock &&
|
||||
`mockResolvedValue` in appMock &&
|
||||
appMock.mockResolvedValue({
|
||||
lib: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
CalendarService: function MockCalendarService() {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
|
||||
if (calendarData?.creationCrash) {
|
||||
throw new Error("MockCalendarService.createEvent fake error");
|
||||
}
|
||||
const [calEvent, credentialId] = rest;
|
||||
log.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
|
||||
createEventCalls.push(rest);
|
||||
return Promise.resolve({
|
||||
type: app.type,
|
||||
additionalInfo: {},
|
||||
uid: "PROBABLY_UNUSED_UID",
|
||||
// A Calendar is always expected to return an id.
|
||||
id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID",
|
||||
iCalUID: normalizedCalendarData.create?.iCalUID,
|
||||
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
|
||||
password: "MOCK_PASSWORD",
|
||||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
|
||||
if (calendarData?.updationCrash) {
|
||||
throw new Error("MockCalendarService.updateEvent fake error");
|
||||
}
|
||||
const [uid, event, externalCalendarId] = rest;
|
||||
log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
updateEventCalls.push(rest);
|
||||
return Promise.resolve({
|
||||
type: app.type,
|
||||
additionalInfo: {},
|
||||
uid: "PROBABLY_UNUSED_UID",
|
||||
iCalUID: normalizedCalendarData.update?.iCalUID,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
|
||||
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
|
||||
password: "MOCK_PASSWORD",
|
||||
url: "https://UNUSED_URL",
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
deleteEvent: async (...rest: any[]) => {
|
||||
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
deleteEventCalls.push(rest);
|
||||
},
|
||||
getAvailability: async (): Promise<EventBusyDate[]> => {
|
||||
if (calendarData?.getAvailabilityCrash) {
|
||||
throw new Error("MockCalendarService.getAvailability fake error");
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolve(calendarData?.busySlots || []);
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return {
|
||||
createEventCalls,
|
||||
deleteEventCalls,
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"turbo": "^1.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"types-ramda": "0.29.4",
|
||||
"@apidevtools/json-schema-ref-parser": "9.0.9",
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
|
@ -21,7 +21,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
eventType,
|
||||
eventTypeFormMetadata,
|
||||
}) {
|
||||
const { asPath } = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
|
||||
const pathname = usePathname();
|
||||
const asPath = useMemo(
|
||||
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
[pathname, searchParams]
|
||||
);
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
const currency = getAppData("currency");
|
||||
|
|
|
@ -29,6 +29,7 @@ export const EventTypeAddonMap = {
|
|||
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
||||
matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")),
|
||||
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
|
||||
"mock-payment-app": dynamic(() => import("./mock-payment-app/components/EventTypeAppCardInterface")),
|
||||
paypal: dynamic(() => import("./paypal/components/EventTypeAppCardInterface")),
|
||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
||||
|
|
|
@ -17,6 +17,7 @@ import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
|||
import { appKeysSchema as make_zod_ts } from "./make/zod";
|
||||
import { appKeysSchema as matomo_zod_ts } from "./matomo/zod";
|
||||
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||
import { appKeysSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
|
||||
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appKeysSchema as paypal_zod_ts } from "./paypal/zod";
|
||||
|
@ -55,6 +56,7 @@ export const appKeysSchemas = {
|
|||
make: make_zod_ts,
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
"mock-payment-app": mock_payment_app_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
paypal: paypal_zod_ts,
|
||||
|
|
|
@ -36,6 +36,7 @@ import make_config_json from "./make/config.json";
|
|||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
import mirotalk_config_json from "./mirotalk/config.json";
|
||||
import mock_payment_app_config_json from "./mock-payment-app/config.json";
|
||||
import n8n_config_json from "./n8n/config.json";
|
||||
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
|
||||
import office365video_config_json from "./office365video/config.json";
|
||||
|
@ -115,6 +116,7 @@ export const appStoreMetadata = {
|
|||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
mirotalk: mirotalk_config_json,
|
||||
"mock-payment-app": mock_payment_app_config_json,
|
||||
n8n: n8n_config_json,
|
||||
office365calendar: office365calendar__metadata_ts,
|
||||
office365video: office365video_config_json,
|
||||
|
|
|
@ -17,6 +17,7 @@ import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
|||
import { appDataSchema as make_zod_ts } from "./make/zod";
|
||||
import { appDataSchema as matomo_zod_ts } from "./matomo/zod";
|
||||
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||
import { appDataSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
|
||||
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appDataSchema as paypal_zod_ts } from "./paypal/zod";
|
||||
|
@ -55,6 +56,7 @@ export const appDataSchemas = {
|
|||
make: make_zod_ts,
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
"mock-payment-app": mock_payment_app_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
paypal: paypal_zod_ts,
|
||||
|
|
|
@ -36,6 +36,7 @@ export const apiHandlers = {
|
|||
matomo: import("./matomo/api"),
|
||||
metapixel: import("./metapixel/api"),
|
||||
mirotalk: import("./mirotalk/api"),
|
||||
"mock-payment-app": import("./mock-payment-app/api"),
|
||||
n8n: import("./n8n/api"),
|
||||
office365calendar: import("./office365calendar/api"),
|
||||
office365video: import("./office365video/api"),
|
||||
|
|
|
@ -41,4 +41,12 @@ const appStore = {
|
|||
shimmervideo: () => import("./shimmervideo"),
|
||||
};
|
||||
|
||||
export default appStore;
|
||||
const exportedAppStore: typeof appStore & {
|
||||
["mock-payment-app"]?: () => Promise<typeof import("./mock-payment-app/index")>;
|
||||
} = appStore;
|
||||
|
||||
if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) {
|
||||
exportedAppStore["mock-payment-app"] = () => import("./mock-payment-app/index");
|
||||
}
|
||||
|
||||
export default exportedAppStore;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
items:
|
||||
- 1.jpeg
|
||||
- 2.jpeg
|
||||
- 3.jpeg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
|
@ -0,0 +1,16 @@
|
|||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1 @@
|
|||
export { default as add } from "./add";
|
|
@ -0,0 +1,136 @@
|
|||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import {
|
||||
currencyOptions,
|
||||
currencySymbols,
|
||||
isAcceptedCurrencyCode,
|
||||
} from "@calcom/app-store/paypal/lib/currencyOptions";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Select, TextField } from "@calcom/ui";
|
||||
|
||||
import type { appDataSchema } from "../zod";
|
||||
import { paymentOptions } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const searchParams = useSearchParams();
|
||||
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
|
||||
const pathname = usePathname();
|
||||
const asPath = useMemo(
|
||||
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
[pathname, searchParams]
|
||||
);
|
||||
const { t } = useLocale();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
const currency = getAppData("currency");
|
||||
const paymentOption = getAppData("paymentOption");
|
||||
const enable = getAppData("enabled");
|
||||
|
||||
const [selectedCurrency, setSelectedCurrency] = useState(currencyOptions.find((c) => c.value === currency));
|
||||
const [currencySymbol, setCurrencySymbol] = useState(
|
||||
isAcceptedCurrencyCode(currency) ? currencySymbols[currency] : ""
|
||||
);
|
||||
|
||||
const [requirePayment, setRequirePayment] = useState(enable);
|
||||
|
||||
const paymentOptionSelectValue = paymentOptions?.find((option) => paymentOption === option.value) || {
|
||||
label: paymentOptions[0].label,
|
||||
value: paymentOptions[0].value,
|
||||
};
|
||||
|
||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + asPath}
|
||||
app={app}
|
||||
switchChecked={requirePayment}
|
||||
switchOnClick={(enabled) => {
|
||||
setRequirePayment(enabled);
|
||||
}}
|
||||
description={<>Add a mock payment to your events</>}>
|
||||
<>
|
||||
{recurringEventDefined ? (
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
) : (
|
||||
requirePayment && (
|
||||
<>
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
id="test-mock-payment-app-price"
|
||||
label="Price"
|
||||
labelSrOnly
|
||||
addOnLeading={currencySymbol}
|
||||
addOnSuffix={currency}
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
className="block w-full rounded-sm pl-2 text-sm"
|
||||
placeholder="Price"
|
||||
onChange={(e) => {
|
||||
setAppData("price", Number(e.target.value) * 100);
|
||||
if (selectedCurrency) {
|
||||
setAppData("currency", selectedCurrency.value);
|
||||
}
|
||||
}}
|
||||
value={price > 0 ? price / 100 : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
id="test-mock-payment-app-currency-id"
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
defaultValue={selectedCurrency}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setSelectedCurrency(e);
|
||||
setCurrencySymbol(currencySymbols[e.value]);
|
||||
setAppData("currency", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 w-60">
|
||||
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
|
||||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
defaultValue={
|
||||
paymentOptionSelectValue
|
||||
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
||||
: { ...paymentOptions[0], label: t(paymentOptions[0].label) }
|
||||
}
|
||||
options={paymentOptions.map((option) => {
|
||||
return { ...option, label: t(option.label) || option.label };
|
||||
})}
|
||||
onChange={(input) => {
|
||||
if (input) setAppData("paymentOption", input.value);
|
||||
}}
|
||||
className="mb-1 h-[38px] w-full"
|
||||
isDisabled={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppCard;
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Mock Payment App",
|
||||
"slug": "mock-payment-app",
|
||||
"type": "mock-payment-app_payment",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://example.com/link",
|
||||
"variant": "payment",
|
||||
"categories": ["payment"],
|
||||
"publisher": "Intuita",
|
||||
"email": "greg@intuita.io",
|
||||
"description": "The mock payment app for tests",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "mock-payment-app",
|
||||
"extendsFeature": "EventType"
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
|
@ -0,0 +1,141 @@
|
|||
import type { Booking, Payment, Prisma, PaymentOption } from "@prisma/client";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
|
||||
|
||||
export class PaymentService implements IAbstractPaymentService {
|
||||
async create(
|
||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"]
|
||||
) {
|
||||
try {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
uid: true,
|
||||
title: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
});
|
||||
|
||||
if (booking === null) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
const uid = uuidv4();
|
||||
|
||||
console.log("CREATE payment");
|
||||
|
||||
const paymentData = await prisma.payment.create({
|
||||
data: {
|
||||
uid,
|
||||
app: {
|
||||
connect: {
|
||||
slug: "mock-payment-app",
|
||||
},
|
||||
},
|
||||
booking: {
|
||||
connect: {
|
||||
id: bookingId,
|
||||
},
|
||||
},
|
||||
amount: payment.amount,
|
||||
externalId: uid,
|
||||
currency: payment.currency,
|
||||
data: {} as Prisma.InputJsonValue,
|
||||
fee: 0,
|
||||
refunded: false,
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
|
||||
return paymentData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error("Payment could not be created");
|
||||
}
|
||||
}
|
||||
async update(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async refund(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async collectCard(
|
||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: number,
|
||||
_bookerEmail: string,
|
||||
paymentOption: PaymentOption
|
||||
): Promise<Payment> {
|
||||
try {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
uid: true,
|
||||
title: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
});
|
||||
if (booking === null) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
const uid = uuidv4();
|
||||
|
||||
const paymentData = await prisma.payment.create({
|
||||
data: {
|
||||
uid,
|
||||
app: {
|
||||
connect: {
|
||||
slug: "paypal",
|
||||
},
|
||||
},
|
||||
booking: {
|
||||
connect: {
|
||||
id: bookingId,
|
||||
},
|
||||
},
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
data: {} as Prisma.InputJsonValue,
|
||||
fee: 0,
|
||||
refunded: false,
|
||||
success: false,
|
||||
paymentOption,
|
||||
externalId: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!paymentData) {
|
||||
throw new Error();
|
||||
}
|
||||
return paymentData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error("Payment could not be created");
|
||||
}
|
||||
}
|
||||
chargeCard(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getPaymentPaidStatus(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getPaymentDetails(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
afterPayment(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
deletePayment(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
isSetupAlready(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { PaymentService } from "./PaymentService";
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/mock-payment-app",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "The mock payment app for tests"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"><title>blockchain</title><g class="nc-icon-wrapper"><path d="M32.874,10.405a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L24,15.828Z" fill="#a0e6ee"></path><path d="M15.126,10.405a1,1,0,0,0-.126.484v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V15.828Z" fill="#6cc4f5"></path><path d="M32.874,10.405a1,1,0,0,1,.126.484v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V15.828Z" fill="#2594d0"></path><path d="M47.874,19.589a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L39,25.013Z" fill="#a0e6ee"></path><path d="M30.126,19.589a1,1,0,0,0-.126.485v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V25.013Z" fill="#6cc4f5"></path><path d="M47.874,19.589a1,1,0,0,1,.126.485v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V25.013Z" fill="#2594d0"></path><path d="M17.874,19.589a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L9,25.013Z" fill="#a0e6ee"></path><path d="M.126,19.589A1,1,0,0,0,0,20.074v8.878a1,1,0,0,0,.479.853l8,4.889A1,1,0,0,0,9,34.841V25.013Z" fill="#6cc4f5"></path><path d="M17.874,19.589a1,1,0,0,1,.126.485v8.878a1,1,0,0,1-.479.853l-8,4.889A1,1,0,0,1,9,34.841V25.013Z" fill="#2594d0"></path><path d="M32.874,29.577a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L24,35Z" fill="#a0e6ee"></path><path d="M15.126,29.577a1,1,0,0,0-.126.484v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V35Z" fill="#6cc4f5"></path><path d="M32.874,29.577a1,1,0,0,1,.126.484v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V35Z" fill="#2594d0"></path></g></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,35 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
|
||||
|
||||
const paymentOptionSchema = z.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const paymentOptionsSchema = z.array(paymentOptionSchema);
|
||||
|
||||
export const paymentOptions = [
|
||||
{
|
||||
label: "on_booking_option",
|
||||
value: "ON_BOOKING",
|
||||
},
|
||||
];
|
||||
|
||||
type PaymentOption = (typeof paymentOptions)[number]["value"];
|
||||
|
||||
const VALUES: [PaymentOption, ...PaymentOption[]] = [
|
||||
paymentOptions[0].value,
|
||||
...paymentOptions.slice(1).map((option) => option.value),
|
||||
];
|
||||
export const paymentOptionEnum = z.enum(VALUES);
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||
z.object({
|
||||
price: z.number(),
|
||||
currency: z.string(),
|
||||
paymentOption: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
);
|
||||
export const appKeysSchema = z.object({});
|
|
@ -1,5 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
|
@ -24,7 +24,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
eventType,
|
||||
eventTypeFormMetadata,
|
||||
}) {
|
||||
const { asPath } = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
|
||||
const pathname = usePathname();
|
||||
const asPath = useMemo(
|
||||
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
|
||||
[pathname, searchParams]
|
||||
);
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
|
||||
|
|
|
@ -597,7 +597,8 @@ async function handler(req: CustomRequest) {
|
|||
// Posible to refactor TODO:
|
||||
const paymentApp = (await appStore[
|
||||
paymentAppCredential?.app?.dirName as keyof typeof appStore
|
||||
]()) as PaymentApp;
|
||||
]?.()) as PaymentApp;
|
||||
|
||||
if (!paymentApp?.lib?.PaymentService) {
|
||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||
return null;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import type { Payment } from "@calcom/prisma/client";
|
||||
|
||||
function hasStringProp<T extends string>(x: unknown, key: T): x is { [key in T]: string } {
|
||||
return !!x && typeof x === "object" && key in x;
|
||||
}
|
||||
|
||||
export function getClientSecretFromPayment(
|
||||
payment: Omit<Partial<Payment>, "data"> & { data: Record<string, unknown> }
|
||||
) {
|
||||
if (
|
||||
payment.paymentOption === "HOLD" &&
|
||||
hasStringProp(payment.data, "setupIntent") &&
|
||||
hasStringProp(payment.data.setupIntent, "client_secret")
|
||||
) {
|
||||
return payment.data.setupIntent.client_secret;
|
||||
}
|
||||
if (hasStringProp(payment.data, "client_secret")) {
|
||||
return payment.data.client_secret;
|
||||
}
|
||||
return "";
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import type { Payment } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
@ -9,7 +9,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|||
|
||||
import { ssrInit } from "../../../../../apps/web/server/lib/ssr";
|
||||
|
||||
export type PaymentPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export type PaymentPageProps = Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">;
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
|
@ -145,23 +145,3 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
function hasStringProp<T extends string>(x: unknown, key: T): x is { [key in T]: string } {
|
||||
return !!x && typeof x === "object" && key in x;
|
||||
}
|
||||
|
||||
function getClientSecretFromPayment(
|
||||
payment: Omit<Partial<Payment>, "data"> & { data: Record<string, unknown> }
|
||||
) {
|
||||
if (
|
||||
payment.paymentOption === "HOLD" &&
|
||||
hasStringProp(payment.data, "setupIntent") &&
|
||||
hasStringProp(payment.data.setupIntent, "client_secret")
|
||||
) {
|
||||
return payment.data.setupIntent.client_secret;
|
||||
}
|
||||
if (hasStringProp(payment.data, "client_secret")) {
|
||||
return payment.data.client_secret;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ const deletePayment = async (
|
|||
): Promise<boolean> => {
|
||||
const paymentApp = (await appStore[
|
||||
paymentAppCredentials?.app?.dirName as keyof typeof appStore
|
||||
]()) as PaymentApp;
|
||||
]?.()) as PaymentApp;
|
||||
if (!paymentApp?.lib?.PaymentService) {
|
||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||
return false;
|
||||
|
|
|
@ -29,7 +29,7 @@ const handlePayment = async (
|
|||
) => {
|
||||
const paymentApp = (await appStore[
|
||||
paymentAppCredentials?.app?.dirName as keyof typeof appStore
|
||||
]()) as PaymentApp;
|
||||
]?.()) as PaymentApp;
|
||||
if (!paymentApp?.lib?.PaymentService) {
|
||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||
return null;
|
||||
|
|
|
@ -168,7 +168,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
|||
// undefined it means that app don't require app/setup/page
|
||||
let isSetupAlready = undefined;
|
||||
if (credential && app.categories.includes("payment")) {
|
||||
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]()) as PaymentApp | null;
|
||||
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null;
|
||||
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
|
||||
const PaymentService = paymentApp.lib.PaymentService;
|
||||
const paymentInstance = new PaymentService(credential);
|
||||
|
|
|
@ -302,7 +302,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
|||
// Posible to refactor TODO:
|
||||
const paymentApp = (await appStore[
|
||||
paymentAppCredential?.app?.dirName as keyof typeof appStore
|
||||
]()) as PaymentApp;
|
||||
]?.()) as PaymentApp;
|
||||
if (!paymentApp?.lib?.PaymentService) {
|
||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||
return null;
|
||||
|
|
|
@ -96,7 +96,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
|
|||
|
||||
const paymentApp = (await appStore[
|
||||
paymentCredential?.app?.dirName as keyof typeof appStore
|
||||
]()) as PaymentApp;
|
||||
]?.()) as PaymentApp;
|
||||
|
||||
if (!paymentApp?.lib?.PaymentService) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Payment service not found" });
|
||||
|
|
|
@ -276,6 +276,7 @@
|
|||
"LARK_OPEN_APP_ID",
|
||||
"LARK_OPEN_APP_SECRET",
|
||||
"LARK_OPEN_VERIFICATION_TOKEN",
|
||||
"MOCK_PAYMENT_APP_ENABLED",
|
||||
"MS_GRAPH_CLIENT_ID",
|
||||
"MS_GRAPH_CLIENT_SECRET",
|
||||
"NEXT_PUBLIC_API_URL",
|
||||
|
|
Loading…
Reference in New Issue
Block a user