diff --git a/.env.example b/.env.example index c0e3a2bb309fcb..00feb50d8bc2ff 100644 --- a/.env.example +++ b/.env.example @@ -242,6 +242,7 @@ E2E_TEST_MAILHOG_ENABLED= # Cloudflare Turnstile NEXT_PUBLIC_CLOUDFLARE_SITEKEY= +NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER= CLOUDFLARE_TURNSTILE_SECRET= # Set the following value to true if you wish to enable Team Impersonation @@ -367,9 +368,7 @@ APP_ROUTER_TEAM_ENABLED=0 APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0 APP_ROUTER_AUTH_LOGIN_ENABLED=0 APP_ROUTER_AUTH_LOGOUT_ENABLED=0 -APP_ROUTER_AUTH_NEW_ENABLED=0 APP_ROUTER_AUTH_SAML_ENABLED=0 -APP_ROUTER_AUTH_ERROR_ENABLED=0 APP_ROUTER_AUTH_PLATFORM_ENABLED=0 APP_ROUTER_AUTH_OAUTH2_ENABLED=0 diff --git a/.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch b/.yarn/patches/libphonenumber-js+1.11.18.patch similarity index 86% rename from .yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch rename to .yarn/patches/libphonenumber-js+1.11.18.patch index 6b0bfb2905d7d8..049f65a115e021 100644 --- a/.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch +++ b/.yarn/patches/libphonenumber-js+1.11.18.patch @@ -1,5 +1,5 @@ diff --git a/index.cjs b/index.cjs -index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644 +index c83f700..da6fc7e 100644 --- a/index.cjs +++ b/index.cjs @@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) { @@ -12,4 +12,4 @@ index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67 +// exports['default'] = min.parsePhoneNumberFromString // `parsePhoneNumberFromString()` named export is now considered legacy: - // it has been promoted to a default export due to being too verbose. + // it has been promoted to a default export due to being too verbose. \ No newline at end of file diff --git a/.yarn/versions/727b22e1.yml b/.yarn/versions/727b22e1.yml new file mode 100644 index 00000000000000..9164d55660135e --- /dev/null +++ b/.yarn/versions/727b22e1.yml @@ -0,0 +1,8 @@ +undecided: + - calcom-monorepo + - "@calcom/app-store-cli" + - "@calcom/platform-constants" + - "@calcom/platform-enums" + - "@calcom/platform-types" + - "@calcom/platform-utils" + - "@calcom/prisma" diff --git a/.yarn/versions/c2e72c83.yml b/.yarn/versions/c2e72c83.yml new file mode 100644 index 00000000000000..8eb32dd031656d --- /dev/null +++ b/.yarn/versions/c2e72c83.yml @@ -0,0 +1,7 @@ +undecided: + - "@calcom/app-store-cli" + - "@calcom/platform-constants" + - "@calcom/platform-enums" + - "@calcom/platform-types" + - "@calcom/platform-utils" + - "@calcom/prisma" diff --git a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts b/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts index 5b7caba9945e46..9a1e9a52770083 100644 --- a/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts +++ b/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts @@ -1,9 +1,12 @@ +import type { RatelimitResponse } from "@unkey/ratelimit"; import type { Request, Response } from "express"; import type { NextApiResponse, NextApiRequest } from "next"; import { createMocks } from "node-mocks-http"; import { describe, it, expect, vi } from "vitest"; +import { handleAutoLock } from "@calcom/lib/autoLock"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { HttpError } from "@calcom/lib/http-error"; import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey"; @@ -14,6 +17,10 @@ vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({ checkRateLimitAndThrowError: vi.fn(), })); +vi.mock("@calcom/lib/autoLock", () => ({ + handleAutoLock: vi.fn(), +})); + describe("rateLimitApiKey middleware", () => { it("should return 401 if no apiKey is provided", async () => { const { req, res } = createMocks({ @@ -55,17 +62,20 @@ describe("rateLimitApiKey middleware", () => { query: { apiKey: "test-key" }, }); - const rateLimiterResponse = { + const rateLimiterResponse: RatelimitResponse = { limit: 100, remaining: 99, reset: Date.now(), + success: true, }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - (checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => { - onRateLimiterResponse(rateLimiterResponse); - }); + (checkRateLimitAndThrowError as any).mockImplementationOnce( + ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { + onRateLimiterResponse(rateLimiterResponse); + } + ); // @ts-expect-error weird typing between middleware and createMocks await rateLimitApiKey(req, res, vi.fn() as any); @@ -89,4 +99,135 @@ describe("rateLimitApiKey middleware", () => { expect(res._getStatusCode()).toBe(429); expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" }); }); + + it("should lock API key when rate limit is repeatedly exceeded", async () => { + const { req, res } = createMocks({ + method: "GET", + query: { apiKey: "test-key" }, + }); + + const rateLimiterResponse: RatelimitResponse = { + success: false, + remaining: 0, + limit: 100, + reset: Date.now(), + }; + + // Mock rate limiter to trigger the onRateLimiterResponse callback + (checkRateLimitAndThrowError as any).mockImplementationOnce( + ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { + onRateLimiterResponse(rateLimiterResponse); + } + ); + + // Mock handleAutoLock to indicate the key was locked + vi.mocked(handleAutoLock).mockResolvedValueOnce(true); + + // @ts-expect-error weird typing between middleware and createMocks + await rateLimitApiKey(req, res, vi.fn() as any); + + expect(handleAutoLock).toHaveBeenCalledWith({ + identifier: "test-key", + identifierType: "apiKey", + rateLimitResponse: rateLimiterResponse, + }); + + expect(res._getStatusCode()).toBe(429); + expect(res._getJSONData()).toEqual({ message: "Too many requests" }); + }); + + it("should handle API key not found error during auto-lock", async () => { + const { req, res } = createMocks({ + method: "GET", + query: { apiKey: "test-key" }, + }); + + const rateLimiterResponse: RatelimitResponse = { + success: false, + remaining: 0, + limit: 100, + reset: Date.now(), + }; + + // Mock rate limiter to trigger the onRateLimiterResponse callback + (checkRateLimitAndThrowError as any).mockImplementationOnce( + ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { + onRateLimiterResponse(rateLimiterResponse); + } + ); + + // Mock handleAutoLock to throw a "No user found" error + vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key.")); + + // @ts-expect-error weird typing between middleware and createMocks + await rateLimitApiKey(req, res, vi.fn() as any); + + expect(handleAutoLock).toHaveBeenCalledWith({ + identifier: "test-key", + identifierType: "apiKey", + rateLimitResponse: rateLimiterResponse, + }); + + expect(res._getStatusCode()).toBe(401); + expect(res._getJSONData()).toEqual({ message: "No user found for this API key." }); + }); + + it("should continue if auto-lock returns false (not locked)", async () => { + const { req, res } = createMocks({ + method: "GET", + query: { apiKey: "test-key" }, + }); + + const rateLimiterResponse: RatelimitResponse = { + success: false, + remaining: 0, + limit: 100, + reset: Date.now(), + }; + + // Mock rate limiter to trigger the onRateLimiterResponse callback + (checkRateLimitAndThrowError as any).mockImplementationOnce( + ({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => { + onRateLimiterResponse(rateLimiterResponse); + } + ); + + // Mock handleAutoLock to indicate the key was not locked + vi.mocked(handleAutoLock).mockResolvedValueOnce(false); + + const next = vi.fn(); + // @ts-expect-error weird typing between middleware and createMocks + await rateLimitApiKey(req, res, next); + + expect(handleAutoLock).toHaveBeenCalledWith({ + identifier: "test-key", + identifierType: "apiKey", + rateLimitResponse: rateLimiterResponse, + }); + + // Verify headers were set but request continued + expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit); + expect(next).toHaveBeenCalled(); + }); + + it("should handle HttpError during rate limiting", async () => { + const { req, res } = createMocks({ + method: "GET", + query: { apiKey: "test-key" }, + }); + + // Mock checkRateLimitAndThrowError to throw HttpError + vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce( + new HttpError({ + statusCode: 429, + message: "Custom rate limit error", + }) + ); + + // @ts-expect-error weird typing between middleware and createMocks + await rateLimitApiKey(req, res, vi.fn() as any); + + expect(res._getStatusCode()).toBe(429); + expect(res._getJSONData()).toEqual({ message: "Custom rate limit error" }); + }); }); diff --git a/apps/api/v1/lib/helpers/rateLimitApiKey.ts b/apps/api/v1/lib/helpers/rateLimitApiKey.ts index 7a4ad03d9c4c8a..734ea27c8de8d8 100644 --- a/apps/api/v1/lib/helpers/rateLimitApiKey.ts +++ b/apps/api/v1/lib/helpers/rateLimitApiKey.ts @@ -1,6 +1,8 @@ import type { NextMiddleware } from "next-api-middleware"; +import { handleAutoLock } from "@calcom/lib/autoLock"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; +import { HttpError } from "@calcom/lib/http-error"; export const rateLimitApiKey: NextMiddleware = async (req, res, next) => { if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" }); @@ -10,14 +12,34 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => { await checkRateLimitAndThrowError({ identifier: req.query.apiKey as string, rateLimitingType: "api", - onRateLimiterResponse: (response) => { + onRateLimiterResponse: async (response) => { res.setHeader("X-RateLimit-Limit", response.limit); res.setHeader("X-RateLimit-Remaining", response.remaining); res.setHeader("X-RateLimit-Reset", response.reset); + + try { + const didLock = await handleAutoLock({ + identifier: req.query.apiKey as string, // Casting as this is verified in another middleware + identifierType: "apiKey", + rateLimitResponse: response, + }); + + if (didLock) { + return res.status(429).json({ message: "Too many requests" }); + } + } catch (error) { + if (error instanceof Error && error.message === "No user found for this API key.") { + return res.status(401).json({ message: error.message }); + } + throw error; + } }, }); } catch (error) { - res.status(429).json({ message: "Rate limit exceeded" }); + if (error instanceof HttpError) { + return res.status(error.statusCode).json({ message: error.message }); + } + return res.status(429).json({ message: "Rate limit exceeded" }); } await next(); diff --git a/apps/api/v1/lib/validations/shared/jsonSchema.ts b/apps/api/v1/lib/validations/shared/jsonSchema.ts index bfc7ae1f2b9feb..423b28e95528db 100644 --- a/apps/api/v1/lib/validations/shared/jsonSchema.ts +++ b/apps/api/v1/lib/validations/shared/jsonSchema.ts @@ -1,9 +1,11 @@ import { z } from "zod"; // Helper schema for JSON fields -type Literal = boolean | number | string; +type Literal = boolean | number | string | null; type Json = Literal | { [key: string]: Json } | Json[]; -const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +const jsonObjectSchema = z.record(z.lazy(() => jsonSchema)); +const jsonArraySchema = z.array(z.lazy(() => jsonSchema)); export const jsonSchema: z.ZodSchema = z.lazy(() => - z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) + z.union([literalSchema, jsonObjectSchema, jsonArraySchema]) ); diff --git a/apps/api/v1/pages/api/teams/_post.ts b/apps/api/v1/pages/api/teams/_post.ts index 0a4807fa18467b..3e8b116070f539 100644 --- a/apps/api/v1/pages/api/teams/_post.ts +++ b/apps/api/v1/pages/api/teams/_post.ts @@ -2,6 +2,7 @@ import type { NextApiRequest } from "next"; import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer"; import stripe from "@calcom/app-store/stripepayment/lib/server"; +import { getDubCustomer } from "@calcom/features/auth/lib/dub"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { HttpError } from "@calcom/lib/http-error"; @@ -192,11 +193,26 @@ const generateTeamCheckoutSession = async ({ pendingPaymentTeamId: number; ownerId: number; }) => { - const customer = await getStripeCustomerIdFromUserId(ownerId); + const [customer, dubCustomer] = await Promise.all([ + getStripeCustomerIdFromUserId(ownerId), + getDubCustomer(ownerId.toString()), + ]); + const session = await stripe.checkout.sessions.create({ customer, mode: "subscription", - allow_promotion_codes: true, + ...(dubCustomer?.discount?.couponId + ? { + discounts: [ + { + coupon: + process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId + ? dubCustomer.discount.couponTestId + : dubCustomer.discount.couponId, + }, + ], + } + : { allow_promotion_codes: true }), success_url: `${WEBAPP_URL}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${WEBAPP_URL}/settings/my-account/profile`, line_items: [ diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 5c7c639c4d876f..d3c48f3772bbd4 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -29,7 +29,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.85", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.86", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 9cf42f32e216af..5f9d1f1ba44b8f 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -220,6 +220,7 @@ describe("OAuth Client Users Endpoints", () => { await userConnectedToOAuth(responseBody.data.user.email); await userHasDefaultEventTypes(responseBody.data.user.id); await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId); + await userHasOnlyOneSchedule(responseBody.data.user.id); }); async function userConnectedToOAuth(userEmail: string) { @@ -260,6 +261,11 @@ describe("OAuth Client Users Endpoints", () => { expect(schedule?.userId).toEqual(userId); } + async function userHasOnlyOneSchedule(userId: number) { + const schedules = await schedulesRepositoryFixture.getByUserId(userId); + expect(schedules?.length).toEqual(1); + } + it(`should fail /POST using already used managed user email`, async () => { const requestBody: CreateManagedUserInput = { email: userEmail, diff --git a/apps/api/v2/test/fixtures/repository/schedules.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/schedules.repository.fixture.ts index f6ee3d998873aa..7ece0dd3bc52ff 100644 --- a/apps/api/v2/test/fixtures/repository/schedules.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/schedules.repository.fixture.ts @@ -29,4 +29,8 @@ export class SchedulesRepositoryFixture { async deleteAvailabilities(scheduleId: Schedule["id"]) { return this.prismaWriteClient.availability.deleteMany({ where: { scheduleId } }); } + + async getByUserId(userId: Schedule["userId"]) { + return this.prismaReadClient.schedule.findMany({ where: { userId } }); + } } diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 27938472378999..2c0d74bc867f18 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -13,9 +13,7 @@ const ROUTES: [URLPattern, boolean][] = [ ["/auth/forgot-password/:path*", process.env.APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED === "1"] as const, ["/auth/login", process.env.APP_ROUTER_AUTH_LOGIN_ENABLED === "1"] as const, ["/auth/logout", process.env.APP_ROUTER_AUTH_LOGOUT_ENABLED === "1"] as const, - ["/auth/new", process.env.APP_ROUTER_AUTH_NEW_ENABLED === "1"] as const, ["/auth/saml-idp", process.env.APP_ROUTER_AUTH_SAML_ENABLED === "1"] as const, - ["/auth/error", process.env.APP_ROUTER_AUTH_ERROR_ENABLED === "1"] as const, ["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const, ["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const, ["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const, diff --git a/apps/web/app/auth/error/page.tsx b/apps/web/app/auth/error/page.tsx new file mode 100644 index 00000000000000..e3942ce67beaa8 --- /dev/null +++ b/apps/web/app/auth/error/page.tsx @@ -0,0 +1,47 @@ +import type { PageProps } from "app/_types"; +import { _generateMetadata, getTranslate } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; +import Link from "next/link"; +import { z } from "zod"; + +import { Button, Icon } from "@calcom/ui"; + +import AuthContainer from "@components/ui/AuthContainer"; + +export const generateMetadata = async () => { + return await _generateMetadata( + (t) => t("error"), + () => "" + ); +}; + +const querySchema = z.object({ + error: z.string().optional(), +}); + +const ServerPage = async ({ searchParams }: PageProps) => { + const t = await getTranslate(); + const { error } = querySchema.parse({ error: searchParams?.error || undefined }); + const errorMsg = error || t("error_during_login"); + return ( + +
+
+ +
+
+ +
+
+
+ + + +
+
+ ); +}; + +export default WithLayout({ ServerPage })<"P">; diff --git a/apps/web/app/future/auth/new/page.tsx b/apps/web/app/auth/new/page.tsx similarity index 100% rename from apps/web/app/future/auth/new/page.tsx rename to apps/web/app/auth/new/page.tsx diff --git a/apps/web/app/future/auth/oauth2/authorize/page.tsx b/apps/web/app/auth/oauth2/authorize/page.tsx similarity index 91% rename from apps/web/app/future/auth/oauth2/authorize/page.tsx rename to apps/web/app/auth/oauth2/authorize/page.tsx index 3fa8dca2358437..6cdb42ab56b3a1 100644 --- a/apps/web/app/future/auth/oauth2/authorize/page.tsx +++ b/apps/web/app/auth/oauth2/authorize/page.tsx @@ -5,7 +5,7 @@ import Page from "~/auth/oauth2/authorize-view"; export const generateMetadata = async () => { return await _generateMetadata( - () => "Authorize", + (t) => t("authorize"), () => "" ); }; diff --git a/apps/web/app/future/auth/platform/authorize/page.tsx b/apps/web/app/auth/platform/authorize/page.tsx similarity index 91% rename from apps/web/app/future/auth/platform/authorize/page.tsx rename to apps/web/app/auth/platform/authorize/page.tsx index 4f263ae4c03d54..898959c4ef5117 100644 --- a/apps/web/app/future/auth/platform/authorize/page.tsx +++ b/apps/web/app/auth/platform/authorize/page.tsx @@ -5,7 +5,7 @@ import Page from "~/auth/platform/authorize-view"; export const generateMetadata = async () => { return await _generateMetadata( - () => "Authorize", + (t) => t("authorize"), () => "" ); }; diff --git a/apps/web/app/future/auth/signin/page.tsx b/apps/web/app/auth/signin/page.tsx similarity index 100% rename from apps/web/app/future/auth/signin/page.tsx rename to apps/web/app/auth/signin/page.tsx diff --git a/apps/web/app/event-types/page.tsx b/apps/web/app/event-types/page.tsx index 2dc466826cc0b2..b504f3ca5d5638 100644 --- a/apps/web/app/event-types/page.tsx +++ b/apps/web/app/event-types/page.tsx @@ -1,8 +1,14 @@ -import { withAppDirSsr } from "app/WithAppDirSsr"; +import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { cookies, headers } from "next/headers"; +import { redirect } from "next/navigation"; -import { getServerSideProps } from "@lib/event-types/getServerSideProps"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; + +import { buildLegacyCtx } from "@lib/buildLegacyCtx"; + +import { ssrInit } from "@server/lib/ssr"; import EventTypes from "~/event-types/views/event-types-listing-view"; @@ -12,6 +18,16 @@ export const generateMetadata = async () => (t) => t("event_types_page_subtitle") ); -const getData = withAppDirSsr(getServerSideProps); +const Page = async ({ params, searchParams }: PageProps) => { + const context = buildLegacyCtx(headers(), cookies(), params, searchParams); + const session = await getServerSessionForAppDir(); + if (!session?.user?.id) { + redirect("/auth/login"); + } + + await ssrInit(context); + + return ; +}; -export default WithLayout({ getLayout: null, getData, Page: EventTypes })<"P">; +export default WithLayout({ ServerPage: Page })<"P">; diff --git a/apps/web/app/future/auth/error/page.tsx b/apps/web/app/future/auth/error/page.tsx deleted file mode 100644 index 12b2b5c97b512f..00000000000000 --- a/apps/web/app/future/auth/error/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { withAppDirSsg } from "app/WithAppDirSsg"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; - -import { getStaticProps } from "@server/lib/auth/error/getStaticProps"; - -import Page from "~/auth/error/error-view"; - -export const generateMetadata = async () => { - return await _generateMetadata( - () => "Error", - () => "" - ); -}; - -const getData = withAppDirSsg(getStaticProps, "future/auth/error"); - -export default WithLayout({ getData, Page, getLayout: null })<"P">; -export const dynamic = "force-static"; diff --git a/apps/web/app/layoutHOC.tsx b/apps/web/app/layoutHOC.tsx index 167219f84ca3a2..6e580e3dea02d9 100644 --- a/apps/web/app/layoutHOC.tsx +++ b/apps/web/app/layoutHOC.tsx @@ -52,7 +52,6 @@ export function WithLayout>({ requiresLicense={requiresLicense || !!(Page && "requiresLicense" in Page && Page.requiresLicense)} nonce={nonce} themeBasis={null} - isThemeSupported={Page && "isThemeSupported" in Page ? (Page.isThemeSupported as boolean) : undefined} isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)} {...props}> {pageWithServerLayout} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 95a9a8c97ec5e5..a3bfdee39102b4 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -20,7 +20,22 @@ enum PageType { function getPageInfo(pathname: string, host: string) { const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({ hostname: host }); const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? []; - if (!routerUsername || (isValidOrgDomain && currentOrgDomain)) { + if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) { + const splitPath = routerUsername.split("/"); + if (splitPath[1] === "team" && splitPath.length === 3) { + return { + username: splitPath[2], + pageType: PageType.TEAM, + url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`, + }; + } else { + return { + username: routerUsername, + pageType: PageType.USER, + url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`, + }; + } + } else { return { username: currentOrgDomain ?? "", pageType: PageType.ORG, @@ -29,21 +44,6 @@ function getPageInfo(pathname: string, host: string) { }`, }; } - - const splitPath = routerUsername.split("/"); - if (splitPath[1] === "team" && splitPath.length === 3) { - return { - username: splitPath[2], - pageType: PageType.TEAM, - url: `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`, - }; - } - - return { - username: routerUsername, - pageType: PageType.USER, - url: `${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`, - }; } async function NotFound() { @@ -126,7 +126,8 @@ async function NotFound() { {username ? ( <> - {username} {t("is_still_available")}{" "} + {username} + {` ${t("is_still_available")} `} {t("register_now")}. ) : null} diff --git a/apps/web/app/org/[orgSlug]/page.tsx b/apps/web/app/org/[orgSlug]/page.tsx index 99b51c0aa24735..215b04ad5feb5a 100644 --- a/apps/web/app/org/[orgSlug]/page.tsx +++ b/apps/web/app/org/[orgSlug]/page.tsx @@ -1 +1,4 @@ -export { default, generateMetadata } from "app/team/[slug]/page"; +import Page from "app/team/[slug]/page"; + +export { generateMetadata } from "app/team/[slug]/page"; +export default Page; diff --git a/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx b/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx index ebcf985632da3a..ebc0046999ea79 100644 --- a/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx +++ b/apps/web/app/org/[orgSlug]/team/[slug]/[type]/page.tsx @@ -1 +1,4 @@ -export { default, generateMetadata } from "app/team/[slug]/[type]/page"; +import Page from "app/team/[slug]/[type]/page"; + +export { generateMetadata } from "app/team/[slug]/[type]/page"; +export default Page; diff --git a/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx b/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx index 99b51c0aa24735..215b04ad5feb5a 100644 --- a/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx +++ b/apps/web/app/org/[orgSlug]/team/[slug]/page.tsx @@ -1 +1,4 @@ -export { default, generateMetadata } from "app/team/[slug]/page"; +import Page from "app/team/[slug]/page"; + +export { generateMetadata } from "app/team/[slug]/page"; +export default Page; diff --git a/apps/web/components/PageWrapperAppDir.tsx b/apps/web/components/PageWrapperAppDir.tsx index f97125fe16d7e4..a85bac21c1dded 100644 --- a/apps/web/components/PageWrapperAppDir.tsx +++ b/apps/web/components/PageWrapperAppDir.tsx @@ -18,7 +18,6 @@ export type PageWrapperProps = Readonly<{ nonce: string | undefined; themeBasis: string | null; dehydratedState?: DehydratedState; - isThemeSupported?: boolean; isBookingPage?: boolean; i18n?: SSRConfig; }>; diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index e6940207856386..ee86420da8e3be 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -68,6 +68,7 @@ type BookingItemProps = BookingItem & { userTimeFormat: number | null | undefined; userEmail: string | undefined; }; + isToday: boolean; }; type ParsedBooking = ReturnType; @@ -511,7 +512,10 @@ function BookingListItem(booking: BookingItemProps) { -
+
diff --git a/apps/web/components/ui/AuthContainer.tsx b/apps/web/components/ui/AuthContainer.tsx index 605875bfc28776..5063234637a13e 100644 --- a/apps/web/components/ui/AuthContainer.tsx +++ b/apps/web/components/ui/AuthContainer.tsx @@ -11,12 +11,13 @@ interface Props { showLogo?: boolean; heading?: string; loading?: boolean; + isAppDir?: boolean; } export default function AuthContainer(props: React.PropsWithChildren) { return (
- + {!props.isAppDir ? : null} {props.showLogo && }
diff --git a/apps/web/lib/app-providers-app-dir.tsx b/apps/web/lib/app-providers-app-dir.tsx index 9d6a18b8ab1f80..516c840535b6ec 100644 --- a/apps/web/lib/app-providers-app-dir.tsx +++ b/apps/web/lib/app-providers-app-dir.tsx @@ -20,6 +20,7 @@ import { FeatureProvider } from "@calcom/features/flags/context/provider"; import { useFlags } from "@calcom/features/flags/hooks"; import useIsBookingPage from "@lib/hooks/useIsBookingPage"; +import useIsThemeSupported from "@lib/hooks/useIsThemeSupported"; import PlainChat from "@lib/plain/plainChat"; import type { WithLocaleProps } from "@lib/withLocale"; import type { WithNonceProps } from "@lib/withNonce"; @@ -48,7 +49,6 @@ export type AppProps = Omit< > & { Component: NextAppProps["Component"] & { requiresLicense?: boolean; - isThemeSupported?: boolean; isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean); getLayout?: (page: React.ReactElement) => ReactNode; PageWrapper?: (props: AppProps) => JSX.Element; @@ -129,7 +129,7 @@ type CalcomThemeProps = Readonly<{ themeBasis: string | null; nonce: string | undefined; children: React.ReactNode; - isThemeSupported?: boolean; + isThemeSupported: boolean; }>; const CalcomThemeProvider = (props: CalcomThemeProps) => { @@ -196,8 +196,7 @@ function getThemeProviderProps({ }) { const themeSupport = props.isBookingPage ? ThemeSupport.Booking - : // if isThemeSupported is explicitly false, we don't use theme there - props.isThemeSupported === false + : props.isThemeSupported === false ? ThemeSupport.None : ThemeSupport.App; @@ -263,6 +262,7 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) { const AppProviders = (props: PageWrapperProps) => { // No need to have intercom on public pages - Good for Page Performance const isBookingPage = useIsBookingPage(); + const isThemeSupported = useIsThemeSupported(); const RemainingProviders = ( @@ -273,7 +273,7 @@ const AppProviders = (props: PageWrapperProps) => { {props.children} diff --git a/apps/web/lib/event-types/getServerSideProps.tsx b/apps/web/lib/event-types/getServerSideProps.tsx deleted file mode 100644 index 76726887e58554..00000000000000 --- a/apps/web/lib/event-types/getServerSideProps.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { GetServerSidePropsContext } from "next"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; - -import { ssrInit } from "@server/lib/ssr"; - -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const ssr = await ssrInit(context); - const session = await getServerSession({ req: context.req }); - - if (!session) { - return { - redirect: { - permanent: false, - destination: "/auth/login", - }, - }; - } - - return { props: { trpcState: ssr.dehydrate() } }; -}; diff --git a/apps/web/lib/hooks/useIsThemeSupported.ts b/apps/web/lib/hooks/useIsThemeSupported.ts new file mode 100644 index 00000000000000..766a91c9025ed7 --- /dev/null +++ b/apps/web/lib/hooks/useIsThemeSupported.ts @@ -0,0 +1,14 @@ +"use client"; + +import { usePathname } from "next/navigation"; + +const THEME_UNSUPPORTED_ROUTES = ["/auth/setup"]; + +export default function useIsThemeSupported(): boolean { + const pathname = usePathname(); + + // Check if current pathname matches any unsupported route + const isUnsupportedRoute = THEME_UNSUPPORTED_ROUTES.some((route) => pathname?.startsWith(route)); + + return !isUnsupportedRoute; +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 94f36de3214a88..027ba5cc6bcb4f 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -167,7 +167,10 @@ export const config = { "/api/trpc/:path*", "/login", "/auth/login", - "/future/auth/login", + "/auth/error", + "/auth/signin", + "/auth/oauth2/authorize", + "/auth/platform/authorize", /** * Paths required by routingForms.handle */ diff --git a/apps/web/modules/auth/error/error-view.tsx b/apps/web/modules/auth/error/error-view.tsx index 2d034a53a0d3d5..967bc20e41fab9 100644 --- a/apps/web/modules/auth/error/error-view.tsx +++ b/apps/web/modules/auth/error/error-view.tsx @@ -9,13 +9,11 @@ import { Button, Icon } from "@calcom/ui"; import AuthContainer from "@components/ui/AuthContainer"; -import type { PageProps } from "@server/lib/auth/error/getStaticProps"; - const querySchema = z.object({ error: z.string().optional(), }); -export default function Error(props: PageProps) { +export default function Error() { const { t } = useLocale(); const searchParams = useSearchParams(); const { error } = querySchema.parse({ error: searchParams?.get("error") || undefined }); diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 0604814265cc61..36e4aaba69dc4e 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -8,7 +8,7 @@ import { createColumnHelper, } from "@tanstack/react-table"; import type { ReactElement } from "react"; -import { Fragment, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import type { z } from "zod"; import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components"; @@ -85,10 +85,16 @@ export default function Bookings(props: BookingsProps) { ); } -type RowData = { - booking: BookingOutput; - recurringInfo?: RecurringInfo; -}; +type RowData = + | { + type: "data"; + booking: BookingOutput; + isToday: boolean; + recurringInfo?: RecurringInfo; + } + | { + type: "today" | "next"; + }; function BookingsContent({ status }: BookingsProps) { const { data: filterQuery } = useFilterQuery(); @@ -118,88 +124,124 @@ function BookingsContent({ status }: BookingsProps) { columnHelper.display({ id: "custom-view", cell: (props) => { - const { booking, recurringInfo } = props.row.original; - return ( - - ); + if (props.row.original.type === "data") { + const { booking, recurringInfo, isToday } = props.row.original; + return ( + + ); + } else if (props.row.original.type === "today") { + return ( +

+ {t("today")} +

+ ); + } else if (props.row.original.type === "next") { + return ( +

+ {t("next")} +

+ ); + } }, }), ]; }, [user, status]); - const isEmpty = !query.data?.pages[0]?.bookings.length; + const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]); - const shownBookings: Record = {}; - const filterBookings = (booking: BookingOutput) => { - if (status === "recurring" || status == "unconfirmed" || status === "cancelled") { - if (!booking.recurringEventId) { - return true; - } - if ( - shownBookings[booking.recurringEventId] !== undefined && - shownBookings[booking.recurringEventId].length > 0 - ) { - shownBookings[booking.recurringEventId].push(booking); - return false; + const flatData = useMemo(() => { + const shownBookings: Record = {}; + const filterBookings = (booking: BookingOutput) => { + if (status === "recurring" || status == "unconfirmed" || status === "cancelled") { + if (!booking.recurringEventId) { + return true; + } + if ( + shownBookings[booking.recurringEventId] !== undefined && + shownBookings[booking.recurringEventId].length > 0 + ) { + shownBookings[booking.recurringEventId].push(booking); + return false; + } + shownBookings[booking.recurringEventId] = [booking]; + } else if (status === "upcoming") { + return ( + dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !== + dayjs().tz(user?.timeZone).format("YYYY-MM-DD") + ); } - shownBookings[booking.recurringEventId] = [booking]; - } else if (status === "upcoming") { - return ( - dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !== - dayjs().tz(user?.timeZone).format("YYYY-MM-DD") - ); - } - return true; - }; + return true; + }; - const flatData = useMemo(() => { return ( query.data?.pages.flatMap((page) => page.bookings.filter(filterBookings).map((booking) => ({ + type: "data", booking, recurringInfo: page.recurringInfo.find( (info) => info.recurringEventId === booking.recurringEventId ), + isToday: false, })) ) || [] ); }, [query.data]); + const bookingsToday = useMemo(() => { + return ( + query.data?.pages.map((page) => + page.bookings + .filter( + (booking: BookingOutput) => + dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") === + dayjs().tz(user?.timeZone).format("YYYY-MM-DD") + ) + .map((booking) => ({ + type: "data" as const, + booking, + recurringInfo: page.recurringInfo.find( + (info) => info.recurringEventId === booking.recurringEventId + ), + isToday: true, + })) + )[0] || [] + ); + }, [query.data]); + + const finalData = useMemo(() => { + if (bookingsToday.length > 0 && status === "upcoming") { + const merged: RowData[] = [ + { type: "today" as const }, + ...bookingsToday, + { type: "next" as const }, + ...flatData, + ]; + return merged; + } else { + return flatData; + } + }, [bookingsToday, flatData, status]); + const table = useReactTable({ - data: flatData, + data: finalData, columns, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), }); - let recurringInfoToday: RecurringInfo | undefined; - - const bookingsToday = - query.data?.pages.map((page) => - page.bookings.filter((booking: BookingOutput) => { - recurringInfoToday = page.recurringInfo.find( - (info) => info.recurringEventId === booking.recurringEventId - ); - - return ( - dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") === - dayjs().tz(user?.timeZone).format("YYYY-MM-DD") - ); - }) - )[0] || []; - return (
@@ -216,34 +258,8 @@ function BookingsContent({ status }: BookingsProps) { {query.status === "success" && !isEmpty && ( <> {!!bookingsToday.length && status === "upcoming" && ( -
- -

{t("today")}

-
-
- - {bookingsToday.map((booking: BookingOutput) => ( - - ))} - -
-
-
+ )} -
)} - diff --git a/apps/web/modules/d/[link]/d-type-view.tsx b/apps/web/modules/d/[link]/d-type-view.tsx index 46f7315074293d..d4b781b162dd34 100644 --- a/apps/web/modules/d/[link]/d-type-view.tsx +++ b/apps/web/modules/d/[link]/d-type-view.tsx @@ -2,7 +2,6 @@ import { Booker } from "@calcom/atoms/monorepo"; import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses"; -import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo"; import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps"; @@ -20,13 +19,6 @@ export default function Type({ }: PageProps) { return (
- { const { t } = useLocale(); - const searchParams = useSearchParams(); const { data: user } = useMeQuery(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_showProfileBanner, setShowProfileBanner] = useState(false); diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index a573bb3ce36650..84e2abca13ce99 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -58,7 +58,7 @@ const signupSchema = apiSignupSchema.extend({ cfToken: z.string().optional(), }); -const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false }); +const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); type FormValues = z.infer; diff --git a/apps/web/modules/team/team-view.tsx b/apps/web/modules/team/team-view.tsx index 59feba41e73606..704783128355e7 100644 --- a/apps/web/modules/team/team-view.tsx +++ b/apps/web/modules/team/team-view.tsx @@ -12,7 +12,6 @@ import { usePathname } from "next/navigation"; import { useEffect } from "react"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription"; import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -20,7 +19,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import useTheme from "@calcom/lib/hooks/useTheme"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui"; +import { Avatar, Button, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; import type { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; @@ -29,14 +28,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps"; import Team from "@components/team/screens/Team"; export type PageProps = inferSSRProps; -function TeamPage({ - team, - considerUnpublished, - markdownStrippedBio, - isValidOrgDomain, - currentOrgDomain, - isSEOIndexable, -}: PageProps) { +function TeamPage({ team, considerUnpublished, isValidOrgDomain }: PageProps) { useTheme(team.theme); const routerQuery = useRouterQuery(); const pathname = usePathname(); @@ -165,22 +157,6 @@ function TeamPage({ return ( <> -
diff --git a/apps/web/package.json b/apps/web/package.json index 80941e67e2a596..c7daaa08916937 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "4.9.1", + "version": "4.9.3", "private": true, "scripts": { "analyze": "ANALYZE=true next build", @@ -94,7 +94,7 @@ "ics": "^2.37.0", "jose": "^4.13.1", "kbar": "^0.1.0-beta.36", - "libphonenumber-js": "^1.10.51", + "libphonenumber-js": "^1.11.18", "lodash": "^4.17.21", "lottie-react": "^2.3.1", "markdown-it": "^13.0.1", diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 21654b543e76ed..222e675fa72e63 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -6,10 +6,18 @@ import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { defaultResponder } from "@calcom/lib/server"; +import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { const userIp = getIP(req); + if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { + await checkCfTurnstileToken({ + token: req.body["cfToken"] as string, + remoteIp: userIp, + }); + } + await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: userIp, diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 196c773d482fbb..3cdc3e28c3a185 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -6,12 +6,20 @@ import type { BookingResponse } from "@calcom/features/bookings/types"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { defaultResponder } from "@calcom/lib/server"; +import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; // @TODO: Didn't look at the contents of this function in order to not break old booking page. async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) { const userIp = getIP(req); + if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { + await checkCfTurnstileToken({ + token: req.body[0]["cfToken"] as string, + remoteIp: userIp, + }); + } + await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: userIp, diff --git a/apps/web/pages/auth/error.tsx b/apps/web/pages/auth/error.tsx deleted file mode 100644 index 2995d07aafb97b..00000000000000 --- a/apps/web/pages/auth/error.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import PageWrapper from "@components/PageWrapper"; - -import type { PageProps } from "@server/lib/auth/error/getStaticProps"; -import { getStaticProps } from "@server/lib/auth/error/getStaticProps"; - -import Error from "~/auth/error/error-view"; - -const Page = (props: PageProps) => ; -Page.PageWrapper = PageWrapper; -export default Page; -export { getStaticProps }; diff --git a/apps/web/pages/auth/new.tsx b/apps/web/pages/auth/new.tsx deleted file mode 100644 index ea4eb236699403..00000000000000 --- a/apps/web/pages/auth/new.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export default function NewUserPage() { - if (typeof window !== "undefined") { - window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com"); - } - return null; -} diff --git a/apps/web/pages/auth/oauth2/authorize.tsx b/apps/web/pages/auth/oauth2/authorize.tsx deleted file mode 100644 index 0783bd442d9296..00000000000000 --- a/apps/web/pages/auth/oauth2/authorize.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import PageWrapper from "@components/PageWrapper"; - -import Authorize from "~/auth/oauth2/authorize-view"; - -const Page = () => ; -Page.PageWrapper = PageWrapper; -export default Page; diff --git a/apps/web/pages/auth/platform/authorize.tsx b/apps/web/pages/auth/platform/authorize.tsx deleted file mode 100644 index fd68c83625a1b4..00000000000000 --- a/apps/web/pages/auth/platform/authorize.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import PageWrapper from "@components/PageWrapper"; - -import Authorize from "~/auth/platform/authorize-view"; - -const Page = () => ; -Page.PageWrapper = PageWrapper; -export default Page; diff --git a/apps/web/pages/auth/signin.tsx b/apps/web/pages/auth/signin.tsx deleted file mode 100644 index 7ce8e789e85cda..00000000000000 --- a/apps/web/pages/auth/signin.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import PageWrapper from "@components/PageWrapper"; - -import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps"; - -import type { PageProps } from "~/auth/signin-view"; -import SignIn from "~/auth/signin-view"; - -const Page = (props: PageProps) => ; - -Page.PageWrapper = PageWrapper; - -export default Page; - -export { getServerSideProps }; diff --git a/apps/web/playwright/wipe-my-cal.e2e.ts b/apps/web/playwright/wipe-my-cal.e2e.ts index 00c754f85c1ec6..a526a1e2f39f43 100644 --- a/apps/web/playwright/wipe-my-cal.e2e.ts +++ b/apps/web/playwright/wipe-my-cal.e2e.ts @@ -42,9 +42,9 @@ test.describe("Wipe my Cal App Test", () => { await page.goto("/bookings/upcoming"); await expect(page.locator("data-testid=wipe-today-button")).toBeVisible(); - const $openBookingCount = await page.locator('[data-testid="bookings"] > *').count(); - const $todayBookingCount = await page.locator('[data-testid="today-bookings"] > *').count(); - expect($openBookingCount + $todayBookingCount).toBe(3); + const $nonTodayBookingCount = await page.locator('[data-today="false"]').count(); + const $todayBookingCount = await page.locator('[data-today="true"]').count(); + expect($nonTodayBookingCount + $todayBookingCount).toBe(3); await page.locator("data-testid=wipe-today-button").click(); diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index d99257c12c1039..3ba3f489f227fe 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "عرض جميع المستخدمين المُدارين الذين تم إنشاؤهم بواسطة عميل OAuth الخاص بك", "select_oAuth_client": "اختر عميل OAuth", "on_every_instance": "في كل مثيل", + "next": "التالي", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/az/common.json b/apps/web/public/static/locales/az/common.json index 7612b7b0cc75a5..ab56b431794472 100644 --- a/apps/web/public/static/locales/az/common.json +++ b/apps/web/public/static/locales/az/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "OAuth müştəriniz tərəfindən yaradılmış bütün idarə edilən istifadəçiləri görün", "select_oAuth_client": "OAuth müştərisini seçin", "on_every_instance": "Hər nümunədə", + "next": "Növbəti", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/bg/common.json b/apps/web/public/static/locales/bg/common.json index 73b1f41b0a747c..559ea60e9292fe 100644 --- a/apps/web/public/static/locales/bg/common.json +++ b/apps/web/public/static/locales/bg/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Вижте всички управлявани потребители, създадени от вашия OAuth клиент", "select_oAuth_client": "Изберете OAuth клиент", "on_every_instance": "При всяко изпълнение", + "next": "Напред", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ca/common.json b/apps/web/public/static/locales/ca/common.json index 81a9cf478eb5e2..4f37cdb08da408 100644 --- a/apps/web/public/static/locales/ca/common.json +++ b/apps/web/public/static/locales/ca/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Veure tots els usuaris gestionats creats pel teu client OAuth", "select_oAuth_client": "Selecciona el client OAuth", "on_every_instance": "A cada instància", + "next": "Següent", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index b4147d729e0494..6edb2a5ad777c8 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Zobrazit všechny spravované uživatele vytvořené vaším OAuth klientem", "select_oAuth_client": "Vybrat OAuth klienta", "on_every_instance": "Na každé instanci", + "next": "Další", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/da/common.json b/apps/web/public/static/locales/da/common.json index 67cb3ffc6b586c..1f5df0dbc25cdd 100644 --- a/apps/web/public/static/locales/da/common.json +++ b/apps/web/public/static/locales/da/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Se alle administrerede brugere oprettet af din OAuth-klient", "select_oAuth_client": "Vælg OAuth-klient", "on_every_instance": "På hver forekomst", + "next": "Næste", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index a33cd6c136b137..a2df59f7d1a23f 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Sehen Sie alle verwalteten Benutzer, die von Ihrem OAuth-Client erstellt wurden", "select_oAuth_client": "OAuth-Client auswählen", "on_every_instance": "Bei jeder Instanz", + "next": "Weiter", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/el/common.json b/apps/web/public/static/locales/el/common.json index 505e5d87839219..44d8dfd9e86d28 100644 --- a/apps/web/public/static/locales/el/common.json +++ b/apps/web/public/static/locales/el/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Δείτε όλους τους διαχειριζόμενους χρήστες που δημιουργήθηκαν από το OAuth client σας", "select_oAuth_client": "Επιλέξτε OAuth Client", "on_every_instance": "Σε κάθε περίπτωση", + "next": "Επόμενο", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index e0babdb930de7d..062768ce006036 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1544,6 +1544,7 @@ "download_transcript": "Download Transcript", "recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download", "transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour", + "you_can_download_transcript_from_attachments": "You can also download transcript from attachments", "link_valid_for_12_hrs": "Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here.", "create_your_first_form": "Create your first form", "create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.", @@ -2929,5 +2930,7 @@ "managed_users_description": "See all the managed users created by your OAuth client", "select_oAuth_client": "Select Oauth Client", "on_every_instance": "On every instance", + "authorize": "Authorize", + "next": "Next", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/es-419/common.json b/apps/web/public/static/locales/es-419/common.json index a61d97d1ea180b..51175d7024a2a3 100644 --- a/apps/web/public/static/locales/es-419/common.json +++ b/apps/web/public/static/locales/es-419/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Ver todos los usuarios administrados creados por tu cliente OAuth", "select_oAuth_client": "Seleccionar cliente OAuth", "on_every_instance": "En cada instancia", + "next": "Siguiente", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index e36c947233e0b6..36a49177012185 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Ver todos los usuarios gestionados creados por tu cliente OAuth", "select_oAuth_client": "Seleccionar cliente OAuth", "on_every_instance": "En cada instancia", + "next": "Siguiente", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/et/common.json b/apps/web/public/static/locales/et/common.json index 76acd103382bd5..59114170396a03 100644 --- a/apps/web/public/static/locales/et/common.json +++ b/apps/web/public/static/locales/et/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Vaata kõiki hallatavaid kasutajaid, mis on loodud sinu OAuth kliendi poolt", "select_oAuth_client": "Vali OAuth klient", "on_every_instance": "Igal korral", + "next": "Edasi", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/eu/common.json b/apps/web/public/static/locales/eu/common.json index 8d4c9bf80cf524..44aaa7cc8e11a4 100644 --- a/apps/web/public/static/locales/eu/common.json +++ b/apps/web/public/static/locales/eu/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Ikusi zure OAuth bezeroak sortutako kudeatutako erabiltzaile guztiak", "select_oAuth_client": "Hautatu OAuth bezeroa", "on_every_instance": "Instantzia bakoitzean", + "next": "Hurrengoa", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/fi/common.json b/apps/web/public/static/locales/fi/common.json index 3c44db6769d683..218513cbbb826c 100644 --- a/apps/web/public/static/locales/fi/common.json +++ b/apps/web/public/static/locales/fi/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Näe kaikki OAuth-asiakassovelluksesi luomat hallinnoidut käyttäjät", "select_oAuth_client": "Valitse OAuth-asiakassovellus", "on_every_instance": "Jokaisessa instanssissa", + "next": "Seuraava", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index e1182b8b02cffe..c9372250802983 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Voir tous les utilisateurs gérés créés par votre client OAuth", "select_oAuth_client": "Sélectionner le client OAuth", "on_every_instance": "Sur chaque instance", + "next": "Suivant", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index fe45f39ca598a9..eb3b577e9e57c2 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "צפה בכל המשתמשים המנוהלים שנוצרו על ידי לקוח OAuth שלך", "select_oAuth_client": "בחר לקוח OAuth", "on_every_instance": "בכל מופע", + "next": "הבא", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/hu/common.json b/apps/web/public/static/locales/hu/common.json index ef3949459786cd..8b41d0f4945f50 100644 --- a/apps/web/public/static/locales/hu/common.json +++ b/apps/web/public/static/locales/hu/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Az OAuth kliense által létrehozott összes kezelt felhasználó megtekintése", "select_oAuth_client": "OAuth kliens kiválasztása", "on_every_instance": "Minden példányon", + "next": "Következő", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 9fad2844e33498..8e20016c858162 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Visualizza tutti gli utenti gestiti creati dal tuo client OAuth", "select_oAuth_client": "Seleziona client OAuth", "on_every_instance": "Su ogni istanza", + "next": "Avanti", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 27dcf13c6481ce..50a9bb97600105 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "OAuthクライアントによって作成された管理対象ユーザーをすべて表示", "select_oAuth_client": "OAuthクライアントを選択", "on_every_instance": "すべてのインスタンスで", + "next": "次へ", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/km/common.json b/apps/web/public/static/locales/km/common.json index 1d832210176017..22d8e40e2c99d1 100644 --- a/apps/web/public/static/locales/km/common.json +++ b/apps/web/public/static/locales/km/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "មើលអ្នកប្រើប្រាស់ដែលគ្រប់គ្រងទាំងអស់ដែលបានបង្កើតដោយ OAuth client របស់អ្នក", "select_oAuth_client": "ជ្រើសរើស OAuth Client", "on_every_instance": "នៅលើគ្រប់ករណី", + "next": "បន្ទាប់", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index 0230cf4983e741..2f1477f682475c 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "OAuth 클라이언트가 생성한 모든 관리 대상 사용자 보기", "select_oAuth_client": "OAuth 클라이언트 선택", "on_every_instance": "모든 인스턴스에서", + "next": "다음", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index deb494c6c02b9b..0d45e30a8fb773 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Bekijk alle beheerde gebruikers die zijn aangemaakt door je OAuth-client", "select_oAuth_client": "Selecteer OAuth-client", "on_every_instance": "Op elke instantie", + "next": "Volgende", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/no/common.json b/apps/web/public/static/locales/no/common.json index 222a3efdb9df19..fe24de9c7792e7 100644 --- a/apps/web/public/static/locales/no/common.json +++ b/apps/web/public/static/locales/no/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Se alle administrerte brukere opprettet av OAuth-klienten din", "select_oAuth_client": "Velg OAuth-klient", "on_every_instance": "På hver instans", + "next": "Neste", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index f6cf09fcc61b40..8f486d3b6f7e22 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Zobacz wszystkich zarządzanych użytkowników utworzonych przez twojego klienta OAuth", "select_oAuth_client": "Wybierz klienta OAuth", "on_every_instance": "W każdej instancji", + "next": "Dalej", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index e19e66efd4fc9a..6ddfba07009c00 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Veja todos os usuários gerenciados criados pelo seu cliente OAuth", "select_oAuth_client": "Selecionar cliente OAuth", "on_every_instance": "Em cada instância", + "next": "Próximo", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index 7a10337ffe57c0..a076b6cf54a744 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Veja todos os usuários gerenciados criados pelo seu cliente OAuth", "select_oAuth_client": "Selecionar cliente OAuth", "on_every_instance": "Em cada instância", + "next": "Próximo", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index 8f2d694ba2c85a..018ebd43d818c9 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Vezi toți utilizatorii administrați creați de clientul tău OAuth", "select_oAuth_client": "Selectează clientul OAuth", "on_every_instance": "La fiecare instanță", + "next": "Următorul", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 5bcfb085a4c123..673ff8caaf5724 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Просмотр всех управляемых пользователей, созданных вашим OAuth-клиентом", "select_oAuth_client": "Выберите OAuth-клиент", "on_every_instance": "На каждом экземпляре", + "next": "Далее", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/sk-SK/common.json b/apps/web/public/static/locales/sk-SK/common.json index 26ba6705a17c6e..f2a1bd82983a80 100644 --- a/apps/web/public/static/locales/sk-SK/common.json +++ b/apps/web/public/static/locales/sk-SK/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Zobraziť všetkých spravovaných používateľov vytvorených vaším OAuth klientom", "select_oAuth_client": "Vyberte OAuth klienta", "on_every_instance": "Pri každej inštancii", + "next": "Ďalej", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Pridajte svoje nové reťazce nad túto líniu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 7a637574be8bce..9d3520a1962c01 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Pogledajte sve upravljane korisnike koje je kreirao vaš OAuth klijent", "select_oAuth_client": "Izaberite OAuth klijenta", "on_every_instance": "Na svakoj instanci", + "next": "Dalje", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 0890fb8d1cd3dd..63592606883f66 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Se alla hanterade användare som skapats av din OAuth-klient", "select_oAuth_client": "Välj OAuth-klient", "on_every_instance": "På varje instans", + "next": "Nästa", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 1b891b14e92103..e15c3aa26af498 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "OAuth istemciniz tarafından oluşturulan tüm yönetilen kullanıcıları görün", "select_oAuth_client": "OAuth istemcisi seçin", "on_every_instance": "Her örnekte", + "next": "İleri", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index 5ffa2d93bf0205..00a3272bc1d0a8 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Переглянути всіх керованих користувачів, створених вашим OAuth-клієнтом", "select_oAuth_client": "Оберіть OAuth-клієнта", "on_every_instance": "На кожному екземплярі", + "next": "Далі", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index de90c165cfa0b8..084cd5442ea807 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "Xem tất cả người dùng được quản lý được tạo bởi OAuth client của bạn", "select_oAuth_client": "Chọn OAuth Client", "on_every_instance": "Trên mỗi phiên bản", + "next": "Tiếp theo", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index 276ca769d812eb..c8c333e163f25e 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "查看由您的 OAuth 客户端创建的所有托管用户", "select_oAuth_client": "选择 OAuth 客户端", "on_every_instance": "在每个实例上", + "next": "下一步", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 1e2a5d4df59f49..ae0516a0db1a50 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -2927,5 +2927,6 @@ "managed_users_description": "查看由您的 OAuth 用戶端建立的所有受管理使用者", "select_oAuth_client": "選擇 OAuth 用戶端", "on_every_instance": "在每個執行個體上", + "next": "下一步", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/apps/web/scripts/vercel-app-router-deploy.sh b/apps/web/scripts/vercel-app-router-deploy.sh index ec1256da7fac8a..6600bf078058d2 100755 --- a/apps/web/scripts/vercel-app-router-deploy.sh +++ b/apps/web/scripts/vercel-app-router-deploy.sh @@ -14,9 +14,7 @@ checkRoute "$APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED" app/future/apps/catego checkRoute "$APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED" app/future/auth/forgot-password checkRoute "$APP_ROUTER_AUTH_LOGIN_ENABLED" app/future/auth/login checkRoute "$APP_ROUTER_AUTH_LOGOUT_ENABLED" app/future/auth/logout -checkRoute "$APP_ROUTER_AUTH_NEW_ENABLED" app/future/auth/new checkRoute "$APP_ROUTER_AUTH_SAML_ENABLED" app/future/auth/saml-idp -checkRoute "$APP_ROUTER_AUTH_ERROR_ENABLED" app/future/auth/error checkRoute "$APP_ROUTER_AUTH_PLATFORM_ENABLED" app/future/auth/platform checkRoute "$APP_ROUTER_AUTH_OAUTH2_ENABLED" app/future/auth/oauth2 checkRoute "$APP_ROUTER_TEAM_ENABLED" app/future/team diff --git a/apps/web/server/lib/auth/error/getStaticProps.ts b/apps/web/server/lib/auth/error/getStaticProps.ts deleted file mode 100644 index cc917a12da0ef6..00000000000000 --- a/apps/web/server/lib/auth/error/getStaticProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"; - -import { getTranslations } from "@server/lib/getTranslations"; - -export type PageProps = InferGetStaticPropsType; -export const getStaticProps = async (context: GetStaticPropsContext) => { - const i18n = await getTranslations(context); - - return { - props: { - i18n, - }, - }; -}; diff --git a/i18n.lock b/i18n.lock index 40a5da47205607..a4e6444008944c 100644 --- a/i18n.lock +++ b/i18n.lock @@ -2929,4 +2929,5 @@ checksums: managed_users_description: 45fea5321847b0c8ee72ce33684c1283 select_oAuth_client: 0ebdcf296c0f68eca626cdb153204607 on_every_instance: db0e9e92055a38572fffa40f44440035 + next: 89ddbcf710eba274963494f312bdc8a9 ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS: 18323f2f3fabb169a7cb50ff62433850 diff --git a/package.json b/package.json index b8ddd812f61141..c299b75dbf0e7a 100644 --- a/package.json +++ b/package.json @@ -128,8 +128,7 @@ "@types/react": "18.0.26", "@types/react-dom": "^18.0.9", "next-i18next@^13.2.2": "patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch", - "libphonenumber-js@^1.10.51": "patch:libphonenumber-js@npm%3A1.10.51#./.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch", - "libphonenumber-js@^1.10.12": "patch:libphonenumber-js@npm%3A1.10.51#./.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch", + "libphonenumber-js": "patch:libphonenumber-js@1.11.18#./.yarn/patches/libphonenumber-js+1.11.18.patch", "dayjs@1.11.2": "patch:dayjs@npm%3A1.11.4#./.yarn/patches/dayjs-npm-1.11.4-97921cd375.patch", "dayjs@^1": "patch:dayjs@npm%3A1.11.4#./.yarn/patches/dayjs-npm-1.11.4-97921cd375.patch", "dayjs@^1.8.29": "patch:dayjs@npm%3A1.11.4#./.yarn/patches/dayjs-npm-1.11.4-97921cd375.patch", diff --git a/packages/app-store-cli/.yarn/ci-cache/install-state.gz b/packages/app-store-cli/.yarn/ci-cache/install-state.gz index ccbf8de40c29c5..8991db3524b355 100644 Binary files a/packages/app-store-cli/.yarn/ci-cache/install-state.gz and b/packages/app-store-cli/.yarn/ci-cache/install-state.gz differ diff --git a/packages/app-store/_pages/setup/_getServerSideProps.tsx b/packages/app-store/_pages/setup/_getServerSideProps.tsx index 39b20bf3983656..36bc532288b6b4 100644 --- a/packages/app-store/_pages/setup/_getServerSideProps.tsx +++ b/packages/app-store/_pages/setup/_getServerSideProps.tsx @@ -5,6 +5,7 @@ export const AppSetupPageMap = { make: import("../../make/pages/setup/_getServerSideProps"), zapier: import("../../zapier/pages/setup/_getServerSideProps"), stripe: import("../../stripepayment/pages/setup/_getServerSideProps"), + hitpay: import("../../hitpay/pages/setup/_getServerSideProps"), }; export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx index b5fd365a930c06..d7d160b3df16fc 100644 --- a/packages/app-store/_pages/setup/index.tsx +++ b/packages/app-store/_pages/setup/index.tsx @@ -15,6 +15,7 @@ export const AppSetupMap = { sendgrid: dynamic(() => import("../../sendgrid/pages/setup")), stripe: dynamic(() => import("../../stripepayment/pages/setup")), paypal: dynamic(() => import("../../paypal/pages/setup")), + hitpay: dynamic(() => import("../../hitpay/pages/setup")), }; export const AppSetupPage = (props: { slug: string }) => { diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index 69926275a260d9..01f24bca32499e 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -27,6 +27,7 @@ export const EventTypeAddonMap = { ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")), giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")), gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")), + hitpay: dynamic(() => import("./hitpay/components/EventTypeAppCardInterface")), hubspot: dynamic(() => import("./hubspot/components/EventTypeAppCardInterface")), insihts: dynamic(() => import("./insihts/components/EventTypeAppCardInterface")), matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")), @@ -57,6 +58,7 @@ export const EventTypeSettingsMap = { ga4: dynamic(() => import("./ga4/components/EventTypeAppSettingsInterface")), giphy: dynamic(() => import("./giphy/components/EventTypeAppSettingsInterface")), gtm: dynamic(() => import("./gtm/components/EventTypeAppSettingsInterface")), + hitpay: dynamic(() => import("./hitpay/components/EventTypeAppSettingsInterface")), metapixel: dynamic(() => import("./metapixel/components/EventTypeAppSettingsInterface")), paypal: dynamic(() => import("./paypal/components/EventTypeAppSettingsInterface")), plausible: dynamic(() => import("./plausible/components/EventTypeAppSettingsInterface")), diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 316bb0b4340198..becb7697c056aa 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -14,6 +14,7 @@ import { appKeysSchema as giphy_zod_ts } from "./giphy/zod"; import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appKeysSchema as googlevideo_zod_ts } from "./googlevideo/zod"; import { appKeysSchema as gtm_zod_ts } from "./gtm/zod"; +import { appKeysSchema as hitpay_zod_ts } from "./hitpay/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appKeysSchema as insihts_zod_ts } from "./insihts/zod"; import { appKeysSchema as intercom_zod_ts } from "./intercom/zod"; @@ -63,6 +64,7 @@ export const appKeysSchemas = { googlecalendar: googlecalendar_zod_ts, googlevideo: googlevideo_zod_ts, gtm: gtm_zod_ts, + hitpay: hitpay_zod_ts, hubspot: hubspot_zod_ts, insihts: insihts_zod_ts, intercom: intercom_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 29c3b8dcc8297d..9db12e052f93ea 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -36,6 +36,7 @@ import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metad import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; import granola_config_json from "./granola/config.json"; import gtm_config_json from "./gtm/config.json"; +import hitpay_config_json from "./hitpay/config.json"; import horizon_workrooms_config_json from "./horizon-workrooms/config.json"; import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; @@ -138,6 +139,7 @@ export const appStoreMetadata = { googlevideo: googlevideo__metadata_ts, granola: granola_config_json, gtm: gtm_config_json, + hitpay: hitpay_config_json, "horizon-workrooms": horizon_workrooms_config_json, hubspot: hubspot__metadata_ts, huddle01video: huddle01video__metadata_ts, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 1a5cd93dcffe94..1acffdaeec7b35 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -14,6 +14,7 @@ import { appDataSchema as giphy_zod_ts } from "./giphy/zod"; import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appDataSchema as googlevideo_zod_ts } from "./googlevideo/zod"; import { appDataSchema as gtm_zod_ts } from "./gtm/zod"; +import { appDataSchema as hitpay_zod_ts } from "./hitpay/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appDataSchema as insihts_zod_ts } from "./insihts/zod"; import { appDataSchema as intercom_zod_ts } from "./intercom/zod"; @@ -63,6 +64,7 @@ export const appDataSchemas = { googlecalendar: googlecalendar_zod_ts, googlevideo: googlevideo_zod_ts, gtm: gtm_zod_ts, + hitpay: hitpay_zod_ts, hubspot: hubspot_zod_ts, insihts: insihts_zod_ts, intercom: intercom_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 5c3c922ea7e333..4d81f334ff8a7f 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -36,6 +36,7 @@ export const apiHandlers = { googlevideo: import("./googlevideo/api"), granola: import("./granola/api"), gtm: import("./gtm/api"), + hitpay: import("./hitpay/api"), "horizon-workrooms": import("./horizon-workrooms/api"), hubspot: import("./hubspot/api"), huddle01video: import("./huddle01video/api"), diff --git a/packages/app-store/campsite/static/1.png b/packages/app-store/campsite/static/1.png index c90fd86c224f29..6c718110fc13ca 100644 Binary files a/packages/app-store/campsite/static/1.png and b/packages/app-store/campsite/static/1.png differ diff --git a/packages/app-store/campsite/static/2.png b/packages/app-store/campsite/static/2.png index a4b9149e56d82a..f0c03397fdf85e 100644 Binary files a/packages/app-store/campsite/static/2.png and b/packages/app-store/campsite/static/2.png differ diff --git a/packages/app-store/campsite/static/3.png b/packages/app-store/campsite/static/3.png index 3c73c072ffd524..340dad0bbd3f50 100644 Binary files a/packages/app-store/campsite/static/3.png and b/packages/app-store/campsite/static/3.png differ diff --git a/packages/app-store/campsite/static/icon.png b/packages/app-store/campsite/static/icon.png index 721d8e9cb15750..68330b8ecfdab9 100644 Binary files a/packages/app-store/campsite/static/icon.png and b/packages/app-store/campsite/static/icon.png differ diff --git a/packages/app-store/hitpay/DESCRIPTION.md b/packages/app-store/hitpay/DESCRIPTION.md new file mode 100644 index 00000000000000..7a8120560e98f0 --- /dev/null +++ b/packages/app-store/hitpay/DESCRIPTION.md @@ -0,0 +1,9 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg + - 4.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/hitpay/LICENSE b/packages/app-store/hitpay/LICENSE new file mode 100644 index 00000000000000..86aae54b2907fd --- /dev/null +++ b/packages/app-store/hitpay/LICENSE @@ -0,0 +1,42 @@ +The Cal.com Commercial License (EE) license (the “EE License”) +Copyright (c) 2020-present Cal.com, Inc + +With regard to the Cal.com Software: + +This software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have agreed to, +and are in compliance with, the Cal.com Subscription Terms available +at https://cal.com/terms (the “EE Terms”), or other agreements governing +the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"), +and otherwise have a valid Cal.com Commercial License subscription ("EE Subscription") +for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence, +you are free to modify this Software and publish patches to the Software. You agree +that Cal.com and/or its licensors (as applicable) retain all right, title and interest in +and to all such modifications and/or patches, and all such modifications and/or +patches may only be used, copied, modified, displayed, distributed, or otherwise +exploited with a valid EE Subscription for the correct number of hosts. +Notwithstanding the foregoing, you may copy and modify the Software for development +and testing purposes, without requiring a subscription. You agree that Cal.com and/or +its licensors (as applicable) retain all right, title and interest in and to all such +modifications. You are not granted any other rights beyond what is expressly stated herein. +Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, +and/or sell the Software. + +This EE License applies only to the part of this Software that is not distributed under +the AGPLv3 license. Any part of this Software distributed under the MIT license or which +is served client-side as an image, font, cascading stylesheet (CSS), file which produces +or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or +in part, is copyrighted under the AGPLv3 license. The full text of this EE License shall +be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Cal.com Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/packages/app-store/hitpay/README.md b/packages/app-store/hitpay/README.md new file mode 100644 index 00000000000000..2f1c87cffe35ec --- /dev/null +++ b/packages/app-store/hitpay/README.md @@ -0,0 +1,6 @@ +## Obraining HitPay API key and Salt + +1. Create a [HitPay Sandbox](https://dashboard.sandbox.hit-pay.com/) or [HitPay](https://dashboard.hit-pay.com/), if you don't have one. +2. Sign into your [HitPay Sandbox](https://dashboard.sandbox.hit-pay.com/login/) or [HitPay](https://dashboard.hit-pay.com/login/) +3. On the left side bar, go to API Keys page by clicking on API Keys. +4. Under API Keys on the right side screen, copy API Keys and Salt and paste them to API Keys and Salt on the HitPay app setup page respectively, while installing or updating the app. \ No newline at end of file diff --git a/packages/app-store/hitpay/api/add.ts b/packages/app-store/hitpay/api/add.ts new file mode 100644 index 00000000000000..1d4593d604b16c --- /dev/null +++ b/packages/app-store/hitpay/api/add.ts @@ -0,0 +1,42 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import config from "../config.json"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session?.user?.id) { + return res.status(401).json({ message: "You must be logged in to do this" }); + } + const appType = config.type; + try { + const alreadyInstalled = await prisma.credential.findFirst({ + where: { + type: appType, + userId: req.session.user.id, + }, + }); + if (alreadyInstalled) { + throw new Error("Already installed"); + } + const installation = await prisma.credential.create({ + data: { + type: appType, + key: {}, + userId: req.session.user.id, + appId: "hitpay", + }, + }); + + if (!installation) { + throw new Error("Unable to create user credential for Alby"); + } + } catch (error: unknown) { + if (error instanceof Error) { + return res.status(500).json({ message: error.message }); + } + return res.status(500); + } + + return res.status(200).json({ url: "/apps/hitpay/setup" }); +} diff --git a/packages/app-store/hitpay/api/callback.ts b/packages/app-store/hitpay/api/callback.ts new file mode 100644 index 00000000000000..fcba2ddf9881b0 --- /dev/null +++ b/packages/app-store/hitpay/api/callback.ts @@ -0,0 +1,74 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import qs from "qs"; + +import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { reference, status } = req.query; + if (!reference) { + throw new HttpCode({ statusCode: 204, message: "Reference not found" }); + } + + const payment = await prisma.payment.findFirst({ + where: { + externalId: reference as string, + }, + select: { + id: true, + amount: true, + bookingId: true, + booking: { + select: { + uid: true, + user: { + select: { + email: true, + username: true, + credentials: { + where: { + type: "hitpay_payment", + }, + }, + }, + }, + responses: true, + eventType: { + select: { + slug: true, + }, + }, + }, + }, + }, + }); + + if (!payment) { + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + } + const key = payment.booking?.user?.credentials?.[0].key; + if (!key) { + throw new HttpCode({ statusCode: 204, message: "Credential not found" }); + } + + if (!payment.booking || !payment.booking.user || !payment.booking.eventType || !payment.booking.responses) { + throw new HttpCode({ statusCode: 204, message: "Booking not correct" }); + } + + if (status !== "completed") { + const url = `/${payment.booking.user.username}/${payment.booking.eventType.slug}`; + return res.redirect(url); + } + + const queryParams = { + "flag.coep": false, + isSuccessBookingPage: true, + email: (payment.booking.responses as { email: string }).email, + eventTypeSlug: payment.booking.eventType.slug, + }; + + const query = qs.stringify(queryParams); + const url = `/booking/${payment.booking.uid}?${query}`; + + return res.redirect(url); +} diff --git a/packages/app-store/hitpay/api/index.ts b/packages/app-store/hitpay/api/index.ts new file mode 100644 index 00000000000000..567bbf79794bf8 --- /dev/null +++ b/packages/app-store/hitpay/api/index.ts @@ -0,0 +1,3 @@ +export { default as add } from "./add"; +export { default as callback } from "./callback"; +export { default as webhook } from "./webhook"; diff --git a/packages/app-store/hitpay/api/webhook.ts b/packages/app-store/hitpay/api/webhook.ts new file mode 100644 index 00000000000000..ec2ab8d9c57e1a --- /dev/null +++ b/packages/app-store/hitpay/api/webhook.ts @@ -0,0 +1,114 @@ +import { createHmac } from "crypto"; +import type { NextApiRequest, NextApiResponse } from "next"; +import type z from "zod"; + +import { IS_PRODUCTION } from "@calcom/lib/constants"; +import { getErrorFromUnknown } from "@calcom/lib/errors"; +import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import { handlePaymentSuccess } from "@calcom/lib/payment/handlePaymentSuccess"; +import prisma from "@calcom/prisma"; + +import type { hitpayCredentialKeysSchema } from "../lib/hitpayCredentialKeysSchema"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +interface WebhookReturn { + payment_id: string; + payment_request_id: string; + phone: string; + amount: string; + currency: string; + status: string; + reference_number: string; + hmac: string; +} + +type ExcludedWebhookReturn = Omit; + +function generateSignatureArray(secret: string, vals: T) { + const source: string[] = []; + Object.keys(vals as { [K: string]: string }) + .sort() + .forEach((key) => { + source.push(`${key}${(vals as { [K: string]: string })[key]}`); + }); + const payload = source.join(""); + const hmac = createHmac("sha256", secret); + const signed = hmac.update(payload, "utf-8").digest("hex"); + return signed; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + debugger; + if (req.method !== "POST") { + throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); + } + const obj: WebhookReturn = req.body as WebhookReturn; + const excluded = { ...obj } as Partial; + delete excluded.hmac; + + const payment = await prisma.payment.findFirst({ + where: { + externalId: obj.payment_request_id, + }, + select: { + id: true, + amount: true, + bookingId: true, + booking: { + select: { + user: { + select: { + credentials: { + where: { + type: "hitpay_payment", + }, + }, + }, + }, + }, + }, + }, + }); + + if (!payment) { + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + } + const key = payment.booking?.user?.credentials?.[0].key; + if (!key) { + throw new HttpCode({ statusCode: 204, message: "Credentials not found" }); + } + + const { isSandbox, prod, sandbox } = key as z.infer; + const keyObj = isSandbox ? sandbox : prod; + if (!keyObj) { + throw new HttpCode({ + statusCode: 204, + message: `${isSandbox ? "Sandbox" : "Production"} Credentials not found`, + }); + } + + const { saltKey } = keyObj; + const signed = generateSignatureArray(saltKey, excluded as ExcludedWebhookReturn); + if (signed !== obj.hmac) { + throw new HttpCode({ statusCode: 400, message: "Bad Request" }); + } + + if (excluded.status !== "completed") { + throw new HttpCode({ statusCode: 204, message: `Payment is ${excluded.status}` }); + } + return await handlePaymentSuccess(payment.id, payment.bookingId); + } catch (_err) { + const err = getErrorFromUnknown(_err); + console.error(`Webhook Error: ${err.message}`); + return res.status(200).send({ + message: err.message, + stack: IS_PRODUCTION ? undefined : err.stack, + }); + } +} diff --git a/packages/app-store/hitpay/components/.gitkeep b/packages/app-store/hitpay/components/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/app-store/hitpay/components/EventTypeAppCardInterface.tsx b/packages/app-store/hitpay/components/EventTypeAppCardInterface.tsx new file mode 100644 index 00000000000000..0340a969cdf16c --- /dev/null +++ b/packages/app-store/hitpay/components/EventTypeAppCardInterface.tsx @@ -0,0 +1,50 @@ +import { usePathname } from "next/navigation"; +import { useState } from "react"; + +import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; +import AppCard from "@calcom/app-store/_components/AppCard"; +import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps"; +import useIsAppEnabled from "../../_utils/useIsAppEnabled"; +import type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; + +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + app, + eventType, + eventTypeFormMetadata, +}) { + const { t } = useLocale(); + const pathname = usePathname(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); + const { enabled, updateEnabled } = useIsAppEnabled(app); + const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + const [requirePayment] = useState(getAppData("enabled")); + const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; + + return ( + { + updateEnabled(e); + }} + teamId={eventType.team?.id || undefined} + disableSwitch={shouldDisableSwitch} + switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> + + + ); +}; + +export default EventTypeAppCard; diff --git a/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 00000000000000..1fb1fc00912e20 --- /dev/null +++ b/packages/app-store/hitpay/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,123 @@ +import { useState, useEffect } from "react"; + +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Alert, Select, TextField } from "@calcom/ui"; + +import { + convertToSmallestCurrencyUnit, + convertFromSmallestToPresentableCurrencyUnit, +} from "../lib/currencyConversions"; +import { paymentOptions, currencyOptions } from "./constants"; + +const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ + getAppData, + setAppData, + disabled, + eventType, +}) => { + const price = getAppData("price"); + const currency = getAppData("currency") || currencyOptions[0].value; + const [selectedCurrency, setSelectedCurrency] = useState( + currencyOptions.find((c) => c.value === currency) || { + label: currencyOptions[0].label, + value: currencyOptions[0].value, + } + ); + const paymentOption = getAppData("paymentOption"); + const requirePayment = getAppData("enabled"); + + const { t } = useLocale(); + const recurringEventDefined = eventType.recurringEvent?.count !== undefined; + const seatsEnabled = !!eventType.seatsPerTimeSlot; + const getCurrencySymbol = (locale: string, currency: string) => + (0) + .toLocaleString(locale, { + style: "currency", + currency, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }) + .replace(/\d/g, "") + .trim(); + + useEffect(() => { + if (requirePayment) { + if (!getAppData("currency")) { + setAppData("currency", currencyOptions[0].value); + } + if (!getAppData("paymentOption")) { + setAppData("paymentOption", paymentOptions[0].value); + } + } + }, [requirePayment, getAppData, setAppData]); + + const disableDecimalPlace = (value: number) => { + const nValue = Math.floor(value); + const sValue = nValue.toString(); + const ret = parseInt(sValue); + return ret; + }; + + return ( + <> + {recurringEventDefined && ( + + )} + {!recurringEventDefined && requirePayment && ( + <> +
+ {selectedCurrency.value ? getCurrencySymbol("en", selectedCurrency.value) : ""} + } + addOnSuffix={currency.toUpperCase()} + addOnClassname="h-[38px]" + step="1" + min="1" + type="number" + required + placeholder="Price" + disabled={disabled} + onChange={(e) => { + setAppData("price", convertToSmallestCurrencyUnit(Number(e.target.value), currency)); + }} + value={ + price > 0 + ? disableDecimalPlace(convertFromSmallestToPresentableCurrencyUnit(price, currency)) + : undefined + } + /> +
+
+ + + + + + +
+
+ ); + } +); + +export default KeyField; diff --git a/packages/app-store/hitpay/components/constants.ts b/packages/app-store/hitpay/components/constants.ts new file mode 100644 index 00000000000000..cd5bad2bd92ae0 --- /dev/null +++ b/packages/app-store/hitpay/components/constants.ts @@ -0,0 +1,71 @@ +export const paymentOptions = [ + { + label: "on_booking_option", + value: "ON_BOOKING", + }, +]; + +export const supportedPaymentMethods = [ + "paynow", + "grabpay", + "shopeepay", + "zip", + "fpx", + "visa", + "master", + "american_express", + "alipay", + "atome", + "unionpay", + "gcash", + "qrph", + "billease", + "seveneleven", + "cebuana", + "palawa", + "ovo", + "gopay", + "linkaja", + "dana", + "kredivo", + "akulakupaylater", + "akulaku", + "indomaret", + "alfamart", + "bri", + "cimb", + "sbpl", + "payid", + "qris", + "bdo", + "bpi", + "duitnow", + "wechatpay", + "qr_promptpay", + "line_pay", + "truemoney_pay", +]; + +export const currencyOptions = [ + { label: "United States dollar (USD)", value: "usd" }, + { label: "Singapore dollar (SGD)", value: "sgd" }, + { label: "Malaysian ringgit (MYR)", value: "myr" }, + { label: "Indonesian rupiah (IDR)", value: "idr" }, + { label: "Japanese yen (JPY)", value: "jpy" }, + { label: "Hong Kong dollar (HKD)", value: "hkd" }, + { label: "Thai baht (THB)", value: "thb" }, + { label: "Australian dollar (AUD)", value: "aud" }, + { label: "New Zealand dollar (NZD)", value: "nzd" }, + { label: "British pound sterling (GBP)", value: "gbp" }, + { label: "Philippine peso (PHP)", value: "php" }, + { label: "Indian rupee (INR)", value: "inr" }, + { label: "Chinese yuan (CNY)", value: "cny" }, + { label: "Euro (EUR)", value: "eur" }, + { label: "Swiss franc (CHF)", value: "chf" }, + { label: "Danish krone (DKK)", value: "dkk" }, + { label: "Swedish krona (SEK)", value: "sek" }, + { label: "Norwegian krone (NOK)", value: "nok" }, + { label: "Vietnamese đồng (VND)", value: "vnd" }, + { label: "Canadian dollar (CAD)", value: "cad" }, + { label: "South Korean won (KRW)", value: "krw" }, +]; diff --git a/packages/app-store/hitpay/config.json b/packages/app-store/hitpay/config.json new file mode 100644 index 00000000000000..ad98701aa12443 --- /dev/null +++ b/packages/app-store/hitpay/config.json @@ -0,0 +1,19 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "HitPay", + "slug": "hitpay", + "type": "hitpay_payment", + "logo": "icon.svg", + "url": "https://dashboard.hit-pay.com", + "variant": "payment", + "categories": ["payment"], + "publisher": "HitPay", + "email": "support@hit-pay.com", + "description": "HitPay helps over 15,000 businesses across Southeast Asia and around the globe process payments efficiently and securely. We unify online, point of sale, and B2B payments into a single, integrated payment processing system.", + "extendsFeature": "EventType", + "isTemplate": false, + "__createdUsingCli": true, + "__template": "event-type-app-card", + "dirName": "hitpay", + "isOAuth": true +} diff --git a/packages/app-store/hitpay/index.ts b/packages/app-store/hitpay/index.ts new file mode 100644 index 00000000000000..e2e9d7b029c031 --- /dev/null +++ b/packages/app-store/hitpay/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/hitpay/lib/PaymentService.ts b/packages/app-store/hitpay/lib/PaymentService.ts new file mode 100644 index 00000000000000..f4092cff9cb318 --- /dev/null +++ b/packages/app-store/hitpay/lib/PaymentService.ts @@ -0,0 +1,170 @@ +import type { Booking, Payment, PaymentOption, Prisma } from "@prisma/client"; +import axios from "axios"; +import qs from "qs"; +import { v4 as uuidv4 } from "uuid"; +import type z from "zod"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import prisma from "@calcom/prisma"; +import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; + +import appConfig from "../config.json"; +import { API_HITPAY, SANDBOX_API_HITPAY } from "./constants"; +import { hitpayCredentialKeysSchema } from "./hitpayCredentialKeysSchema"; + +const log = logger.getSubLogger({ prefix: ["payment-service:hitpay"] }); + +export class PaymentService implements IAbstractPaymentService { + private credentials: z.infer | null; + + constructor(credentials: { key: Prisma.JsonValue }) { + const keyParsing = hitpayCredentialKeysSchema.safeParse(credentials.key); + if (keyParsing.success) { + this.credentials = keyParsing.data; + } else { + this.credentials = null; + } + } + + async create( + payment: Pick, + bookingId: Booking["id"], + userId: Booking["userId"], + username: string | null, + bookerName: string, + paymentOption: PaymentOption, + bookerEmail: string + ) { + try { + const booking = await prisma.booking.findFirst({ + select: { + uid: true, + title: true, + }, + where: { + id: bookingId, + }, + }); + + if (!booking || !this.credentials) { + throw new Error("Booking or API key not found"); + } + + const { isSandbox } = this.credentials; + const keyObj = isSandbox ? this.credentials.sandbox : this.credentials.prod; + if (!keyObj || !keyObj?.apiKey) { + throw new Error("API key not found"); + } + + const hitpayAPIurl = isSandbox ? SANDBOX_API_HITPAY : API_HITPAY; + + const requestUrl = `${hitpayAPIurl}/v1/payment-requests`; + const redirectUri = `${WEBAPP_URL}/api/integrations/${appConfig.slug}/callback`; + const webhookUri = `${WEBAPP_URL}/api/integrations/${appConfig.slug}/webhook`; + + const formData = { + amount: payment.amount / 100, + currency: payment.currency, + email: bookerEmail, + name: bookerName, + reference_number: bookingId.toString(), + redirect_url: redirectUri, + webhook: webhookUri, + channel: "api_cal", + purpose: booking.title, + }; + + const response = await axios.post(requestUrl, qs.stringify(formData), { + headers: { + "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", + "X-BUSINESS-API-KEY": keyObj.apiKey, + "X-Requested-With": "XMLHttpRequest", + }, + }); + + const data = response.data; + const uid = uuidv4(); + + const paymentData = await prisma.payment.create({ + data: { + uid, + app: { + connect: { + slug: "hitpay", + }, + }, + booking: { + connect: { + id: bookingId, + }, + }, + amount: parseFloat(data.amount.replace(/,/g, "")) * 100, + externalId: data.id, + currency: data.currency, + data: Object.assign({}, data, { isPaid: false }) as unknown as Prisma.InputJsonValue, + fee: 0, + refunded: false, + success: false, + }, + }); + + if (!paymentData) { + throw new Error("Failed to store Payment data"); + } + return paymentData; + } catch (error) { + log.error("Payment could not be created", bookingId, safeStringify(error)); + throw new Error(ErrorCode.PaymentCreationFailure); + } + } + async update(): Promise { + throw new Error("Method not implemented."); + } + async refund(): Promise { + throw new Error("Method not implemented."); + } + + async collectCard( + _payment: Pick, + _bookingId: number, + _bookerEmail: string, + _paymentOption: PaymentOption + ): Promise { + throw new Error("Method not implemented"); + } + chargeCard( + _payment: Pick, + _bookingId: number + ): Promise { + throw new Error("Method not implemented."); + } + getPaymentPaidStatus(): Promise { + throw new Error("Method not implemented."); + } + getPaymentDetails(): Promise { + throw new Error("Method not implemented."); + } + afterPayment( + _event: CalendarEvent, + _booking: { + user: { email: string | null; name: string | null; timeZone: string } | null; + id: number; + startTime: { toISOString: () => string }; + uid: string; + }, + _paymentData: Payment + ): Promise { + return Promise.resolve(); + } + deletePayment(_paymentId: number): Promise { + return Promise.resolve(false); + } + + isSetupAlready(): boolean { + return !!this.credentials; + } +} diff --git a/packages/app-store/hitpay/lib/constants.ts b/packages/app-store/hitpay/lib/constants.ts new file mode 100644 index 00000000000000..5889ac4efa265d --- /dev/null +++ b/packages/app-store/hitpay/lib/constants.ts @@ -0,0 +1,3 @@ +export const API_HITPAY = process.env.NEXT_PUBLIC_API_HITPAY_PRODUCTION || "https://api.hit-pay.com"; +export const SANDBOX_API_HITPAY = + process.env.NEXT_PUBLIC_API_HITPAY_SANDBOX || "https://api.sandbox.hit-pay.com"; diff --git a/packages/app-store/hitpay/lib/currencyConversions.ts b/packages/app-store/hitpay/lib/currencyConversions.ts new file mode 100644 index 00000000000000..212bc3c519a68f --- /dev/null +++ b/packages/app-store/hitpay/lib/currencyConversions.ts @@ -0,0 +1,32 @@ +const zeroDecimalCurrencies = [ + "BIF", + "CLP", + "DJF", + "GNF", + "JPY", + "KMF", + "KRW", + "MGA", + "PYG", + "RWF", + "UGX", + "VND", + "VUV", + "XAF", + "XOF", + "XPF", +]; + +export const convertToSmallestCurrencyUnit = (amount: number, currency: string) => { + if (zeroDecimalCurrencies.includes(currency.toUpperCase())) { + return amount; + } + return Math.round(amount * 100); +}; + +export const convertFromSmallestToPresentableCurrencyUnit = (amount: number, currency: string) => { + if (zeroDecimalCurrencies.includes(currency.toUpperCase())) { + return amount; + } + return amount / 100; +}; diff --git a/packages/app-store/hitpay/lib/hitpayCredentialKeysSchema.ts b/packages/app-store/hitpay/lib/hitpayCredentialKeysSchema.ts new file mode 100644 index 00000000000000..9e9cfe630f3e2f --- /dev/null +++ b/packages/app-store/hitpay/lib/hitpayCredentialKeysSchema.ts @@ -0,0 +1,17 @@ +import z from "zod"; + +export const hitpayCredentialKeysSchema = z.object({ + prod: z + .object({ + apiKey: z.string(), + saltKey: z.string(), + }) + .optional(), + sandbox: z + .object({ + apiKey: z.string(), + saltKey: z.string(), + }) + .optional(), + isSandbox: z.boolean(), +}); diff --git a/packages/app-store/hitpay/lib/index.ts b/packages/app-store/hitpay/lib/index.ts new file mode 100644 index 00000000000000..c758477166646e --- /dev/null +++ b/packages/app-store/hitpay/lib/index.ts @@ -0,0 +1,2 @@ +export * from "./constants"; +export * from "./PaymentService"; diff --git a/packages/app-store/hitpay/package.json b/packages/app-store/hitpay/package.json new file mode 100644 index 00000000000000..52b0a790f53915 --- /dev/null +++ b/packages/app-store/hitpay/package.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "license": "AGPLv3 (see LICENSE file)", + "name": "@calcom/hitpay", + "version": "0.0.0", + "main": "./index.ts", + "description": "HitPay helps over 15,000 businesses across Southeast Asia and around the globe process payments efficiently and securely. We unify online, point of sale, and B2B payments into a single, integrated payment processing system.", + "dependencies": { + "@calcom/lib": "*", + "@calcom/prisma": "*" + }, + "devDependencies": { + "@calcom/types": "*" + } +} diff --git a/packages/app-store/hitpay/pages/setup/_getServerSideProps.tsx b/packages/app-store/hitpay/pages/setup/_getServerSideProps.tsx new file mode 100644 index 00000000000000..f56c64e9aa59da --- /dev/null +++ b/packages/app-store/hitpay/pages/setup/_getServerSideProps.tsx @@ -0,0 +1,47 @@ +import type { GetServerSidePropsContext } from "next"; + +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import prisma from "@calcom/prisma"; + +import { hitpayCredentialKeysSchema } from "../../lib/hitpayCredentialKeysSchema"; +import type { IHitPaySetupProps } from "./index"; + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const notFound = { notFound: true } as const; + + if (typeof ctx.params?.slug !== "string") return notFound; + + const { req, res } = ctx; + const session = await getServerSession({ req, res }); + + if (!session?.user?.id) { + const redirect = { redirect: { permanent: false, destination: "/auth/login" } } as const; + + return redirect; + } + + const credentials = await prisma.credential.findFirst({ + where: { + type: "hitpay_payment", + userId: session?.user.id, + }, + }); + + let props: IHitPaySetupProps = { + isSandbox: false, + }; + + if (credentials?.key) { + const keyParsing = hitpayCredentialKeysSchema.safeParse(credentials.key); + if (keyParsing.success) { + props = { + ...props, + ...keyParsing.data, + }; + } + } + + return { + props, + }; +}; diff --git a/packages/app-store/hitpay/pages/setup/index.tsx b/packages/app-store/hitpay/pages/setup/index.tsx new file mode 100644 index 00000000000000..729007be82e1dd --- /dev/null +++ b/packages/app-store/hitpay/pages/setup/index.tsx @@ -0,0 +1,301 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Toaster } from "react-hot-toast"; +import { z } from "zod"; + +import AppNotInstalledMessage from "@calcom/app-store/_components/AppNotInstalledMessage"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc"; +import { Button, showToast, Icon, Switch } from "@calcom/ui"; +import { HeadSeo } from "@calcom/ui"; + +import KeyField from "../../components/KeyInput"; +import { hitpayCredentialKeysSchema } from "../../lib/hitpayCredentialKeysSchema"; + +export type IHitPaySetupProps = z.infer; + +export default function HitPaySetup(props: IHitPaySetupProps) { + const params = useCompatSearchParams(); + if (params?.get("callback") === "true") { + return ; + } + + return ; +} + +function HitPaySetupCallback() { + const [error, setError] = useState(null); + const searchParams = useCompatSearchParams(); + + useEffect(() => { + if (!searchParams) { + return; + } + + if (!window.opener) { + setError("Something went wrong. Opener not available. Please contact support@getalby.com"); + return; + } + + const code = searchParams?.get("code"); + const error = searchParams?.get("error"); + + if (!code) { + setError("declined"); + } + if (error) { + setError(error); + return; + } + + window.opener.postMessage({ + type: "hitpay:oauth:success", + payload: { code }, + }); + window.close(); + }, [searchParams]); + + return ( +
+ {error &&

Authorization failed: {error}

} + {!error &&

Connecting...

} +
+ ); +} + +function HitPaySetupPage(props: IHitPaySetupProps) { + const [loading, setLoading] = useState(false); + const [updatable, setUpdatable] = useState(false); + const [isSandbox, SetIsSandbox] = useState(props.isSandbox || false); + const [keyData, setKeyData] = useState< + | { + apiKey: string; + saltKey: string; + } + | undefined + >(); + const session = useSession(); + const router = useRouter(); + const { t } = useLocale(); + + const settingsSchema = z.object({ + apiKey: z + .string() + .trim() + .min(64) + .max(64, { + message: t("max_limit_allowed_hint", { limit: 64 }), + }), + saltKey: z + .string() + .trim() + .min(64) + .max(64, { + message: t("max_limit_allowed_hint", { limit: 64 }), + }), + }); + + const integrations = trpc.viewer.integrations.useQuery({ variant: "payment", appId: "hitpay" }); + const [HitPayPaymentAppCredentials] = integrations.data?.items || []; + const [credentialId] = HitPayPaymentAppCredentials?.userCredentialIds || [-1]; + const showContent = !!integrations.data && integrations.isSuccess && !!credentialId; + + const saveKeysMutation = trpc.viewer.appsRouter.updateAppCredentials.useMutation({ + onSuccess: () => { + showToast(t("keys_have_been_saved"), "success"); + router.push("/event-types"); + }, + onError: (error) => { + showToast(error.message, "error"); + }, + }); + + const deleteMutation = trpc.viewer.deleteCredential.useMutation({ + onSuccess: () => { + router.push("/apps/hitpay"); + }, + onError: () => { + showToast(t("error_removing_app"), "error"); + }, + }); + + const { + register, + handleSubmit, + formState: { errors }, + watch, + reset, + } = useForm>({ + reValidateMode: "onChange", + resolver: zodResolver(settingsSchema), + }); + + useEffect(() => { + const keyObj = isSandbox ? props.sandbox : props.prod; + reset({ + apiKey: keyObj?.apiKey || "", + saltKey: keyObj?.saltKey || "", + }); + setKeyData(keyObj); + }, [isSandbox]); + + useEffect(() => { + const subscription = watch((value) => { + const { apiKey, saltKey } = value; + if (apiKey && saltKey && (keyData?.apiKey !== apiKey || keyData?.saltKey !== saltKey)) { + setUpdatable(true); + } else { + setUpdatable(false); + } + }); + + return () => subscription.unsubscribe(); + }, [watch, keyData]); + + const onSubmit = handleSubmit(async (data) => { + if (loading) return; + setLoading(true); + try { + const keyParams = { + isSandbox, + prod: isSandbox ? props.prod : data, + sandbox: isSandbox ? data : props.sandbox, + }; + saveKeysMutation.mutate({ + credentialId, + key: hitpayCredentialKeysSchema.parse(keyParams), + }); + } catch (error: unknown) { + let message = ""; + if (error instanceof Error) { + message = error.message; + } + showToast(message, "error"); + } finally { + setLoading(false); + } + }); + + const onCancel = () => { + deleteMutation.mutate({ id: credentialId }); + }; + + const hitpayIcon = ( + <> + HitPay Icon + + ); + + if (session.status === "loading") return <>; + + if (integrations.isPending) { + return
; + } + + return ( + <> + +
+ {showContent ? ( +
+
+ Create or connect to an existing + HitPay account to receive payments for your paid bookings. +
+ +
+
+
+

Account Information

+
+ SetIsSandbox(value as boolean)} + checked={isSandbox} + label="Sandbox" + /> +
+
+
+
+ + + {errors.apiKey && ( +

+ {errors.apiKey?.message} +

+ )} +
+
+ + + {errors.saltKey && ( +

+ {errors.saltKey?.message} +

+ )} +
+
+
+ {!props.prod && !props.sandbox ? ( +
+ + +
+ ) : ( +
+ + + + +
+ )} +
+
+ ) : ( + + )} + +
+ + ); +} diff --git a/packages/app-store/hitpay/static/1.jpeg b/packages/app-store/hitpay/static/1.jpeg new file mode 100644 index 00000000000000..3660aabab2ef03 Binary files /dev/null and b/packages/app-store/hitpay/static/1.jpeg differ diff --git a/packages/app-store/hitpay/static/2.jpeg b/packages/app-store/hitpay/static/2.jpeg new file mode 100644 index 00000000000000..faaca1d8f97e0b Binary files /dev/null and b/packages/app-store/hitpay/static/2.jpeg differ diff --git a/packages/app-store/hitpay/static/3.jpeg b/packages/app-store/hitpay/static/3.jpeg new file mode 100644 index 00000000000000..85d26924a6440f Binary files /dev/null and b/packages/app-store/hitpay/static/3.jpeg differ diff --git a/packages/app-store/hitpay/static/4.jpeg b/packages/app-store/hitpay/static/4.jpeg new file mode 100644 index 00000000000000..73fae5c70c48e7 Binary files /dev/null and b/packages/app-store/hitpay/static/4.jpeg differ diff --git a/packages/app-store/hitpay/static/icon.svg b/packages/app-store/hitpay/static/icon.svg new file mode 100644 index 00000000000000..69737e8ce90e54 --- /dev/null +++ b/packages/app-store/hitpay/static/icon.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/packages/app-store/hitpay/zod.ts b/packages/app-store/hitpay/zod.ts new file mode 100644 index 00000000000000..f9c1dac4873162 --- /dev/null +++ b/packages/app-store/hitpay/zod.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + +export const PaypalPaymentOptions = [ + { + label: "on_booking_option", + value: "ON_BOOKING", + }, +]; + +type PaymentOption = (typeof PaypalPaymentOptions)[number]["value"]; +const VALUES: [PaymentOption, ...PaymentOption[]] = [ + PaypalPaymentOptions[0].value, + ...PaypalPaymentOptions.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({}); diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 6cdcd1e5c969a5..7f2fbe13d8fc00 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -43,6 +43,7 @@ const appStore = { basecamp3: () => import("./basecamp3"), telegramvideo: () => import("./telegram"), shimmervideo: () => import("./shimmervideo"), + hitpay: () => import("./hitpay"), }; const exportedAppStore: typeof appStore & { diff --git a/packages/app-store/make/DESCRIPTION.md b/packages/app-store/make/DESCRIPTION.md index 200508b623ebf1..6fed4654dba9da 100644 --- a/packages/app-store/make/DESCRIPTION.md +++ b/packages/app-store/make/DESCRIPTION.md @@ -1,10 +1,10 @@ --- items: - - 1.jpeg - - 2.jpeg - - 3.jpeg - - 4.jpeg - - 5.jpeg + - 1.png + - 2.png + - 3.png + - 4.png + - 5.png --- Workflow automation for everyone. Use the Cal.com app in Make to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting has ended. You can also get all your booking with the 'List Bookings' module.

**After Installation:** Have you lost your API key? You can always generate a new key on the **Make Setup Page** diff --git a/packages/app-store/make/static/1.jpeg b/packages/app-store/make/static/1.jpeg deleted file mode 100644 index 54f811f91887fb..00000000000000 Binary files a/packages/app-store/make/static/1.jpeg and /dev/null differ diff --git a/packages/app-store/make/static/1.png b/packages/app-store/make/static/1.png new file mode 100644 index 00000000000000..2fc712ec5d7ec1 Binary files /dev/null and b/packages/app-store/make/static/1.png differ diff --git a/packages/app-store/make/static/2.jpeg b/packages/app-store/make/static/2.jpeg deleted file mode 100644 index c6637a01c8de6b..00000000000000 Binary files a/packages/app-store/make/static/2.jpeg and /dev/null differ diff --git a/packages/app-store/make/static/2.png b/packages/app-store/make/static/2.png new file mode 100644 index 00000000000000..a92b0ce387dce1 Binary files /dev/null and b/packages/app-store/make/static/2.png differ diff --git a/packages/app-store/make/static/3.jpeg b/packages/app-store/make/static/3.jpeg deleted file mode 100644 index 15c25d425f0dc0..00000000000000 Binary files a/packages/app-store/make/static/3.jpeg and /dev/null differ diff --git a/packages/app-store/make/static/3.png b/packages/app-store/make/static/3.png new file mode 100644 index 00000000000000..0e63581f61e122 Binary files /dev/null and b/packages/app-store/make/static/3.png differ diff --git a/packages/app-store/make/static/4.jpeg b/packages/app-store/make/static/4.jpeg deleted file mode 100644 index 6ae8a3cabc5821..00000000000000 Binary files a/packages/app-store/make/static/4.jpeg and /dev/null differ diff --git a/packages/app-store/make/static/4.png b/packages/app-store/make/static/4.png new file mode 100644 index 00000000000000..ee31ba47aca858 Binary files /dev/null and b/packages/app-store/make/static/4.png differ diff --git a/packages/app-store/make/static/5.jpeg b/packages/app-store/make/static/5.jpeg deleted file mode 100644 index 29667aff9ad09c..00000000000000 Binary files a/packages/app-store/make/static/5.jpeg and /dev/null differ diff --git a/packages/app-store/make/static/5.png b/packages/app-store/make/static/5.png new file mode 100644 index 00000000000000..8ede695bd01739 Binary files /dev/null and b/packages/app-store/make/static/5.png differ diff --git a/packages/app-store/metapixel/static/1.png b/packages/app-store/metapixel/static/1.png index b649ae76483bba..92cb7203394f87 100644 Binary files a/packages/app-store/metapixel/static/1.png and b/packages/app-store/metapixel/static/1.png differ diff --git a/packages/app-store/metapixel/static/2.png b/packages/app-store/metapixel/static/2.png index db614249397032..a29a5a6113025b 100644 Binary files a/packages/app-store/metapixel/static/2.png and b/packages/app-store/metapixel/static/2.png differ diff --git a/packages/app-store/metapixel/static/3.png b/packages/app-store/metapixel/static/3.png index 75f58a8d02c056..545ef99ba73c48 100644 Binary files a/packages/app-store/metapixel/static/3.png and b/packages/app-store/metapixel/static/3.png differ diff --git a/packages/app-store/ping/static/1.png b/packages/app-store/ping/static/1.png index be598248522398..47248fccb34ac5 100644 Binary files a/packages/app-store/ping/static/1.png and b/packages/app-store/ping/static/1.png differ diff --git a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx index 5f9098a1732baf..b70747ae9c8fc0 100644 --- a/packages/app-store/routing-forms/pages/forms/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/forms/[...appPages].tsx @@ -173,169 +173,167 @@ export default function RoutingForms({
}> -
-
-
- -
- } - /> - } - noResultsScreen={ - - } - SkeletonLoader={SkeletonLoaderTeamList}> -
- - {forms?.map(({ form, readOnly }, index) => { - if (!form) { - return null; - } +
+
+ +
+ } + /> + } + noResultsScreen={ + + } + SkeletonLoader={SkeletonLoaderTeamList}> +
+ + {forms?.map(({ form, readOnly }, index) => { + if (!form) { + return null; + } - const description = form.description || ""; - form.routes = form.routes || []; - const fields = form.fields || []; - const userRoutes = form.routes.filter((route) => !isFallbackRoute(route)); - const firstItem = forms[0].form; - const lastItem = forms[forms.length - 1].form; + const description = form.description || ""; + form.routes = form.routes || []; + const fields = form.fields || []; + const userRoutes = form.routes.filter((route) => !isFallbackRoute(route)); + const firstItem = forms[0].form; + const lastItem = forms[forms.length - 1].form; - return ( -
- {!(firstItem && firstItem.id === form.id) && ( - moveRoutingForm(index, -1)} arrowDirection="up" /> - )} + return ( +
+ {!(firstItem && firstItem.id === form.id) && ( + moveRoutingForm(index, -1)} arrowDirection="up" /> + )} - {!(lastItem && lastItem.id === form.id) && ( - moveRoutingForm(index, 1)} arrowDirection="down" /> - )} - - {form.team?.name && ( -
- - {form.team.name} - -
- )} - - - - - + {!(lastItem && lastItem.id === form.id) && ( + moveRoutingForm(index, 1)} arrowDirection="down" /> + )} + + {form.team?.name && ( +
+ + {form.team.name} + +
+ )} + + + + + + + - - - {t("edit")} - - - {t("download_responses")} - + color="minimal" + className="!flex" + StartIcon="pencil"> + {t("edit")} + + + {t("download_responses")} + + + {t("duplicate")} + + {typeformApp?.isInstalled ? ( - {t("duplicate")} - - {typeformApp?.isInstalled ? ( - - {t("Copy Typeform Redirect Url")} - - ) : null} - - {t("delete")} + type="button" + StartIcon="link"> + {t("Copy Typeform Redirect Url")} - - - - }> -
- - {fields.length} {fields.length === 1 ? "field" : "fields"} - - - {userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"} - - - {form._count.responses}{" "} - {form._count.responses === 1 ? "response" : "responses"} - -
-
-
- ); - })} - -
- -
+ ) : null} + + {t("delete")} + + + + + }> +
+ + {fields.length} {fields.length === 1 ? "field" : "fields"} + + + {userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"} + + + {form._count.responses}{" "} + {form._count.responses === 1 ? "response" : "responses"} + +
+ +
+ ); + })} +
+
+
diff --git a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx index ed50535bade222..ca5e2bc08a87ae 100644 --- a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx @@ -57,6 +57,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ { label: t("text"), value: SalesforceFieldType.TEXT }, { label: t("date"), value: SalesforceFieldType.DATE }, { label: t("phone").charAt(0).toUpperCase() + t("phone").slice(1), value: SalesforceFieldType.PHONE }, + { label: t("custom"), value: SalesforceFieldType.CUSTOM }, ]; const [writeToPersonObjectFieldType, setWriteToPersonObjectFieldType] = useState(fieldTypeOptions[0]); diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 766baefa5800eb..ea00be2ecdee57 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -902,10 +902,8 @@ export default class SalesforceCRMService implements CRM { ) { const conn = await this.conn; const { createEventOn, onBookingWriteToRecordFields = {} } = this.getAppOptions(); - // Determine record type (Contact or Lead) const personRecordType = this.determinePersonRecordType(createEventOn); - // Search the fields and ensure 1. they exist 2. they're the right type const fieldsToWriteOn = Object.keys(onBookingWriteToRecordFields); const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType); @@ -930,7 +928,6 @@ export default class SalesforceCRMService implements CRM { organizerEmail, calEventResponses, }); - // Update the person record await conn .sobject(personRecordType) @@ -970,6 +967,19 @@ export default class SalesforceCRMService implements CRM { continue; } + if (fieldConfig.fieldType === SalesforceFieldType.CUSTOM) { + const extractedValue = await this.getTextFieldValue({ + fieldValue: fieldConfig.value, + fieldLength: field.length, + calEventResponses, + bookingUid, + }); + if (extractedValue) { + writeOnRecordBody[field.name] = extractedValue; + continue; + } + } + // Handle different field types if (fieldConfig.fieldType === field.type) { if (field.type === SalesforceFieldType.TEXT || field.type === SalesforceFieldType.PHONE) { @@ -1034,7 +1044,7 @@ export default class SalesforceCRMService implements CRM { bookingUid, }: { fieldValue: string; - fieldLength: number; + fieldLength?: number; calEventResponses?: CalEventResponses | null; bookingUid?: string | null; }) { @@ -1042,7 +1052,6 @@ export default class SalesforceCRMService implements CRM { if (!fieldValue.startsWith("{") && !fieldValue.endsWith("}")) return fieldValue; let valueToWrite = fieldValue; - if (fieldValue.startsWith("{form:")) { // Get routing from response if (!bookingUid) return; @@ -1057,7 +1066,7 @@ export default class SalesforceCRMService implements CRM { if (valueToWrite === fieldValue) return; // Trim incase the replacement values increased the length - return valueToWrite.substring(0, fieldLength); + return fieldLength ? valueToWrite.substring(0, fieldLength) : valueToWrite; } private async getTextValueFromRoutingFormResponse(fieldValue: string, bookingUid: string) { @@ -1082,7 +1091,6 @@ export default class SalesforceCRMService implements CRM { // Search for fieldValue, only handle raw text return for now for (const fieldId of Object.keys(response)) { const field = response[fieldId]; - if (field?.identifier === identifierField) { return field.value.toString(); } diff --git a/packages/app-store/salesforce/lib/enums.ts b/packages/app-store/salesforce/lib/enums.ts index d345bf2a42b98c..9d66ba306b6057 100644 --- a/packages/app-store/salesforce/lib/enums.ts +++ b/packages/app-store/salesforce/lib/enums.ts @@ -13,6 +13,7 @@ export enum SalesforceFieldType { DATE = "date", TEXT = "string", PHONE = "phone", + CUSTOM = "custom", } export enum DateFieldTypeData { diff --git a/packages/app-store/zoomvideo/static/zoom4.png b/packages/app-store/zoomvideo/static/zoom4.png index b17b2678d867fe..fb42f33b5bb4c9 100644 Binary files a/packages/app-store/zoomvideo/static/zoom4.png and b/packages/app-store/zoomvideo/static/zoom4.png differ diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 4baa35c80d964d..1555766da62966 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -286,6 +286,15 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA shouldServeCache, } = availabilitySchema.parse(query); + log.debug( + `[getUserAvailability] EventType: ${eventTypeId} | User: ${username} (ID: ${userId}) - Called with: ${safeStringify( + { + query, + initialData, + } + )}` + ); + if (!dateFrom.isValid() || !dateTo.isValid()) { throw new HttpError({ statusCode: 400, message: "Invalid time range given." }); } @@ -296,11 +305,9 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const user = initialData?.user || (await getUser(where)); - if (!user) throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" }); - log.debug( - "getUserAvailability for user", - safeStringify({ user: { id: user.id }, slot: { dateFrom, dateTo } }) - ); + if (!user) { + throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" }); + } let eventType: EventType | null = initialData?.eventType || null; if (!eventType && eventTypeId) eventType = await getEventType(eventTypeId); @@ -420,13 +427,14 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const isDefaultSchedule = userSchedule && userSchedule.id === schedule?.id; log.debug( - "Using schedule:", - safeStringify({ - chosenSchedule: schedule, - eventTypeSchedule: eventType?.schedule, - userSchedule: userSchedule, - hostSchedule: hostSchedule, - }) + `[getUserAvailability] EventType: ${eventTypeId} | User: ${username} (ID: ${userId}) - usingSchedule: ${safeStringify( + { + chosenSchedule: schedule, + eventTypeSchedule: eventType?.schedule, + userSchedule: userSchedule, + hostSchedule: hostSchedule, + } + )}` ); if ( @@ -566,7 +574,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const dateRangesInWhichUserIsAvailable = subtract(dateRanges, formattedBusyTimes); const dateRangesInWhichUserIsAvailableWithoutOOO = subtract(oooExcludedDateRanges, formattedBusyTimes); - return { + const result = { busy: detailedBusyTimes, timeZone, dateRanges: dateRangesInWhichUserIsAvailable, @@ -576,6 +584,14 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA currentSeats, datesOutOfOffice, }; + + log.debug( + `[getUserAvailability] EventType: ${eventTypeId} | User: ${username} (ID: ${userId}) - Result: ${safeStringify( + result + )}` + ); + + return result; }; export const getPeriodStartDatesBetween = ( diff --git a/packages/emails/src/templates/DailyVideoDownloadTranscriptEmail.tsx b/packages/emails/src/templates/DailyVideoDownloadTranscriptEmail.tsx index 316e370ed409c7..3c9fa5ae9fcfc6 100644 --- a/packages/emails/src/templates/DailyVideoDownloadTranscriptEmail.tsx +++ b/packages/emails/src/templates/DailyVideoDownloadTranscriptEmail.tsx @@ -1,8 +1,8 @@ import type { TFunction } from "next-i18next"; -import { WEBAPP_URL, APP_NAME, COMPANY_NAME } from "@calcom/lib/constants"; +import { WEBAPP_URL, COMPANY_NAME } from "@calcom/lib/constants"; -import { V2BaseEmailHtml, CallToAction } from "../components"; +import { V2BaseEmailHtml } from "../components"; interface DailyVideoDownloadTranscriptEmailProps { language: TFunction; @@ -55,13 +55,13 @@ export const DailyVideoDownloadTranscriptEmail = ( <>{props.language("hi_user_name", { name: props.name })},

- <>{props.language("transcript_from_previous_call", { appName: APP_NAME })} + <>{props.language("you_can_download_transcript_from_attachments")}

- {props.transcriptDownloadLinks.map((downloadLink, index) => { + {props.transcriptDownloadLinks.map((_, index) => { return (
{props.date} Transcript {index + 1}

-
); })} diff --git a/packages/emails/templates/attendee-daily-video-download-transcript-email.ts b/packages/emails/templates/attendee-daily-video-download-transcript-email.ts index 5c7355016c1e40..a063e212f866b8 100644 --- a/packages/emails/templates/attendee-daily-video-download-transcript-email.ts +++ b/packages/emails/templates/attendee-daily-video-download-transcript-email.ts @@ -21,6 +21,18 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail this.t = attendee.language.translate; } protected async getNodeMailerPayload(): Promise> { + const attachments = await Promise.all( + this.transcriptDownloadLinks.map(async (url, index) => { + const response = await fetch(url); + const buffer = await response.arrayBuffer(); + return { + filename: `transcript-${index + 1}.vtt`, + content: Buffer.from(buffer), + contentType: "text/vtt", + }; + }) + ); + return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -36,6 +48,7 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail language: this.t, name: this.attendee.name, }), + attachments, }; } diff --git a/packages/emails/templates/organizer-daily-video-download-transcript-email.ts b/packages/emails/templates/organizer-daily-video-download-transcript-email.ts index 058b48d954b4ec..7a41a35c587111 100644 --- a/packages/emails/templates/organizer-daily-video-download-transcript-email.ts +++ b/packages/emails/templates/organizer-daily-video-download-transcript-email.ts @@ -20,6 +20,18 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai this.t = this.calEvent.organizer.language.translate; } protected async getNodeMailerPayload(): Promise> { + const attachments = await Promise.all( + this.transcriptDownloadLinks.map(async (url, index) => { + const response = await fetch(url); + const buffer = await response.arrayBuffer(); + return { + filename: `transcript-${index + 1}.vtt`, + content: Buffer.from(buffer), + contentType: "text/vtt", + }; + }) + ); + return { to: `${this.calEvent.organizer.email}>`, from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`, @@ -35,6 +47,7 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai language: this.t, name: this.calEvent.organizer.name, }), + attachments, }; } diff --git a/apps/web/components/auth/Turnstile.tsx b/packages/features/auth/Turnstile.tsx similarity index 100% rename from apps/web/components/auth/Turnstile.tsx rename to packages/features/auth/Turnstile.tsx diff --git a/packages/features/auth/lib/dub.ts b/packages/features/auth/lib/dub.ts index 59acf19b971d21..fc3317594ff9f2 100644 --- a/packages/features/auth/lib/dub.ts +++ b/packages/features/auth/lib/dub.ts @@ -3,3 +3,16 @@ import { Dub } from "dub"; export const dub = new Dub({ token: process.env.DUB_API_KEY, }); + +export const getDubCustomer = async (userId: string) => { + if (!process.env.DUB_API_KEY) { + return null; + } + + const customer = await dub.customers.list({ + externalId: userId, + includeExpandedFields: true, + }); + + return customer.length > 0 ? customer[0] : null; +}; diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json index ba504222041816..73c312f394a1d9 100644 --- a/packages/features/auth/package.json +++ b/packages/features/auth/package.json @@ -13,7 +13,7 @@ "@calcom/trpc": "*", "@calcom/ui": "*", "bcryptjs": "^2.4.3", - "dub": "^0.35.0", + "dub": "^0.46.15", "handlebars": "^4.7.7", "jose": "^4.13.1", "lru-cache": "^9.0.3", diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 4cd6355e6e2c4d..726fcd840d0449 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -72,6 +72,7 @@ const BookerComponent = ({ areInstantMeetingParametersSet = false, userLocale, hasValidLicense, + renderCaptcha, }: BookerProps & WrappedBookerProps) => { const searchParams = useCompatSearchParams(); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); @@ -166,6 +167,7 @@ const BookerComponent = ({ return bookerState === "booking" ? ( { setSelectedTimeslot(null); if (seatedEventData.bookingUid) { diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index fca672531db43c..612ac7c7123d19 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -1,12 +1,18 @@ import type { TFunction } from "next-i18next"; import { Trans } from "next-i18next"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useMemo, useState } from "react"; import type { FieldError } from "react-hook-form"; import { useIsPlatformBookerEmbed } from "@calcom/atoms/monorepo"; import type { BookerEvent } from "@calcom/features/bookings/types"; -import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; +import { + WEBSITE_PRIVACY_POLICY_URL, + WEBSITE_TERMS_URL, + CLOUDFLARE_SITE_ID, + CLOUDFLARE_USE_TURNSTILE_IN_BOOKER, +} from "@calcom/lib/constants"; import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, Button, EmptyScreen, Form } from "@calcom/ui"; @@ -17,6 +23,8 @@ import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBo import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; +const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false }); + type BookEventFormProps = { onCancel?: () => void; onSubmit: () => void; @@ -29,6 +37,7 @@ type BookEventFormProps = { extraOptions: Record; isPlatform?: boolean; isVerificationCodeSending: boolean; + renderCaptcha?: boolean; }; export const BookEventForm = ({ @@ -45,6 +54,7 @@ export const BookEventForm = ({ extraOptions, isVerificationCodeSending, isPlatform = false, + renderCaptcha, }: Omit & { eventQuery: { isError: boolean; @@ -61,6 +71,10 @@ export const BookEventForm = ({ const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); + // Cloudflare Turnstile Captcha + const shouldRenderCaptcha = + renderCaptcha && CLOUDFLARE_SITE_ID && CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1"; + const [responseVercelIdHeader] = useState(null); const { t } = useLocale(); @@ -88,6 +102,8 @@ export const BookEventForm = ({ return ; } + const watchedCfToken = bookingForm.watch("cfToken"); + return (
)} + {/* Cloudflare Turnstile Captcha */} + {shouldRenderCaptcha ? ( + { + bookingForm.setValue("cfToken", token); + }} + /> + ) : null} {!isPlatform && (
)} + {isPlatformBookerEmbed && (
{t("proceeding_agreement")}{" "} @@ -176,9 +202,11 @@ export const BookEventForm = ({ {t("back")} )} +