From a28b9cacd28b4b76602b69583a57c05062b08cfe Mon Sep 17 00:00:00 2001 From: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:36:48 +0200 Subject: [PATCH] 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 --- .../getting-started/[[...step]]/page.tsx | 8 +- apps/web/app/future/payment/[uid]/page.tsx | 167 ++++++++++++++++++ apps/web/playwright/payment.e2e.ts | 68 +++++++ .../utils/bookingScenario/bookingScenario.ts | 133 +++++++------- package.json | 1 + .../components/EventTypeAppCardInterface.tsx | 12 +- packages/app-store/apps.browser.generated.tsx | 1 + .../app-store/apps.keys-schemas.generated.ts | 2 + packages/app-store/apps.metadata.generated.ts | 2 + packages/app-store/apps.schemas.generated.ts | 2 + packages/app-store/apps.server.generated.ts | 1 + packages/app-store/index.ts | 10 +- .../app-store/mock-payment-app/DESCRIPTION.md | 8 + .../app-store/mock-payment-app/api/add.ts | 16 ++ .../app-store/mock-payment-app/api/index.ts | 1 + .../components/EventTypeAppCardInterface.tsx | 136 ++++++++++++++ .../app-store/mock-payment-app/config.json | 18 ++ packages/app-store/mock-payment-app/index.ts | 2 + .../mock-payment-app/lib/PaymentService.ts | 141 +++++++++++++++ .../app-store/mock-payment-app/lib/index.ts | 1 + .../app-store/mock-payment-app/package.json | 14 ++ .../mock-payment-app/static/icon.svg | 1 + packages/app-store/mock-payment-app/zod.ts | 35 ++++ .../components/EventTypeAppCardInterface.tsx | 12 +- .../bookings/lib/handleCancelBooking.ts | 3 +- .../ee/payments/components/PaymentPage.tsx | 2 + .../pages/getClientSecretFromPayment.ts | 21 +++ .../features/ee/payments/pages/payment.tsx | 24 +-- packages/lib/payment/deletePayment.ts | 2 +- packages/lib/payment/handlePayment.ts | 2 +- .../loggedInViewer/integrations.handler.ts | 2 +- .../viewer/bookings/confirm.handler.ts | 2 +- .../viewer/payments/chargeCard.handler.ts | 2 +- turbo.json | 1 + 34 files changed, 748 insertions(+), 105 deletions(-) create mode 100644 apps/web/app/future/payment/[uid]/page.tsx create mode 100644 apps/web/playwright/payment.e2e.ts create mode 100644 packages/app-store/mock-payment-app/DESCRIPTION.md create mode 100644 packages/app-store/mock-payment-app/api/add.ts create mode 100644 packages/app-store/mock-payment-app/api/index.ts create mode 100644 packages/app-store/mock-payment-app/components/EventTypeAppCardInterface.tsx create mode 100644 packages/app-store/mock-payment-app/config.json create mode 100644 packages/app-store/mock-payment-app/index.ts create mode 100644 packages/app-store/mock-payment-app/lib/PaymentService.ts create mode 100644 packages/app-store/mock-payment-app/lib/index.ts create mode 100644 packages/app-store/mock-payment-app/package.json create mode 100644 packages/app-store/mock-payment-app/static/icon.svg create mode 100644 packages/app-store/mock-payment-app/zod.ts create mode 100644 packages/features/ee/payments/pages/getClientSecretFromPayment.ts diff --git a/apps/web/app/future/getting-started/[[...step]]/page.tsx b/apps/web/app/future/getting-started/[[...step]]/page.tsx index 1665eca319..b8ad1b6c84 100644 --- a/apps/web/app/future/getting-started/[[...step]]/page.tsx +++ b/apps/web/app/future/getting-started/[[...step]]/page.tsx @@ -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"); diff --git a/apps/web/app/future/payment/[uid]/page.tsx b/apps/web/app/future/payment/[uid]/page.tsx new file mode 100644 index 0000000000..23d343a3d2 --- /dev/null +++ b/apps/web/app/future/payment/[uid]/page.tsx @@ -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, + }; + + 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 }); diff --git a/apps/web/playwright/payment.e2e.ts b/apps/web/playwright/payment.e2e.ts new file mode 100644 index 0000000000..d4014a7156 --- /dev/null +++ b/apps/web/playwright/payment.e2e.ts @@ -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(); + }); + }); +}); diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 06271d0f8e..40e3d17662 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -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 { - 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 { - 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 => { - 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 { + 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 { + 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 => { + if (calendarData?.getAvailabilityCrash) { + throw new Error("MockCalendarService.getAvailability fake error"); + } + return new Promise((resolve) => { + resolve(calendarData?.busySlots || []); + }); + }, + }; + }, }, - }, - }); + }); return { createEventCalls, deleteEventCalls, diff --git a/package.json b/package.json index b45ec93a00..3b36d66293 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx index f6bf717dd6..3bc5e0e3fb 100644 --- a/packages/app-store/alby/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/alby/components/EventTypeAppCardInterface.tsx @@ -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(); const price = getAppData("price"); const currency = getAppData("currency"); diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index f4f9eedd75..19142d4eb5 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -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")), diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 541bb55bd3..e2fdde3852 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -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, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index e2c9f3e978..5e74a228cb 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.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, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 3e1e37634e..279ba0782c 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -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, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 55d544efdc..5c92f6400b 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.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"), diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 940e644994..c00d34704e 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -41,4 +41,12 @@ const appStore = { shimmervideo: () => import("./shimmervideo"), }; -export default appStore; +const exportedAppStore: typeof appStore & { + ["mock-payment-app"]?: () => Promise; +} = appStore; + +if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) { + exportedAppStore["mock-payment-app"] = () => import("./mock-payment-app/index"); +} + +export default exportedAppStore; diff --git a/packages/app-store/mock-payment-app/DESCRIPTION.md b/packages/app-store/mock-payment-app/DESCRIPTION.md new file mode 100644 index 0000000000..a4b15502c3 --- /dev/null +++ b/packages/app-store/mock-payment-app/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/mock-payment-app/api/add.ts b/packages/app-store/mock-payment-app/api/add.ts new file mode 100644 index 0000000000..6ab3106577 --- /dev/null +++ b/packages/app-store/mock-payment-app/api/add.ts @@ -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; diff --git a/packages/app-store/mock-payment-app/api/index.ts b/packages/app-store/mock-payment-app/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/mock-payment-app/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/mock-payment-app/components/EventTypeAppCardInterface.tsx b/packages/app-store/mock-payment-app/components/EventTypeAppCardInterface.tsx new file mode 100644 index 0000000000..677575cc0c --- /dev/null +++ b/packages/app-store/mock-payment-app/components/EventTypeAppCardInterface.tsx @@ -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(); + 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 ( + { + setRequirePayment(enabled); + }} + description={<>Add a mock payment to your events}> + <> + {recurringEventDefined ? ( + + ) : ( + requirePayment && ( + <> +
+ { + setAppData("price", Number(e.target.value) * 100); + if (selectedCurrency) { + setAppData("currency", selectedCurrency.value); + } + }} + value={price > 0 ? price / 100 : undefined} + /> +
+
+ +