From cf5507f57eee50c10d967a2892d39fea75f2f763 Mon Sep 17 00:00:00 2001 From: "Marvel.Codes" Date: Sat, 4 Apr 2026 22:45:02 +0100 Subject: [PATCH 1/3] feat: add Paystack payment integration Add Paystack as a payment provider for Cal.com, enabling hosts to collect payments via Paystack when attendees book events. Paystack is widely used across Africa and supports NGN, GHS, ZAR, KES, and USD currencies. Key components: - PaymentService implementing IAbstractPaymentService (create, refund, update) - PaystackClient REST wrapper for Paystack API (initialize, verify, refund) - Webhook endpoint with HMAC-SHA512 signature verification - Verify endpoint for callback-based payment confirmation - Event type app card UI for configuring price, currency, and refund policy - Setup page for entering Paystack API keys (public + secret) - PaystackPaymentComponent using Paystack inline popup checkout - App install flow via add.ts with isOAuth: true for proper setup routing Follows existing payment app patterns (HitPay, Stripe) for: - App store registration (config.json, _metadata.ts, zod schemas) - Server-side props for setup page authentication - Generated file registration (payment.services, apps.metadata, etc.) - Seed script entry for local development --- .../payment/[uid]/PaymentPage.tsx | 19 + apps/web/components/apps/AppSetupPage.tsx | 1 + apps/web/components/apps/paystack/Setup.tsx | 132 +++++ .../_pages/setup/_getServerSideProps.tsx | 1 + .../app-store/analytics.services.generated.ts | 9 +- packages/app-store/apps.browser.generated.tsx | 112 ++--- .../app-store/apps.keys-schemas.generated.ts | 208 ++++---- packages/app-store/apps.metadata.generated.ts | 452 +++++++++--------- packages/app-store/apps.schemas.generated.ts | 208 ++++---- packages/app-store/apps.server.generated.ts | 173 +++---- .../bookerApps.metadata.generated.ts | 182 +++---- .../app-store/calendar.services.generated.ts | 29 +- packages/app-store/crm.apps.generated.ts | 14 +- .../app-store/payment.services.generated.ts | 15 +- packages/app-store/paystack/_metadata.ts | 22 + packages/app-store/paystack/api/add.ts | 45 ++ packages/app-store/paystack/api/index.ts | 3 + packages/app-store/paystack/api/verify.ts | 109 +++++ packages/app-store/paystack/api/webhook.ts | 148 ++++++ .../components/EventTypeAppCardInterface.tsx | 59 +++ .../EventTypeAppSettingsInterface.tsx | 167 +++++++ .../components/PaystackPaymentComponent.tsx | 115 +++++ packages/app-store/paystack/config.json | 20 + packages/app-store/paystack/index.ts | 2 + .../app-store/paystack/lib/PaymentService.ts | 207 ++++++++ .../app-store/paystack/lib/PaystackClient.ts | 84 ++++ .../lib/__tests__/PaystackClient.test.ts | 161 +++++++ .../__tests__/verifyWebhookSignature.test.ts | 28 ++ .../app-store/paystack/lib/currencyOptions.ts | 19 + .../paystack/lib/verifyWebhookSignature.ts | 13 + packages/app-store/paystack/package.json | 20 + .../pages/setup/_getServerSideProps.ts | 40 ++ packages/app-store/paystack/static/icon.svg | 1 + packages/app-store/paystack/zod.ts | 20 + packages/app-store/redirect-apps.generated.ts | 33 +- .../app-store/video.adapters.generated.ts | 31 +- packages/i18n/locales/en/common.json | 12 + scripts/seed-app-store.ts | 3 + yarn.lock | 21 +- 39 files changed, 2186 insertions(+), 752 deletions(-) create mode 100644 apps/web/components/apps/paystack/Setup.tsx create mode 100644 packages/app-store/paystack/_metadata.ts create mode 100644 packages/app-store/paystack/api/add.ts create mode 100644 packages/app-store/paystack/api/index.ts create mode 100644 packages/app-store/paystack/api/verify.ts create mode 100644 packages/app-store/paystack/api/webhook.ts create mode 100644 packages/app-store/paystack/components/EventTypeAppCardInterface.tsx create mode 100644 packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx create mode 100644 packages/app-store/paystack/components/PaystackPaymentComponent.tsx create mode 100644 packages/app-store/paystack/config.json create mode 100644 packages/app-store/paystack/index.ts create mode 100644 packages/app-store/paystack/lib/PaymentService.ts create mode 100644 packages/app-store/paystack/lib/PaystackClient.ts create mode 100644 packages/app-store/paystack/lib/__tests__/PaystackClient.test.ts create mode 100644 packages/app-store/paystack/lib/__tests__/verifyWebhookSignature.test.ts create mode 100644 packages/app-store/paystack/lib/currencyOptions.ts create mode 100644 packages/app-store/paystack/lib/verifyWebhookSignature.ts create mode 100644 packages/app-store/paystack/package.json create mode 100644 packages/app-store/paystack/pages/setup/_getServerSideProps.ts create mode 100644 packages/app-store/paystack/static/icon.svg create mode 100644 packages/app-store/paystack/zod.ts diff --git a/apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx b/apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx index 71367a72a882ad..4936c22ccb3849 100644 --- a/apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx +++ b/apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx @@ -56,6 +56,13 @@ const BtcpayPaymentComponent = dynamic( } ); +const PaystackPaymentComponent = dynamic( + () => import("@calcom/app-store/paystack/components/PaystackPaymentComponent"), + { + ssr: false, + } +); + const PaymentPage: FC = (props) => { const { t, i18n } = useLocale(); const [is24h, setIs24h] = useState(isBrowserLocale24h()); @@ -174,6 +181,18 @@ const PaymentPage: FC = (props) => { {props.payment.appId === "btcpayserver" && !props.payment.success && ( )} + {props.payment.appId === "paystack" && !props.payment.success && ( + + )} {props.payment.refunded && (
{t("refunded")}
)} diff --git a/apps/web/components/apps/AppSetupPage.tsx b/apps/web/components/apps/AppSetupPage.tsx index 88acf8c89bbb76..7d52d74d86d55b 100644 --- a/apps/web/components/apps/AppSetupPage.tsx +++ b/apps/web/components/apps/AppSetupPage.tsx @@ -15,6 +15,7 @@ export const AppSetupMap = { paypal: dynamic(() => import("@calcom/web/components/apps/paypal/Setup")), hitpay: dynamic(() => import("@calcom/web/components/apps/hitpay/Setup")), btcpayserver: dynamic(() => import("@calcom/web/components/apps/btcpayserver/Setup")), + paystack: dynamic(() => import("@calcom/web/components/apps/paystack/Setup")), }; export const AppSetupPage = (props: { slug: string }) => { diff --git a/apps/web/components/apps/paystack/Setup.tsx b/apps/web/components/apps/paystack/Setup.tsx new file mode 100644 index 00000000000000..71484e73e1da80 --- /dev/null +++ b/apps/web/components/apps/paystack/Setup.tsx @@ -0,0 +1,132 @@ +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Toaster } from "sonner"; + +import AppNotInstalledMessage from "@calcom/app-store/_components/AppNotInstalledMessage"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Button } from "@calcom/ui/components/button"; +import { TextField } from "@calcom/ui/components/form"; +import { showToast } from "@calcom/ui/components/toast"; + +export default function PaystackSetup() { + const [newPublicKey, setNewPublicKey] = useState(""); + const [newSecretKey, setNewSecretKey] = useState(""); + const router = useRouter(); + const { t } = useLocale(); + + const integrations = trpc.viewer.apps.integrations.useQuery({ + variant: "payment", + appId: "paystack", + }); + + const [paystackCredentials] = integrations.data?.items || []; + const [credentialId] = paystackCredentials?.userCredentialIds || [-1]; + + const showContent = !!integrations.data && integrations.isSuccess && !!credentialId; + + const saveKeysMutation = trpc.viewer.apps.updateAppCredentials.useMutation({ + onSuccess: () => { + showToast(t("keys_have_been_saved"), "success"); + router.push("/event-types"); + }, + onError: (error) => { + showToast(error.message, "error"); + }, + }); + + if (integrations.isPending) { + return
; + } + + return ( +
+ {showContent ? ( +
+
+
+ Paystack +

Paystack

+
+ +
{ + e.preventDefault(); + saveKeysMutation.mutate({ + credentialId, + key: { + public_key: newPublicKey, + secret_key: newSecretKey, + }, + }); + }}> + setNewPublicKey(e.target.value)} + role="presentation" + className="mb-6" + placeholder="pk_test_xxxxxxxxx" + /> + + setNewSecretKey(e.target.value)} + placeholder="sk_test_xxxxxxxxx" + /> + +
+ +
+ + +
+

{t("getting_started")}

+

+ {t("paystack_getting_started_description")}{" "} + + {t("paystack_dashboard")} + + . +

+ +

{t("paystack_webhook_setup")}

+

+ {t("paystack_webhook_setup_description")} +

+ + {typeof window !== "undefined" ? window.location.origin : "https://your-cal.com"} + /api/integrations/paystack/webhook + +
+
+
+ ) : ( + + )} + + +
+ ); +} diff --git a/packages/app-store/_pages/setup/_getServerSideProps.tsx b/packages/app-store/_pages/setup/_getServerSideProps.tsx index 8004f5c19ed4a9..31442cb75c8811 100644 --- a/packages/app-store/_pages/setup/_getServerSideProps.tsx +++ b/packages/app-store/_pages/setup/_getServerSideProps.tsx @@ -6,6 +6,7 @@ export const AppSetupPageMap = { stripe: import("../../stripepayment/pages/setup/_getServerSideProps"), hitpay: import("../../hitpay/pages/setup/_getServerSideProps"), btcpayserver: import("../../btcpayserver/pages/setup/_getServerSideProps"), + paystack: import("../../paystack/pages/setup/_getServerSideProps"), }; export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { diff --git a/packages/app-store/analytics.services.generated.ts b/packages/app-store/analytics.services.generated.ts index 92bc722083016c..8f101cded8ea14 100644 --- a/packages/app-store/analytics.services.generated.ts +++ b/packages/app-store/analytics.services.generated.ts @@ -2,9 +2,6 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -export const AnalyticsServiceMap = - process.env.NEXT_PUBLIC_IS_E2E === "1" - ? {} - : { - dub: import("./dub/lib/AnalyticsService"), - }; +export const AnalyticsServiceMap = process.env.NEXT_PUBLIC_IS_E2E === '1' ? {} : { +"dub": import("./dub/lib/AnalyticsService"), +}; \ No newline at end of file diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index cf6fef19fff46f..f2510a99095cb4 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -2,69 +2,63 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import dynamic from "next/dynamic"; +import dynamic from "next/dynamic" export const InstallAppButtonMap = { - exchange2013calendar: dynamic(() => import("./exchange2013calendar/components/InstallAppButton")), - exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")), - office365video: dynamic(() => import("./office365video/components/InstallAppButton")), - vital: dynamic(() => import("./vital/components/InstallAppButton")), +"exchange2013calendar": dynamic(() => import("./exchange2013calendar/components/InstallAppButton")), +"exchange2016calendar": dynamic(() => import("./exchange2016calendar/components/InstallAppButton")), +"office365video": dynamic(() => import("./office365video/components/InstallAppButton")), +"vital": dynamic(() => import("./vital/components/InstallAppButton")), }; export const AppSettingsComponentsMap = { - "general-app-settings": dynamic( - () => import("./templates/general-app-settings/components/AppSettingsInterface") - ), - weather_in_your_calendar: dynamic( - () => import("./weather_in_your_calendar/components/AppSettingsInterface") - ), - zapier: dynamic(() => import("./zapier/components/AppSettingsInterface")), +"general-app-settings": dynamic(() => import("./templates/general-app-settings/components/AppSettingsInterface")), +"weather_in_your_calendar": dynamic(() => import("./weather_in_your_calendar/components/AppSettingsInterface")), +"zapier": dynamic(() => import("./zapier/components/AppSettingsInterface")), }; export const EventTypeAddonMap = { - alby: dynamic(() => import("./alby/components/EventTypeAppCardInterface")), - basecamp3: dynamic(() => import("./basecamp3/components/EventTypeAppCardInterface")), - btcpayserver: dynamic(() => import("./btcpayserver/components/EventTypeAppCardInterface")), - closecom: dynamic(() => import("./closecom/components/EventTypeAppCardInterface")), - databuddy: dynamic(() => import("./databuddy/components/EventTypeAppCardInterface")), - fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), - 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")), - metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")), - "mock-payment-app": dynamic(() => import("./mock-payment-app/components/EventTypeAppCardInterface")), - paypal: dynamic(() => import("./paypal/components/EventTypeAppCardInterface")), - "pipedrive-crm": dynamic(() => import("./pipedrive-crm/components/EventTypeAppCardInterface")), - plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")), - posthog: dynamic(() => import("./posthog/components/EventTypeAppCardInterface")), - qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")), - salesforce: dynamic(() => import("./salesforce/components/EventTypeAppCardInterface")), - stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppCardInterface")), - "booking-pages-tag": dynamic( - () => import("./templates/booking-pages-tag/components/EventTypeAppCardInterface") - ), - "event-type-app-card": dynamic( - () => import("./templates/event-type-app-card/components/EventTypeAppCardInterface") - ), - twipla: dynamic(() => import("./twipla/components/EventTypeAppCardInterface")), - umami: dynamic(() => import("./umami/components/EventTypeAppCardInterface")), - "zoho-bigin": dynamic(() => import("./zoho-bigin/components/EventTypeAppCardInterface")), - zohocrm: dynamic(() => import("./zohocrm/components/EventTypeAppCardInterface")), +"alby": dynamic(() => import("./alby/components/EventTypeAppCardInterface")), +"basecamp3": dynamic(() => import("./basecamp3/components/EventTypeAppCardInterface")), +"btcpayserver": dynamic(() => import("./btcpayserver/components/EventTypeAppCardInterface")), +"closecom": dynamic(() => import("./closecom/components/EventTypeAppCardInterface")), +"databuddy": dynamic(() => import("./databuddy/components/EventTypeAppCardInterface")), +"fathom": dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), +"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")), +"metapixel": dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")), +"mock-payment-app": dynamic(() => import("./mock-payment-app/components/EventTypeAppCardInterface")), +"paypal": dynamic(() => import("./paypal/components/EventTypeAppCardInterface")), +"paystack": dynamic(() => import("./paystack/components/EventTypeAppCardInterface")), +"pipedrive-crm": dynamic(() => import("./pipedrive-crm/components/EventTypeAppCardInterface")), +"plausible": dynamic(() => import("./plausible/components/EventTypeAppCardInterface")), +"posthog": dynamic(() => import("./posthog/components/EventTypeAppCardInterface")), +"qr_code": dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")), +"salesforce": dynamic(() => import("./salesforce/components/EventTypeAppCardInterface")), +"stripepayment": dynamic(() => import("./stripepayment/components/EventTypeAppCardInterface")), +"booking-pages-tag": dynamic(() => import("./templates/booking-pages-tag/components/EventTypeAppCardInterface")), +"event-type-app-card": dynamic(() => import("./templates/event-type-app-card/components/EventTypeAppCardInterface")), +"twipla": dynamic(() => import("./twipla/components/EventTypeAppCardInterface")), +"umami": dynamic(() => import("./umami/components/EventTypeAppCardInterface")), +"zoho-bigin": dynamic(() => import("./zoho-bigin/components/EventTypeAppCardInterface")), +"zohocrm": dynamic(() => import("./zohocrm/components/EventTypeAppCardInterface")), }; export const EventTypeSettingsMap = { - alby: dynamic(() => import("./alby/components/EventTypeAppSettingsInterface")), - basecamp3: dynamic(() => import("./basecamp3/components/EventTypeAppSettingsInterface")), - btcpayserver: dynamic(() => import("./btcpayserver/components/EventTypeAppSettingsInterface")), - databuddy: dynamic(() => import("./databuddy/components/EventTypeAppSettingsInterface")), - fathom: dynamic(() => import("./fathom/components/EventTypeAppSettingsInterface")), - 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")), - qr_code: dynamic(() => import("./qr_code/components/EventTypeAppSettingsInterface")), - stripepayment: dynamic(() => import("./stripepayment/components/EventTypeAppSettingsInterface")), -}; +"alby": dynamic(() => import("./alby/components/EventTypeAppSettingsInterface")), +"basecamp3": dynamic(() => import("./basecamp3/components/EventTypeAppSettingsInterface")), +"btcpayserver": dynamic(() => import("./btcpayserver/components/EventTypeAppSettingsInterface")), +"databuddy": dynamic(() => import("./databuddy/components/EventTypeAppSettingsInterface")), +"fathom": dynamic(() => import("./fathom/components/EventTypeAppSettingsInterface")), +"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")), +"paystack": dynamic(() => import("./paystack/components/EventTypeAppSettingsInterface")), +"plausible": dynamic(() => import("./plausible/components/EventTypeAppSettingsInterface")), +"qr_code": dynamic(() => import("./qr_code/components/EventTypeAppSettingsInterface")), +"stripepayment": dynamic(() => import("./stripepayment/components/EventTypeAppSettingsInterface")), +}; \ No newline at end of file diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 5df66d19f7411c..5f65e45a5407d3 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -2,107 +2,109 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import { appKeysSchema as alby_zod_ts } from "./alby/zod"; -import { appKeysSchema as basecamp3_zod_ts } from "./basecamp3/zod"; -import { appKeysSchema as btcpayserver_zod_ts } from "./btcpayserver/zod"; -import { appKeysSchema as closecom_zod_ts } from "./closecom/zod"; -import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; -import { appKeysSchema as databuddy_zod_ts } from "./databuddy/zod"; -import { appKeysSchema as dub_zod_ts } from "./dub/zod"; -import { appKeysSchema as fathom_zod_ts } from "./fathom/zod"; -import { appKeysSchema as feishucalendar_zod_ts } from "./feishucalendar/zod"; -import { appKeysSchema as ga4_zod_ts } from "./ga4/zod"; -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"; -import { appKeysSchema as jelly_zod_ts } from "./jelly/zod"; -import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; -import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; -import { appKeysSchema as lyra_zod_ts } from "./lyra/zod"; -import { appKeysSchema as make_zod_ts } from "./make/zod"; -import { appKeysSchema as matomo_zod_ts } from "./matomo/zod"; -import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod"; -import { appKeysSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod"; -import { appKeysSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod"; -import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod"; -import { appKeysSchema as office365video_zod_ts } from "./office365video/zod"; -import { appKeysSchema as paypal_zod_ts } from "./paypal/zod"; -import { appKeysSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod"; -import { appKeysSchema as plausible_zod_ts } from "./plausible/zod"; -import { appKeysSchema as posthog_zod_ts } from "./posthog/zod"; -import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod"; -import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod"; -import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod"; -import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod"; -import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod"; -import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; -import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; -import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; -import { appKeysSchema as twipla_zod_ts } from "./twipla/zod"; -import { appKeysSchema as umami_zod_ts } from "./umami/zod"; -import { appKeysSchema as vital_zod_ts } from "./vital/zod"; -import { appKeysSchema as webex_zod_ts } from "./webex/zod"; -import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod"; -import { appKeysSchema as zapier_zod_ts } from "./zapier/zod"; -import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; -import { appKeysSchema as zohocalendar_zod_ts } from "./zohocalendar/zod"; -import { appKeysSchema as zohocrm_zod_ts } from "./zohocrm/zod"; -import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; +import { appKeysSchema as alby_zod_ts } from "./alby/zod" +import { appKeysSchema as basecamp3_zod_ts } from "./basecamp3/zod" +import { appKeysSchema as btcpayserver_zod_ts } from "./btcpayserver/zod" +import { appKeysSchema as closecom_zod_ts } from "./closecom/zod" +import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod" +import { appKeysSchema as databuddy_zod_ts } from "./databuddy/zod" +import { appKeysSchema as dub_zod_ts } from "./dub/zod" +import { appKeysSchema as fathom_zod_ts } from "./fathom/zod" +import { appKeysSchema as feishucalendar_zod_ts } from "./feishucalendar/zod" +import { appKeysSchema as ga4_zod_ts } from "./ga4/zod" +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" +import { appKeysSchema as jelly_zod_ts } from "./jelly/zod" +import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod" +import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod" +import { appKeysSchema as lyra_zod_ts } from "./lyra/zod" +import { appKeysSchema as make_zod_ts } from "./make/zod" +import { appKeysSchema as matomo_zod_ts } from "./matomo/zod" +import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod" +import { appKeysSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod" +import { appKeysSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod" +import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod" +import { appKeysSchema as office365video_zod_ts } from "./office365video/zod" +import { appKeysSchema as paypal_zod_ts } from "./paypal/zod" +import { appKeysSchema as paystack_zod_ts } from "./paystack/zod" +import { appKeysSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod" +import { appKeysSchema as plausible_zod_ts } from "./plausible/zod" +import { appKeysSchema as posthog_zod_ts } from "./posthog/zod" +import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod" +import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod" +import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod" +import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod" +import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod" +import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod" +import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod" +import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod" +import { appKeysSchema as twipla_zod_ts } from "./twipla/zod" +import { appKeysSchema as umami_zod_ts } from "./umami/zod" +import { appKeysSchema as vital_zod_ts } from "./vital/zod" +import { appKeysSchema as webex_zod_ts } from "./webex/zod" +import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod" +import { appKeysSchema as zapier_zod_ts } from "./zapier/zod" +import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod" +import { appKeysSchema as zohocalendar_zod_ts } from "./zohocalendar/zod" +import { appKeysSchema as zohocrm_zod_ts } from "./zohocrm/zod" +import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod" export const appKeysSchemas = { - alby: alby_zod_ts, - basecamp3: basecamp3_zod_ts, - btcpayserver: btcpayserver_zod_ts, - closecom: closecom_zod_ts, - dailyvideo: dailyvideo_zod_ts, - databuddy: databuddy_zod_ts, - dub: dub_zod_ts, - fathom: fathom_zod_ts, - feishucalendar: feishucalendar_zod_ts, - ga4: ga4_zod_ts, - giphy: giphy_zod_ts, - 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, - jelly: jelly_zod_ts, - jitsivideo: jitsivideo_zod_ts, - larkcalendar: larkcalendar_zod_ts, - lyra: lyra_zod_ts, - make: make_zod_ts, - matomo: matomo_zod_ts, - metapixel: metapixel_zod_ts, - "mock-payment-app": mock_payment_app_zod_ts, - nextcloudtalk: nextcloudtalk_zod_ts, - office365calendar: office365calendar_zod_ts, - office365video: office365video_zod_ts, - paypal: paypal_zod_ts, - "pipedrive-crm": pipedrive_crm_zod_ts, - plausible: plausible_zod_ts, - posthog: posthog_zod_ts, - qr_code: qr_code_zod_ts, - "routing-forms": routing_forms_zod_ts, - salesforce: salesforce_zod_ts, - shimmervideo: shimmervideo_zod_ts, - stripe: stripepayment_zod_ts, - tandemvideo: tandemvideo_zod_ts, - "booking-pages-tag": booking_pages_tag_zod_ts, - "event-type-app-card": event_type_app_card_zod_ts, - twipla: twipla_zod_ts, - umami: umami_zod_ts, - vital: vital_zod_ts, - webex: webex_zod_ts, - wordpress: wordpress_zod_ts, - zapier: zapier_zod_ts, - "zoho-bigin": zoho_bigin_zod_ts, - zohocalendar: zohocalendar_zod_ts, - zohocrm: zohocrm_zod_ts, - zoomvideo: zoomvideo_zod_ts, -}; +"alby": alby_zod_ts, +"basecamp3": basecamp3_zod_ts, +"btcpayserver": btcpayserver_zod_ts, +"closecom": closecom_zod_ts, +"dailyvideo": dailyvideo_zod_ts, +"databuddy": databuddy_zod_ts, +"dub": dub_zod_ts, +"fathom": fathom_zod_ts, +"feishucalendar": feishucalendar_zod_ts, +"ga4": ga4_zod_ts, +"giphy": giphy_zod_ts, +"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, +"jelly": jelly_zod_ts, +"jitsivideo": jitsivideo_zod_ts, +"larkcalendar": larkcalendar_zod_ts, +"lyra": lyra_zod_ts, +"make": make_zod_ts, +"matomo": matomo_zod_ts, +"metapixel": metapixel_zod_ts, +"mock-payment-app": mock_payment_app_zod_ts, +"nextcloudtalk": nextcloudtalk_zod_ts, +"office365calendar": office365calendar_zod_ts, +"office365video": office365video_zod_ts, +"paypal": paypal_zod_ts, +"paystack": paystack_zod_ts, +"pipedrive-crm": pipedrive_crm_zod_ts, +"plausible": plausible_zod_ts, +"posthog": posthog_zod_ts, +"qr_code": qr_code_zod_ts, +"routing-forms": routing_forms_zod_ts, +"salesforce": salesforce_zod_ts, +"shimmervideo": shimmervideo_zod_ts, +"stripe": stripepayment_zod_ts, +"tandemvideo": tandemvideo_zod_ts, +"booking-pages-tag": booking_pages_tag_zod_ts, +"event-type-app-card": event_type_app_card_zod_ts, +"twipla": twipla_zod_ts, +"umami": umami_zod_ts, +"vital": vital_zod_ts, +"webex": webex_zod_ts, +"wordpress": wordpress_zod_ts, +"zapier": zapier_zod_ts, +"zoho-bigin": zoho_bigin_zod_ts, +"zohocalendar": zohocalendar_zod_ts, +"zohocrm": zohocrm_zod_ts, +"zoomvideo": zoomvideo_zod_ts, +}; \ No newline at end of file diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index f964d1780504f3..74edbcbe5a5c9c 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -2,229 +2,231 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import alby_config_json from "./alby/config.json"; -import amie_config_json from "./amie/config.json"; -import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata"; -import attio_config_json from "./attio/config.json"; -import autocheckin_config_json from "./autocheckin/config.json"; -import baa_for_hipaa_config_json from "./baa-for-hipaa/config.json"; -import basecamp3_config_json from "./basecamp3/config.json"; -import bolna_config_json from "./bolna/config.json"; -import btcpayserver_config_json from "./btcpayserver/config.json"; -import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata"; -import campfire_config_json from "./campfire/config.json"; -import caretta_config_json from "./caretta/config.json"; -import chatbase_config_json from "./chatbase/config.json"; -import clic_config_json from "./clic/config.json"; -import closecom_config_json from "./closecom/config.json"; -import cron_config_json from "./cron/config.json"; -import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; -import databuddy_config_json from "./databuddy/config.json"; -import deel_config_json from "./deel/config.json"; -import demodesk_config_json from "./demodesk/config.json"; -import dialpad_config_json from "./dialpad/config.json"; -import discord_config_json from "./discord/config.json"; -import dub_config_json from "./dub/config.json"; -import eightxeight_config_json from "./eightxeight/config.json"; -import element_call_config_json from "./element-call/config.json"; -import elevenlabs_config_json from "./elevenlabs/config.json"; -import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata"; -import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata"; -import exchangecalendar_config_json from "./exchangecalendar/config.json"; -import facetime_config_json from "./facetime/config.json"; -import famulor_config_json from "./famulor/config.json"; -import fathom_config_json from "./fathom/config.json"; -import { metadata as feishucalendar__metadata_ts } from "./feishucalendar/_metadata"; -import fonio_ai_config_json from "./fonio-ai/config.json"; -import framer_config_json from "./framer/config.json"; -import ga4_config_json from "./ga4/config.json"; -import { metadata as giphy__metadata_ts } from "./giphy/_metadata"; -import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata"; -import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; -import granola_config_json from "./granola/config.json"; -import greetmate_ai_config_json from "./greetmate-ai/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"; -import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json"; -import insihts_config_json from "./insihts/config.json"; -import intercom_config_json from "./intercom/config.json"; -import jelly_config_json from "./jelly/config.json"; -import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; -import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; -import lindy_config_json from "./lindy/config.json"; -import linear_config_json from "./linear/config.json"; -import lyra_config_json from "./lyra/config.json"; -import make_config_json from "./make/config.json"; -import matomo_config_json from "./matomo/config.json"; -import metapixel_config_json from "./metapixel/config.json"; -import millis_ai_config_json from "./millis-ai/config.json"; -import mirotalk_config_json from "./mirotalk/config.json"; -import mock_payment_app_config_json from "./mock-payment-app/config.json"; -import monobot_config_json from "./monobot/config.json"; -import n8n_config_json from "./n8n/config.json"; -import nextcloudtalk_config_json from "./nextcloudtalk/config.json"; -import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata"; -import office365video_config_json from "./office365video/config.json"; -import paypal_config_json from "./paypal/config.json"; -import ping_config_json from "./ping/config.json"; -import pipedream_config_json from "./pipedream/config.json"; -import pipedrive_crm_config_json from "./pipedrive-crm/config.json"; -import plausible_config_json from "./plausible/config.json"; -import posthog_config_json from "./posthog/config.json"; -import qr_code_config_json from "./qr_code/config.json"; -import raycast_config_json from "./raycast/config.json"; -import retell_ai_config_json from "./retell-ai/config.json"; -import riverside_config_json from "./riverside/config.json"; -import roam_config_json from "./roam/config.json"; -import routing_forms_config_json from "./routing-forms/config.json"; -import salesforce_config_json from "./salesforce/config.json"; -import salesroom_config_json from "./salesroom/config.json"; -import sendgrid_config_json from "./sendgrid/config.json"; -import shimmervideo_config_json from "./shimmervideo/config.json"; -import signal_config_json from "./signal/config.json"; -import sirius_video_config_json from "./sirius_video/config.json"; -import skype_config_json from "./skype/config.json"; -import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata"; -import sylapsvideo_config_json from "./sylapsvideo/config.json"; -import synthflow_config_json from "./synthflow/config.json"; -import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; -import telegram_config_json from "./telegram/config.json"; -import telli_config_json from "./telli/config.json"; -import basic_config_json from "./templates/basic/config.json"; -import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json"; -import event_type_app_card_config_json from "./templates/event-type-app-card/config.json"; -import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json"; -import general_app_settings_config_json from "./templates/general-app-settings/config.json"; -import link_as_an_app_config_json from "./templates/link-as-an-app/config.json"; -import twipla_config_json from "./twipla/config.json"; -import umami_config_json from "./umami/config.json"; -import vimcal_config_json from "./vimcal/config.json"; -import { metadata as vital__metadata_ts } from "./vital/_metadata"; -import weather_in_your_calendar_config_json from "./weather_in_your_calendar/config.json"; -import webex_config_json from "./webex/config.json"; -import whatsapp_config_json from "./whatsapp/config.json"; -import whereby_config_json from "./whereby/config.json"; -import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata"; -import wordpress_config_json from "./wordpress/config.json"; -import zapier_config_json from "./zapier/config.json"; -import zoho_bigin_config_json from "./zoho-bigin/config.json"; -import zohocalendar_config_json from "./zohocalendar/config.json"; -import zohocrm_config_json from "./zohocrm/config.json"; -import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; +import alby_config_json from "./alby/config.json" +import amie_config_json from "./amie/config.json" +import { metadata as applecalendar__metadata_ts } from "./applecalendar/_metadata" +import attio_config_json from "./attio/config.json" +import autocheckin_config_json from "./autocheckin/config.json" +import baa_for_hipaa_config_json from "./baa-for-hipaa/config.json" +import basecamp3_config_json from "./basecamp3/config.json" +import bolna_config_json from "./bolna/config.json" +import btcpayserver_config_json from "./btcpayserver/config.json" +import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata" +import campfire_config_json from "./campfire/config.json" +import caretta_config_json from "./caretta/config.json" +import chatbase_config_json from "./chatbase/config.json" +import clic_config_json from "./clic/config.json" +import closecom_config_json from "./closecom/config.json" +import cron_config_json from "./cron/config.json" +import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata" +import databuddy_config_json from "./databuddy/config.json" +import deel_config_json from "./deel/config.json" +import demodesk_config_json from "./demodesk/config.json" +import dialpad_config_json from "./dialpad/config.json" +import discord_config_json from "./discord/config.json" +import dub_config_json from "./dub/config.json" +import eightxeight_config_json from "./eightxeight/config.json" +import element_call_config_json from "./element-call/config.json" +import elevenlabs_config_json from "./elevenlabs/config.json" +import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata" +import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata" +import exchangecalendar_config_json from "./exchangecalendar/config.json" +import facetime_config_json from "./facetime/config.json" +import famulor_config_json from "./famulor/config.json" +import fathom_config_json from "./fathom/config.json" +import { metadata as feishucalendar__metadata_ts } from "./feishucalendar/_metadata" +import fonio_ai_config_json from "./fonio-ai/config.json" +import framer_config_json from "./framer/config.json" +import ga4_config_json from "./ga4/config.json" +import { metadata as giphy__metadata_ts } from "./giphy/_metadata" +import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata" +import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata" +import granola_config_json from "./granola/config.json" +import greetmate_ai_config_json from "./greetmate-ai/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" +import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json" +import insihts_config_json from "./insihts/config.json" +import intercom_config_json from "./intercom/config.json" +import jelly_config_json from "./jelly/config.json" +import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata" +import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata" +import lindy_config_json from "./lindy/config.json" +import linear_config_json from "./linear/config.json" +import lyra_config_json from "./lyra/config.json" +import make_config_json from "./make/config.json" +import matomo_config_json from "./matomo/config.json" +import metapixel_config_json from "./metapixel/config.json" +import millis_ai_config_json from "./millis-ai/config.json" +import mirotalk_config_json from "./mirotalk/config.json" +import mock_payment_app_config_json from "./mock-payment-app/config.json" +import monobot_config_json from "./monobot/config.json" +import n8n_config_json from "./n8n/config.json" +import nextcloudtalk_config_json from "./nextcloudtalk/config.json" +import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata" +import office365video_config_json from "./office365video/config.json" +import paypal_config_json from "./paypal/config.json" +import paystack_config_json from "./paystack/config.json" +import ping_config_json from "./ping/config.json" +import pipedream_config_json from "./pipedream/config.json" +import pipedrive_crm_config_json from "./pipedrive-crm/config.json" +import plausible_config_json from "./plausible/config.json" +import posthog_config_json from "./posthog/config.json" +import qr_code_config_json from "./qr_code/config.json" +import raycast_config_json from "./raycast/config.json" +import retell_ai_config_json from "./retell-ai/config.json" +import riverside_config_json from "./riverside/config.json" +import roam_config_json from "./roam/config.json" +import routing_forms_config_json from "./routing-forms/config.json" +import salesforce_config_json from "./salesforce/config.json" +import salesroom_config_json from "./salesroom/config.json" +import sendgrid_config_json from "./sendgrid/config.json" +import shimmervideo_config_json from "./shimmervideo/config.json" +import signal_config_json from "./signal/config.json" +import sirius_video_config_json from "./sirius_video/config.json" +import skype_config_json from "./skype/config.json" +import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata" +import sylapsvideo_config_json from "./sylapsvideo/config.json" +import synthflow_config_json from "./synthflow/config.json" +import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata" +import telegram_config_json from "./telegram/config.json" +import telli_config_json from "./telli/config.json" +import basic_config_json from "./templates/basic/config.json" +import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json" +import event_type_app_card_config_json from "./templates/event-type-app-card/config.json" +import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json" +import general_app_settings_config_json from "./templates/general-app-settings/config.json" +import link_as_an_app_config_json from "./templates/link-as-an-app/config.json" +import twipla_config_json from "./twipla/config.json" +import umami_config_json from "./umami/config.json" +import vimcal_config_json from "./vimcal/config.json" +import { metadata as vital__metadata_ts } from "./vital/_metadata" +import weather_in_your_calendar_config_json from "./weather_in_your_calendar/config.json" +import webex_config_json from "./webex/config.json" +import whatsapp_config_json from "./whatsapp/config.json" +import whereby_config_json from "./whereby/config.json" +import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata" +import wordpress_config_json from "./wordpress/config.json" +import zapier_config_json from "./zapier/config.json" +import zoho_bigin_config_json from "./zoho-bigin/config.json" +import zohocalendar_config_json from "./zohocalendar/config.json" +import zohocrm_config_json from "./zohocrm/config.json" +import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata" export const appStoreMetadata = { - alby: alby_config_json, - amie: amie_config_json, - applecalendar: applecalendar__metadata_ts, - attio: attio_config_json, - autocheckin: autocheckin_config_json, - "baa-for-hipaa": baa_for_hipaa_config_json, - basecamp3: basecamp3_config_json, - bolna: bolna_config_json, - btcpayserver: btcpayserver_config_json, - caldavcalendar: caldavcalendar__metadata_ts, - campfire: campfire_config_json, - caretta: caretta_config_json, - chatbase: chatbase_config_json, - clic: clic_config_json, - closecom: closecom_config_json, - cron: cron_config_json, - dailyvideo: dailyvideo__metadata_ts, - databuddy: databuddy_config_json, - deel: deel_config_json, - demodesk: demodesk_config_json, - dialpad: dialpad_config_json, - discord: discord_config_json, - dub: dub_config_json, - eightxeight: eightxeight_config_json, - "element-call": element_call_config_json, - elevenlabs: elevenlabs_config_json, - exchange2013calendar: exchange2013calendar__metadata_ts, - exchange2016calendar: exchange2016calendar__metadata_ts, - exchangecalendar: exchangecalendar_config_json, - facetime: facetime_config_json, - famulor: famulor_config_json, - fathom: fathom_config_json, - feishucalendar: feishucalendar__metadata_ts, - "fonio-ai": fonio_ai_config_json, - framer: framer_config_json, - ga4: ga4_config_json, - giphy: giphy__metadata_ts, - googlecalendar: googlecalendar__metadata_ts, - googlevideo: googlevideo__metadata_ts, - granola: granola_config_json, - "greetmate-ai": greetmate_ai_config_json, - gtm: gtm_config_json, - hitpay: hitpay_config_json, - "horizon-workrooms": horizon_workrooms_config_json, - hubspot: hubspot__metadata_ts, - huddle01video: huddle01video__metadata_ts, - "ics-feedcalendar": ics_feedcalendar_config_json, - insihts: insihts_config_json, - intercom: intercom_config_json, - jelly: jelly_config_json, - jitsivideo: jitsivideo__metadata_ts, - larkcalendar: larkcalendar__metadata_ts, - lindy: lindy_config_json, - linear: linear_config_json, - lyra: lyra_config_json, - make: make_config_json, - matomo: matomo_config_json, - metapixel: metapixel_config_json, - "millis-ai": millis_ai_config_json, - mirotalk: mirotalk_config_json, - "mock-payment-app": mock_payment_app_config_json, - monobot: monobot_config_json, - n8n: n8n_config_json, - nextcloudtalk: nextcloudtalk_config_json, - office365calendar: office365calendar__metadata_ts, - office365video: office365video_config_json, - paypal: paypal_config_json, - ping: ping_config_json, - pipedream: pipedream_config_json, - "pipedrive-crm": pipedrive_crm_config_json, - plausible: plausible_config_json, - posthog: posthog_config_json, - qr_code: qr_code_config_json, - raycast: raycast_config_json, - "retell-ai": retell_ai_config_json, - riverside: riverside_config_json, - roam: roam_config_json, - "routing-forms": routing_forms_config_json, - salesforce: salesforce_config_json, - salesroom: salesroom_config_json, - sendgrid: sendgrid_config_json, - shimmervideo: shimmervideo_config_json, - signal: signal_config_json, - sirius_video: sirius_video_config_json, - skype: skype_config_json, - stripepayment: stripepayment__metadata_ts, - sylapsvideo: sylapsvideo_config_json, - synthflow: synthflow_config_json, - tandemvideo: tandemvideo__metadata_ts, - telegram: telegram_config_json, - telli: telli_config_json, - basic: basic_config_json, - "booking-pages-tag": booking_pages_tag_config_json, - "event-type-app-card": event_type_app_card_config_json, - "event-type-location-video-static": event_type_location_video_static_config_json, - "general-app-settings": general_app_settings_config_json, - "link-as-an-app": link_as_an_app_config_json, - twipla: twipla_config_json, - umami: umami_config_json, - vimcal: vimcal_config_json, - vital: vital__metadata_ts, - weather_in_your_calendar: weather_in_your_calendar_config_json, - webex: webex_config_json, - whatsapp: whatsapp_config_json, - whereby: whereby_config_json, - wipemycalother: wipemycalother__metadata_ts, - wordpress: wordpress_config_json, - zapier: zapier_config_json, - "zoho-bigin": zoho_bigin_config_json, - zohocalendar: zohocalendar_config_json, - zohocrm: zohocrm_config_json, - zoomvideo: zoomvideo__metadata_ts, -}; +"alby": alby_config_json, +"amie": amie_config_json, +"applecalendar": applecalendar__metadata_ts, +"attio": attio_config_json, +"autocheckin": autocheckin_config_json, +"baa-for-hipaa": baa_for_hipaa_config_json, +"basecamp3": basecamp3_config_json, +"bolna": bolna_config_json, +"btcpayserver": btcpayserver_config_json, +"caldavcalendar": caldavcalendar__metadata_ts, +"campfire": campfire_config_json, +"caretta": caretta_config_json, +"chatbase": chatbase_config_json, +"clic": clic_config_json, +"closecom": closecom_config_json, +"cron": cron_config_json, +"dailyvideo": dailyvideo__metadata_ts, +"databuddy": databuddy_config_json, +"deel": deel_config_json, +"demodesk": demodesk_config_json, +"dialpad": dialpad_config_json, +"discord": discord_config_json, +"dub": dub_config_json, +"eightxeight": eightxeight_config_json, +"element-call": element_call_config_json, +"elevenlabs": elevenlabs_config_json, +"exchange2013calendar": exchange2013calendar__metadata_ts, +"exchange2016calendar": exchange2016calendar__metadata_ts, +"exchangecalendar": exchangecalendar_config_json, +"facetime": facetime_config_json, +"famulor": famulor_config_json, +"fathom": fathom_config_json, +"feishucalendar": feishucalendar__metadata_ts, +"fonio-ai": fonio_ai_config_json, +"framer": framer_config_json, +"ga4": ga4_config_json, +"giphy": giphy__metadata_ts, +"googlecalendar": googlecalendar__metadata_ts, +"googlevideo": googlevideo__metadata_ts, +"granola": granola_config_json, +"greetmate-ai": greetmate_ai_config_json, +"gtm": gtm_config_json, +"hitpay": hitpay_config_json, +"horizon-workrooms": horizon_workrooms_config_json, +"hubspot": hubspot__metadata_ts, +"huddle01video": huddle01video__metadata_ts, +"ics-feedcalendar": ics_feedcalendar_config_json, +"insihts": insihts_config_json, +"intercom": intercom_config_json, +"jelly": jelly_config_json, +"jitsivideo": jitsivideo__metadata_ts, +"larkcalendar": larkcalendar__metadata_ts, +"lindy": lindy_config_json, +"linear": linear_config_json, +"lyra": lyra_config_json, +"make": make_config_json, +"matomo": matomo_config_json, +"metapixel": metapixel_config_json, +"millis-ai": millis_ai_config_json, +"mirotalk": mirotalk_config_json, +"mock-payment-app": mock_payment_app_config_json, +"monobot": monobot_config_json, +"n8n": n8n_config_json, +"nextcloudtalk": nextcloudtalk_config_json, +"office365calendar": office365calendar__metadata_ts, +"office365video": office365video_config_json, +"paypal": paypal_config_json, +"paystack": paystack_config_json, +"ping": ping_config_json, +"pipedream": pipedream_config_json, +"pipedrive-crm": pipedrive_crm_config_json, +"plausible": plausible_config_json, +"posthog": posthog_config_json, +"qr_code": qr_code_config_json, +"raycast": raycast_config_json, +"retell-ai": retell_ai_config_json, +"riverside": riverside_config_json, +"roam": roam_config_json, +"routing-forms": routing_forms_config_json, +"salesforce": salesforce_config_json, +"salesroom": salesroom_config_json, +"sendgrid": sendgrid_config_json, +"shimmervideo": shimmervideo_config_json, +"signal": signal_config_json, +"sirius_video": sirius_video_config_json, +"skype": skype_config_json, +"stripepayment": stripepayment__metadata_ts, +"sylapsvideo": sylapsvideo_config_json, +"synthflow": synthflow_config_json, +"tandemvideo": tandemvideo__metadata_ts, +"telegram": telegram_config_json, +"telli": telli_config_json, +"basic": basic_config_json, +"booking-pages-tag": booking_pages_tag_config_json, +"event-type-app-card": event_type_app_card_config_json, +"event-type-location-video-static": event_type_location_video_static_config_json, +"general-app-settings": general_app_settings_config_json, +"link-as-an-app": link_as_an_app_config_json, +"twipla": twipla_config_json, +"umami": umami_config_json, +"vimcal": vimcal_config_json, +"vital": vital__metadata_ts, +"weather_in_your_calendar": weather_in_your_calendar_config_json, +"webex": webex_config_json, +"whatsapp": whatsapp_config_json, +"whereby": whereby_config_json, +"wipemycalother": wipemycalother__metadata_ts, +"wordpress": wordpress_config_json, +"zapier": zapier_config_json, +"zoho-bigin": zoho_bigin_config_json, +"zohocalendar": zohocalendar_config_json, +"zohocrm": zohocrm_config_json, +"zoomvideo": zoomvideo__metadata_ts, +}; \ No newline at end of file diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index f4017ac5881a7d..9a136f7fe7ea12 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -2,107 +2,109 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import { appDataSchema as alby_zod_ts } from "./alby/zod"; -import { appDataSchema as basecamp3_zod_ts } from "./basecamp3/zod"; -import { appDataSchema as btcpayserver_zod_ts } from "./btcpayserver/zod"; -import { appDataSchema as closecom_zod_ts } from "./closecom/zod"; -import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; -import { appDataSchema as databuddy_zod_ts } from "./databuddy/zod"; -import { appDataSchema as dub_zod_ts } from "./dub/zod"; -import { appDataSchema as fathom_zod_ts } from "./fathom/zod"; -import { appDataSchema as feishucalendar_zod_ts } from "./feishucalendar/zod"; -import { appDataSchema as ga4_zod_ts } from "./ga4/zod"; -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"; -import { appDataSchema as jelly_zod_ts } from "./jelly/zod"; -import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; -import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; -import { appDataSchema as lyra_zod_ts } from "./lyra/zod"; -import { appDataSchema as make_zod_ts } from "./make/zod"; -import { appDataSchema as matomo_zod_ts } from "./matomo/zod"; -import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod"; -import { appDataSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod"; -import { appDataSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod"; -import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod"; -import { appDataSchema as office365video_zod_ts } from "./office365video/zod"; -import { appDataSchema as paypal_zod_ts } from "./paypal/zod"; -import { appDataSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod"; -import { appDataSchema as plausible_zod_ts } from "./plausible/zod"; -import { appDataSchema as posthog_zod_ts } from "./posthog/zod"; -import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod"; -import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod"; -import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod"; -import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod"; -import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod"; -import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; -import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; -import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; -import { appDataSchema as twipla_zod_ts } from "./twipla/zod"; -import { appDataSchema as umami_zod_ts } from "./umami/zod"; -import { appDataSchema as vital_zod_ts } from "./vital/zod"; -import { appDataSchema as webex_zod_ts } from "./webex/zod"; -import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod"; -import { appDataSchema as zapier_zod_ts } from "./zapier/zod"; -import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; -import { appDataSchema as zohocalendar_zod_ts } from "./zohocalendar/zod"; -import { appDataSchema as zohocrm_zod_ts } from "./zohocrm/zod"; -import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; +import { appDataSchema as alby_zod_ts } from "./alby/zod" +import { appDataSchema as basecamp3_zod_ts } from "./basecamp3/zod" +import { appDataSchema as btcpayserver_zod_ts } from "./btcpayserver/zod" +import { appDataSchema as closecom_zod_ts } from "./closecom/zod" +import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod" +import { appDataSchema as databuddy_zod_ts } from "./databuddy/zod" +import { appDataSchema as dub_zod_ts } from "./dub/zod" +import { appDataSchema as fathom_zod_ts } from "./fathom/zod" +import { appDataSchema as feishucalendar_zod_ts } from "./feishucalendar/zod" +import { appDataSchema as ga4_zod_ts } from "./ga4/zod" +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" +import { appDataSchema as jelly_zod_ts } from "./jelly/zod" +import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod" +import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod" +import { appDataSchema as lyra_zod_ts } from "./lyra/zod" +import { appDataSchema as make_zod_ts } from "./make/zod" +import { appDataSchema as matomo_zod_ts } from "./matomo/zod" +import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod" +import { appDataSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod" +import { appDataSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod" +import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod" +import { appDataSchema as office365video_zod_ts } from "./office365video/zod" +import { appDataSchema as paypal_zod_ts } from "./paypal/zod" +import { appDataSchema as paystack_zod_ts } from "./paystack/zod" +import { appDataSchema as pipedrive_crm_zod_ts } from "./pipedrive-crm/zod" +import { appDataSchema as plausible_zod_ts } from "./plausible/zod" +import { appDataSchema as posthog_zod_ts } from "./posthog/zod" +import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod" +import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod" +import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod" +import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod" +import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod" +import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod" +import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod" +import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod" +import { appDataSchema as twipla_zod_ts } from "./twipla/zod" +import { appDataSchema as umami_zod_ts } from "./umami/zod" +import { appDataSchema as vital_zod_ts } from "./vital/zod" +import { appDataSchema as webex_zod_ts } from "./webex/zod" +import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod" +import { appDataSchema as zapier_zod_ts } from "./zapier/zod" +import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod" +import { appDataSchema as zohocalendar_zod_ts } from "./zohocalendar/zod" +import { appDataSchema as zohocrm_zod_ts } from "./zohocrm/zod" +import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod" export const appDataSchemas = { - alby: alby_zod_ts, - basecamp3: basecamp3_zod_ts, - btcpayserver: btcpayserver_zod_ts, - closecom: closecom_zod_ts, - dailyvideo: dailyvideo_zod_ts, - databuddy: databuddy_zod_ts, - dub: dub_zod_ts, - fathom: fathom_zod_ts, - feishucalendar: feishucalendar_zod_ts, - ga4: ga4_zod_ts, - giphy: giphy_zod_ts, - 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, - jelly: jelly_zod_ts, - jitsivideo: jitsivideo_zod_ts, - larkcalendar: larkcalendar_zod_ts, - lyra: lyra_zod_ts, - make: make_zod_ts, - matomo: matomo_zod_ts, - metapixel: metapixel_zod_ts, - "mock-payment-app": mock_payment_app_zod_ts, - nextcloudtalk: nextcloudtalk_zod_ts, - office365calendar: office365calendar_zod_ts, - office365video: office365video_zod_ts, - paypal: paypal_zod_ts, - "pipedrive-crm": pipedrive_crm_zod_ts, - plausible: plausible_zod_ts, - posthog: posthog_zod_ts, - qr_code: qr_code_zod_ts, - "routing-forms": routing_forms_zod_ts, - salesforce: salesforce_zod_ts, - shimmervideo: shimmervideo_zod_ts, - stripe: stripepayment_zod_ts, - tandemvideo: tandemvideo_zod_ts, - "booking-pages-tag": booking_pages_tag_zod_ts, - "event-type-app-card": event_type_app_card_zod_ts, - twipla: twipla_zod_ts, - umami: umami_zod_ts, - vital: vital_zod_ts, - webex: webex_zod_ts, - wordpress: wordpress_zod_ts, - zapier: zapier_zod_ts, - "zoho-bigin": zoho_bigin_zod_ts, - zohocalendar: zohocalendar_zod_ts, - zohocrm: zohocrm_zod_ts, - zoomvideo: zoomvideo_zod_ts, -}; +"alby": alby_zod_ts, +"basecamp3": basecamp3_zod_ts, +"btcpayserver": btcpayserver_zod_ts, +"closecom": closecom_zod_ts, +"dailyvideo": dailyvideo_zod_ts, +"databuddy": databuddy_zod_ts, +"dub": dub_zod_ts, +"fathom": fathom_zod_ts, +"feishucalendar": feishucalendar_zod_ts, +"ga4": ga4_zod_ts, +"giphy": giphy_zod_ts, +"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, +"jelly": jelly_zod_ts, +"jitsivideo": jitsivideo_zod_ts, +"larkcalendar": larkcalendar_zod_ts, +"lyra": lyra_zod_ts, +"make": make_zod_ts, +"matomo": matomo_zod_ts, +"metapixel": metapixel_zod_ts, +"mock-payment-app": mock_payment_app_zod_ts, +"nextcloudtalk": nextcloudtalk_zod_ts, +"office365calendar": office365calendar_zod_ts, +"office365video": office365video_zod_ts, +"paypal": paypal_zod_ts, +"paystack": paystack_zod_ts, +"pipedrive-crm": pipedrive_crm_zod_ts, +"plausible": plausible_zod_ts, +"posthog": posthog_zod_ts, +"qr_code": qr_code_zod_ts, +"routing-forms": routing_forms_zod_ts, +"salesforce": salesforce_zod_ts, +"shimmervideo": shimmervideo_zod_ts, +"stripe": stripepayment_zod_ts, +"tandemvideo": tandemvideo_zod_ts, +"booking-pages-tag": booking_pages_tag_zod_ts, +"event-type-app-card": event_type_app_card_zod_ts, +"twipla": twipla_zod_ts, +"umami": umami_zod_ts, +"vital": vital_zod_ts, +"webex": webex_zod_ts, +"wordpress": wordpress_zod_ts, +"zapier": zapier_zod_ts, +"zoho-bigin": zoho_bigin_zod_ts, +"zohocalendar": zohocalendar_zod_ts, +"zohocrm": zohocrm_zod_ts, +"zoomvideo": zoomvideo_zod_ts, +}; \ No newline at end of file diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 650bc722dbf6cf..55c69298bb197e 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -3,89 +3,90 @@ Don't modify this file manually. **/ export const apiHandlers = { - alby: import("./alby/api"), - applecalendar: import("./applecalendar/api"), - attio: import("./attio/api"), - basecamp3: import("./basecamp3/api"), - btcpayserver: import("./btcpayserver/api"), - caldavcalendar: import("./caldavcalendar/api"), - campfire: import("./campfire/api"), - closecom: import("./closecom/api"), - databuddy: import("./databuddy/api"), - demodesk: import("./demodesk/api"), - dialpad: import("./dialpad/api"), - discord: import("./discord/api"), - dub: import("./dub/api"), - eightxeight: import("./eightxeight/api"), - "element-call": import("./element-call/api"), - exchange2013calendar: import("./exchange2013calendar/api"), - exchange2016calendar: import("./exchange2016calendar/api"), - exchangecalendar: import("./exchangecalendar/api"), - facetime: import("./facetime/api"), - famulor: import("./famulor/api"), - fathom: import("./fathom/api"), - feishucalendar: import("./feishucalendar/api"), - ga4: import("./ga4/api"), - giphy: import("./giphy/api"), - googlecalendar: import("./googlecalendar/api"), - googlevideo: import("./googlevideo/api"), - gtm: import("./gtm/api"), - hitpay: import("./hitpay/api"), - "horizon-workrooms": import("./horizon-workrooms/api"), - hubspot: import("./hubspot/api"), - huddle01video: import("./huddle01video/api"), - "ics-feedcalendar": import("./ics-feedcalendar/api"), - insihts: import("./insihts/api"), - intercom: import("./intercom/api"), - jelly: import("./jelly/api"), - jitsivideo: import("./jitsivideo/api"), - larkcalendar: import("./larkcalendar/api"), - linear: import("./linear/api"), - lyra: import("./lyra/api"), - make: import("./make/api"), - matomo: import("./matomo/api"), - metapixel: import("./metapixel/api"), - mirotalk: import("./mirotalk/api"), - "mock-payment-app": import("./mock-payment-app/api"), - nextcloudtalk: import("./nextcloudtalk/api"), - office365calendar: import("./office365calendar/api"), - office365video: import("./office365video/api"), - paypal: import("./paypal/api"), - ping: import("./ping/api"), - "pipedrive-crm": import("./pipedrive-crm/api"), - plausible: import("./plausible/api"), - posthog: import("./posthog/api"), - qr_code: import("./qr_code/api"), - riverside: import("./riverside/api"), - roam: import("./roam/api"), - "routing-forms": import("./routing-forms/api"), - salesforce: import("./salesforce/api"), - salesroom: import("./salesroom/api"), - sendgrid: import("./sendgrid/api"), - shimmervideo: import("./shimmervideo/api"), - signal: import("./signal/api"), - sirius_video: import("./sirius_video/api"), - skype: import("./skype/api"), - stripepayment: import("./stripepayment/api"), - sylapsvideo: import("./sylapsvideo/api"), - tandemvideo: import("./tandemvideo/api"), - telegram: import("./telegram/api"), - basic: import("./templates/basic/api"), - "booking-pages-tag": import("./templates/booking-pages-tag/api"), - "event-type-app-card": import("./templates/event-type-app-card/api"), - "event-type-location-video-static": import("./templates/event-type-location-video-static/api"), - "general-app-settings": import("./templates/general-app-settings/api"), - twipla: import("./twipla/api"), - umami: import("./umami/api"), - vital: import("./vital/api"), - weather_in_your_calendar: import("./weather_in_your_calendar/api"), - webex: import("./webex/api"), - whatsapp: import("./whatsapp/api"), - whereby: import("./whereby/api"), - wipemycalother: import("./wipemycalother/api"), - zapier: import("./zapier/api"), - "zoho-bigin": import("./zoho-bigin/api"), - zohocalendar: import("./zohocalendar/api"), - zohocrm: import("./zohocrm/api"), - zoomvideo: import("./zoomvideo/api"), -}; +"alby": import("./alby/api"), +"applecalendar": import("./applecalendar/api"), +"attio": import("./attio/api"), +"basecamp3": import("./basecamp3/api"), +"btcpayserver": import("./btcpayserver/api"), +"caldavcalendar": import("./caldavcalendar/api"), +"campfire": import("./campfire/api"), +"closecom": import("./closecom/api"), +"databuddy": import("./databuddy/api"), +"demodesk": import("./demodesk/api"), +"dialpad": import("./dialpad/api"), +"discord": import("./discord/api"), +"dub": import("./dub/api"), +"eightxeight": import("./eightxeight/api"), +"element-call": import("./element-call/api"), +"exchange2013calendar": import("./exchange2013calendar/api"), +"exchange2016calendar": import("./exchange2016calendar/api"), +"exchangecalendar": import("./exchangecalendar/api"), +"facetime": import("./facetime/api"), +"famulor": import("./famulor/api"), +"fathom": import("./fathom/api"), +"feishucalendar": import("./feishucalendar/api"), +"ga4": import("./ga4/api"), +"giphy": import("./giphy/api"), +"googlecalendar": import("./googlecalendar/api"), +"googlevideo": import("./googlevideo/api"), +"gtm": import("./gtm/api"), +"hitpay": import("./hitpay/api"), +"horizon-workrooms": import("./horizon-workrooms/api"), +"hubspot": import("./hubspot/api"), +"huddle01video": import("./huddle01video/api"), +"ics-feedcalendar": import("./ics-feedcalendar/api"), +"insihts": import("./insihts/api"), +"intercom": import("./intercom/api"), +"jelly": import("./jelly/api"), +"jitsivideo": import("./jitsivideo/api"), +"larkcalendar": import("./larkcalendar/api"), +"linear": import("./linear/api"), +"lyra": import("./lyra/api"), +"make": import("./make/api"), +"matomo": import("./matomo/api"), +"metapixel": import("./metapixel/api"), +"mirotalk": import("./mirotalk/api"), +"mock-payment-app": import("./mock-payment-app/api"), +"nextcloudtalk": import("./nextcloudtalk/api"), +"office365calendar": import("./office365calendar/api"), +"office365video": import("./office365video/api"), +"paypal": import("./paypal/api"), +"paystack": import("./paystack/api"), +"ping": import("./ping/api"), +"pipedrive-crm": import("./pipedrive-crm/api"), +"plausible": import("./plausible/api"), +"posthog": import("./posthog/api"), +"qr_code": import("./qr_code/api"), +"riverside": import("./riverside/api"), +"roam": import("./roam/api"), +"routing-forms": import("./routing-forms/api"), +"salesforce": import("./salesforce/api"), +"salesroom": import("./salesroom/api"), +"sendgrid": import("./sendgrid/api"), +"shimmervideo": import("./shimmervideo/api"), +"signal": import("./signal/api"), +"sirius_video": import("./sirius_video/api"), +"skype": import("./skype/api"), +"stripepayment": import("./stripepayment/api"), +"sylapsvideo": import("./sylapsvideo/api"), +"tandemvideo": import("./tandemvideo/api"), +"telegram": import("./telegram/api"), +"basic": import("./templates/basic/api"), +"booking-pages-tag": import("./templates/booking-pages-tag/api"), +"event-type-app-card": import("./templates/event-type-app-card/api"), +"event-type-location-video-static": import("./templates/event-type-location-video-static/api"), +"general-app-settings": import("./templates/general-app-settings/api"), +"twipla": import("./twipla/api"), +"umami": import("./umami/api"), +"vital": import("./vital/api"), +"weather_in_your_calendar": import("./weather_in_your_calendar/api"), +"webex": import("./webex/api"), +"whatsapp": import("./whatsapp/api"), +"whereby": import("./whereby/api"), +"wipemycalother": import("./wipemycalother/api"), +"zapier": import("./zapier/api"), +"zoho-bigin": import("./zoho-bigin/api"), +"zohocalendar": import("./zohocalendar/api"), +"zohocrm": import("./zohocrm/api"), +"zoomvideo": import("./zoomvideo/api"), +}; \ No newline at end of file diff --git a/packages/app-store/bookerApps.metadata.generated.ts b/packages/app-store/bookerApps.metadata.generated.ts index 16d9e3f77e50d2..2abb260052f19f 100644 --- a/packages/app-store/bookerApps.metadata.generated.ts +++ b/packages/app-store/bookerApps.metadata.generated.ts @@ -2,95 +2,95 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -import campfire_config_json from "./campfire/config.json"; -import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; -import databuddy_config_json from "./databuddy/config.json"; -import demodesk_config_json from "./demodesk/config.json"; -import dialpad_config_json from "./dialpad/config.json"; -import discord_config_json from "./discord/config.json"; -import eightxeight_config_json from "./eightxeight/config.json"; -import element_call_config_json from "./element-call/config.json"; -import facetime_config_json from "./facetime/config.json"; -import fathom_config_json from "./fathom/config.json"; -import ga4_config_json from "./ga4/config.json"; -import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; -import gtm_config_json from "./gtm/config.json"; -import horizon_workrooms_config_json from "./horizon-workrooms/config.json"; -import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; -import insihts_config_json from "./insihts/config.json"; -import jelly_config_json from "./jelly/config.json"; -import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; -import lyra_config_json from "./lyra/config.json"; -import matomo_config_json from "./matomo/config.json"; -import metapixel_config_json from "./metapixel/config.json"; -import mirotalk_config_json from "./mirotalk/config.json"; -import nextcloudtalk_config_json from "./nextcloudtalk/config.json"; -import office365video_config_json from "./office365video/config.json"; -import ping_config_json from "./ping/config.json"; -import plausible_config_json from "./plausible/config.json"; -import posthog_config_json from "./posthog/config.json"; -import riverside_config_json from "./riverside/config.json"; -import roam_config_json from "./roam/config.json"; -import salesroom_config_json from "./salesroom/config.json"; -import shimmervideo_config_json from "./shimmervideo/config.json"; -import signal_config_json from "./signal/config.json"; -import sirius_video_config_json from "./sirius_video/config.json"; -import skype_config_json from "./skype/config.json"; -import sylapsvideo_config_json from "./sylapsvideo/config.json"; -import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata"; -import telegram_config_json from "./telegram/config.json"; -import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json"; -import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json"; -import twipla_config_json from "./twipla/config.json"; -import umami_config_json from "./umami/config.json"; -import webex_config_json from "./webex/config.json"; -import whatsapp_config_json from "./whatsapp/config.json"; -import whereby_config_json from "./whereby/config.json"; -import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; +import campfire_config_json from "./campfire/config.json" +import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata" +import databuddy_config_json from "./databuddy/config.json" +import demodesk_config_json from "./demodesk/config.json" +import dialpad_config_json from "./dialpad/config.json" +import discord_config_json from "./discord/config.json" +import eightxeight_config_json from "./eightxeight/config.json" +import element_call_config_json from "./element-call/config.json" +import facetime_config_json from "./facetime/config.json" +import fathom_config_json from "./fathom/config.json" +import ga4_config_json from "./ga4/config.json" +import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata" +import gtm_config_json from "./gtm/config.json" +import horizon_workrooms_config_json from "./horizon-workrooms/config.json" +import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata" +import insihts_config_json from "./insihts/config.json" +import jelly_config_json from "./jelly/config.json" +import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata" +import lyra_config_json from "./lyra/config.json" +import matomo_config_json from "./matomo/config.json" +import metapixel_config_json from "./metapixel/config.json" +import mirotalk_config_json from "./mirotalk/config.json" +import nextcloudtalk_config_json from "./nextcloudtalk/config.json" +import office365video_config_json from "./office365video/config.json" +import ping_config_json from "./ping/config.json" +import plausible_config_json from "./plausible/config.json" +import posthog_config_json from "./posthog/config.json" +import riverside_config_json from "./riverside/config.json" +import roam_config_json from "./roam/config.json" +import salesroom_config_json from "./salesroom/config.json" +import shimmervideo_config_json from "./shimmervideo/config.json" +import signal_config_json from "./signal/config.json" +import sirius_video_config_json from "./sirius_video/config.json" +import skype_config_json from "./skype/config.json" +import sylapsvideo_config_json from "./sylapsvideo/config.json" +import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata" +import telegram_config_json from "./telegram/config.json" +import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json" +import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json" +import twipla_config_json from "./twipla/config.json" +import umami_config_json from "./umami/config.json" +import webex_config_json from "./webex/config.json" +import whatsapp_config_json from "./whatsapp/config.json" +import whereby_config_json from "./whereby/config.json" +import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata" export const appStoreMetadata = { - campfire: campfire_config_json, - dailyvideo: dailyvideo__metadata_ts, - databuddy: databuddy_config_json, - demodesk: demodesk_config_json, - dialpad: dialpad_config_json, - discord: discord_config_json, - eightxeight: eightxeight_config_json, - "element-call": element_call_config_json, - facetime: facetime_config_json, - fathom: fathom_config_json, - ga4: ga4_config_json, - googlevideo: googlevideo__metadata_ts, - gtm: gtm_config_json, - "horizon-workrooms": horizon_workrooms_config_json, - huddle01video: huddle01video__metadata_ts, - insihts: insihts_config_json, - jelly: jelly_config_json, - jitsivideo: jitsivideo__metadata_ts, - lyra: lyra_config_json, - matomo: matomo_config_json, - metapixel: metapixel_config_json, - mirotalk: mirotalk_config_json, - nextcloudtalk: nextcloudtalk_config_json, - office365video: office365video_config_json, - ping: ping_config_json, - plausible: plausible_config_json, - posthog: posthog_config_json, - riverside: riverside_config_json, - roam: roam_config_json, - salesroom: salesroom_config_json, - shimmervideo: shimmervideo_config_json, - signal: signal_config_json, - sirius_video: sirius_video_config_json, - skype: skype_config_json, - sylapsvideo: sylapsvideo_config_json, - tandemvideo: tandemvideo__metadata_ts, - telegram: telegram_config_json, - "booking-pages-tag": booking_pages_tag_config_json, - "event-type-location-video-static": event_type_location_video_static_config_json, - twipla: twipla_config_json, - umami: umami_config_json, - webex: webex_config_json, - whatsapp: whatsapp_config_json, - whereby: whereby_config_json, - zoomvideo: zoomvideo__metadata_ts, -}; +"campfire": campfire_config_json, +"dailyvideo": dailyvideo__metadata_ts, +"databuddy": databuddy_config_json, +"demodesk": demodesk_config_json, +"dialpad": dialpad_config_json, +"discord": discord_config_json, +"eightxeight": eightxeight_config_json, +"element-call": element_call_config_json, +"facetime": facetime_config_json, +"fathom": fathom_config_json, +"ga4": ga4_config_json, +"googlevideo": googlevideo__metadata_ts, +"gtm": gtm_config_json, +"horizon-workrooms": horizon_workrooms_config_json, +"huddle01video": huddle01video__metadata_ts, +"insihts": insihts_config_json, +"jelly": jelly_config_json, +"jitsivideo": jitsivideo__metadata_ts, +"lyra": lyra_config_json, +"matomo": matomo_config_json, +"metapixel": metapixel_config_json, +"mirotalk": mirotalk_config_json, +"nextcloudtalk": nextcloudtalk_config_json, +"office365video": office365video_config_json, +"ping": ping_config_json, +"plausible": plausible_config_json, +"posthog": posthog_config_json, +"riverside": riverside_config_json, +"roam": roam_config_json, +"salesroom": salesroom_config_json, +"shimmervideo": shimmervideo_config_json, +"signal": signal_config_json, +"sirius_video": sirius_video_config_json, +"skype": skype_config_json, +"sylapsvideo": sylapsvideo_config_json, +"tandemvideo": tandemvideo__metadata_ts, +"telegram": telegram_config_json, +"booking-pages-tag": booking_pages_tag_config_json, +"event-type-location-video-static": event_type_location_video_static_config_json, +"twipla": twipla_config_json, +"umami": umami_config_json, +"webex": webex_config_json, +"whatsapp": whatsapp_config_json, +"whereby": whereby_config_json, +"zoomvideo": zoomvideo__metadata_ts, +}; \ No newline at end of file diff --git a/packages/app-store/calendar.services.generated.ts b/packages/app-store/calendar.services.generated.ts index 9795cfe01ea444..4f2bb7de323ea7 100644 --- a/packages/app-store/calendar.services.generated.ts +++ b/packages/app-store/calendar.services.generated.ts @@ -2,19 +2,16 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -export const CalendarServiceMap = - process.env.NEXT_PUBLIC_IS_E2E === "1" - ? {} - : { - applecalendar: import("./applecalendar/lib/CalendarService"), - caldavcalendar: import("./caldavcalendar/lib/CalendarService"), - exchange2013calendar: import("./exchange2013calendar/lib/CalendarService"), - exchange2016calendar: import("./exchange2016calendar/lib/CalendarService"), - exchangecalendar: import("./exchangecalendar/lib/CalendarService"), - feishucalendar: import("./feishucalendar/lib/CalendarService"), - googlecalendar: import("./googlecalendar/lib/CalendarService"), - "ics-feedcalendar": import("./ics-feedcalendar/lib/CalendarService"), - larkcalendar: import("./larkcalendar/lib/CalendarService"), - office365calendar: import("./office365calendar/lib/CalendarService"), - zohocalendar: import("./zohocalendar/lib/CalendarService"), - }; +export const CalendarServiceMap = process.env.NEXT_PUBLIC_IS_E2E === '1' ? {} : { +"applecalendar": import("./applecalendar/lib/CalendarService"), +"caldavcalendar": import("./caldavcalendar/lib/CalendarService"), +"exchange2013calendar": import("./exchange2013calendar/lib/CalendarService"), +"exchange2016calendar": import("./exchange2016calendar/lib/CalendarService"), +"exchangecalendar": import("./exchangecalendar/lib/CalendarService"), +"feishucalendar": import("./feishucalendar/lib/CalendarService"), +"googlecalendar": import("./googlecalendar/lib/CalendarService"), +"ics-feedcalendar": import("./ics-feedcalendar/lib/CalendarService"), +"larkcalendar": import("./larkcalendar/lib/CalendarService"), +"office365calendar": import("./office365calendar/lib/CalendarService"), +"zohocalendar": import("./zohocalendar/lib/CalendarService"), +}; \ No newline at end of file diff --git a/packages/app-store/crm.apps.generated.ts b/packages/app-store/crm.apps.generated.ts index 94392d5ee50fcd..31e47670b83af2 100644 --- a/packages/app-store/crm.apps.generated.ts +++ b/packages/app-store/crm.apps.generated.ts @@ -3,10 +3,10 @@ Don't modify this file manually. **/ export const CrmServiceMap = { - closecom: import("./closecom/lib/CrmService"), - hubspot: import("./hubspot/lib/CrmService"), - "pipedrive-crm": import("./pipedrive-crm/lib/CrmService"), - salesforce: import("./salesforce/lib/CrmService"), - "zoho-bigin": import("./zoho-bigin/lib/CrmService"), - zohocrm: import("./zohocrm/lib/CrmService"), -}; +"closecom": import("./closecom/lib/CrmService"), +"hubspot": import("./hubspot/lib/CrmService"), +"pipedrive-crm": import("./pipedrive-crm/lib/CrmService"), +"salesforce": import("./salesforce/lib/CrmService"), +"zoho-bigin": import("./zoho-bigin/lib/CrmService"), +"zohocrm": import("./zohocrm/lib/CrmService"), +}; \ No newline at end of file diff --git a/packages/app-store/payment.services.generated.ts b/packages/app-store/payment.services.generated.ts index 2e42d632580810..aeaf08bbe5703d 100644 --- a/packages/app-store/payment.services.generated.ts +++ b/packages/app-store/payment.services.generated.ts @@ -3,10 +3,11 @@ Don't modify this file manually. **/ export const PaymentServiceMap = { - alby: import("./alby/lib/PaymentService"), - btcpayserver: import("./btcpayserver/lib/PaymentService"), - hitpay: import("./hitpay/lib/PaymentService"), - "mock-payment-app": import("./mock-payment-app/lib/PaymentService"), - paypal: import("./paypal/lib/PaymentService"), - stripepayment: import("./stripepayment/lib/PaymentService"), -}; +"alby": import("./alby/lib/PaymentService"), +"btcpayserver": import("./btcpayserver/lib/PaymentService"), +"hitpay": import("./hitpay/lib/PaymentService"), +"mock-payment-app": import("./mock-payment-app/lib/PaymentService"), +"paypal": import("./paypal/lib/PaymentService"), +"paystack": import("./paystack/lib/PaymentService"), +"stripepayment": import("./stripepayment/lib/PaymentService"), +}; \ No newline at end of file diff --git a/packages/app-store/paystack/_metadata.ts b/packages/app-store/paystack/_metadata.ts new file mode 100644 index 00000000000000..d783e59bfb1467 --- /dev/null +++ b/packages/app-store/paystack/_metadata.ts @@ -0,0 +1,22 @@ +import type { AppMeta } from "@calcom/types/App"; + +import _package from "./package.json"; + +export const metadata = { + name: "Paystack", + description: _package.description, + installed: true, + type: "paystack_payment", + variant: "payment", + logo: "icon.svg", + publisher: "Cal.com", + url: "https://paystack.com", + categories: ["payment"], + slug: "paystack", + title: "Paystack", + email: "support@cal.com", + dirName: "paystack", + isOAuth: true, +} as AppMeta; + +export default metadata; diff --git a/packages/app-store/paystack/api/add.ts b/packages/app-store/paystack/api/add.ts new file mode 100644 index 00000000000000..1c3543a30c8073 --- /dev/null +++ b/packages/app-store/paystack/api/add.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam"; +import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; +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 { teamId } = req.query; + const teamIdNumber = teamId ? Number(teamId) : null; + + await throwIfNotHaveAdminAccessToTeam({ teamId: teamIdNumber, userId: req.session.user.id }); + const installForObject = teamIdNumber ? { teamId: teamIdNumber } : { userId: req.session.user.id }; + + const appType = config.type; + try { + const alreadyInstalled = await prisma.credential.findFirst({ + where: { + type: appType, + ...installForObject, + }, + }); + if (alreadyInstalled) { + throw new Error("Already installed"); + } + await prisma.credential.create({ + data: { + type: appType, + key: {}, + appId: "paystack", + ...(teamIdNumber ? { teamId: teamIdNumber } : { userId: req.session.user.id }), + }, + }); + } catch (error: unknown) { + const httpError = getServerErrorFromUnknown(error); + return res.status(httpError.statusCode).json({ message: httpError.message }); + } + + return res.status(200).json({ url: `/apps/paystack/setup${teamIdNumber ? `?teamId=${teamIdNumber}` : ""}` }); +} diff --git a/packages/app-store/paystack/api/index.ts b/packages/app-store/paystack/api/index.ts new file mode 100644 index 00000000000000..214855db1a31c9 --- /dev/null +++ b/packages/app-store/paystack/api/index.ts @@ -0,0 +1,3 @@ +export { default as add } from "./add"; +export { default as webhook } from "./webhook"; +export { default as verify } from "./verify"; diff --git a/packages/app-store/paystack/api/verify.ts b/packages/app-store/paystack/api/verify.ts new file mode 100644 index 00000000000000..96eae892ef57c3 --- /dev/null +++ b/packages/app-store/paystack/api/verify.ts @@ -0,0 +1,109 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess"; +import { IS_PRODUCTION } from "@calcom/lib/constants"; +import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import prisma from "@calcom/prisma"; + +import { appKeysSchema } from "../zod"; +import { PaystackClient } from "../lib/PaystackClient"; + +const log = logger.getSubLogger({ prefix: ["[paystackVerify]"] }); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (req.method !== "GET") { + throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); + } + + const reference = req.query.reference as string; + if (!reference) { + throw new HttpCode({ statusCode: 400, message: "Missing reference parameter" }); + } + + const payment = await prisma.payment.findFirst({ + where: { externalId: reference }, + select: { + id: true, + bookingId: true, + success: true, + booking: { + select: { + eventType: { + select: { metadata: true }, + }, + userId: true, + }, + }, + }, + }); + + if (!payment?.bookingId) { + throw new HttpCode({ statusCode: 404, message: "Payment not found" }); + } + + // Already processed + if (payment.success) { + res.status(200).json({ status: "success", message: "Payment already confirmed" }); + return; + } + + // Find credential + const metadata = payment.booking?.eventType?.metadata as Record | null; + const paystackAppData = (metadata?.apps as Record | undefined)?.paystack as + | { credentialId?: number } + | undefined; + + const credentialQuery = paystackAppData?.credentialId + ? { id: paystackAppData.credentialId } + : { userId: payment.booking?.userId, appId: "paystack" as const }; + + const credential = await prisma.credential.findFirst({ + where: credentialQuery, + select: { key: true }, + }); + + if (!credential) { + throw new HttpCode({ statusCode: 500, message: "Missing payment credentials" }); + } + + const parsedKeys = appKeysSchema.safeParse(credential.key); + if (!parsedKeys.success) { + throw new HttpCode({ statusCode: 500, message: "Malformed credentials" }); + } + + // Verify with Paystack + const client = new PaystackClient(parsedKeys.data.secret_key); + const verification = await client.verifyTransaction(reference); + + if (verification.status !== "success") { + res.status(200).json({ status: verification.status, message: "Payment not yet successful" }); + return; + } + + // Confirm booking + const traceContext = distributedTracing.createTrace("paystack_verify", { + meta: { reference, bookingId: payment.bookingId }, + }); + + await handlePaymentSuccess({ + paymentId: payment.id, + bookingId: payment.bookingId, + appSlug: "paystack", + traceContext, + }); + + res.status(200).json({ status: "success", message: "Payment confirmed" }); + } catch (_err) { + const err = getServerErrorFromUnknown(_err); + log.error(`Verify Error: ${err.message}`, safeStringify(err)); + res.status(err.statusCode).json({ + message: err.message, + stack: IS_PRODUCTION ? undefined : err.cause?.stack, + }); + } +} diff --git a/packages/app-store/paystack/api/webhook.ts b/packages/app-store/paystack/api/webhook.ts new file mode 100644 index 00000000000000..085e1899641793 --- /dev/null +++ b/packages/app-store/paystack/api/webhook.ts @@ -0,0 +1,148 @@ +import { buffer } from "micro"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess"; +import { IS_PRODUCTION } from "@calcom/lib/constants"; +import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import prisma from "@calcom/prisma"; + +import { appKeysSchema } from "../zod"; +import { PaystackClient } from "../lib/PaystackClient"; +import { verifyWebhookSignature } from "../lib/verifyWebhookSignature"; + +const log = logger.getSubLogger({ prefix: ["[paystackWebhook]"] }); + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (req.method !== "POST") { + throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); + } + + const requestBuffer = await buffer(req); + const bodyString = requestBuffer.toString(); + + // Parse body to get the reference (needed to find the credential for signature verification) + let parsedBody: { event: string; data: { reference: string } }; + try { + parsedBody = JSON.parse(bodyString); + } catch { + throw new HttpCode({ statusCode: 400, message: "Invalid JSON body" }); + } + + if (!parsedBody?.data?.reference) { + throw new HttpCode({ statusCode: 400, message: "Missing reference in payload" }); + } + + const reference = parsedBody.data.reference; + + // Look up payment by reference to find the credential + const payment = await prisma.payment.findFirst({ + where: { externalId: reference }, + select: { + id: true, + bookingId: true, + success: true, + booking: { + select: { + eventType: { + select: { + metadata: true, + }, + }, + userId: true, + }, + }, + }, + }); + + if (!payment?.bookingId) { + log.error("Payment not found for reference", { reference }); + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + } + + // Find the credential to verify the signature + const metadata = payment.booking?.eventType?.metadata as Record | null; + const paystackAppData = (metadata?.apps as Record | undefined)?.paystack as + | { credentialId?: number } + | undefined; + + const credentialQuery = paystackAppData?.credentialId + ? { id: paystackAppData.credentialId } + : { userId: payment.booking?.userId, appId: "paystack" as const }; + + const credential = await prisma.credential.findFirst({ + where: credentialQuery, + select: { key: true }, + }); + + if (!credential) { + log.error("Paystack credentials not found"); + throw new HttpCode({ statusCode: 500, message: "Missing payment credentials" }); + } + + const parsedKeys = appKeysSchema.safeParse(credential.key); + if (!parsedKeys.success) { + throw new HttpCode({ statusCode: 500, message: "Malformed credentials" }); + } + + // Verify webhook signature + const signature = req.headers["x-paystack-signature"] as string | undefined; + if (!signature || !verifyWebhookSignature(bodyString, signature, parsedKeys.data.secret_key)) { + log.error("Invalid Paystack webhook signature"); + throw new HttpCode({ statusCode: 401, message: "Invalid signature" }); + } + + // Only handle charge.success events + if (parsedBody.event !== "charge.success") { + res.status(200).json({ message: `Unhandled event type: ${parsedBody.event}` }); + return; + } + + // Idempotency: if already successful, skip + if (payment.success) { + res.status(200).json({ message: "Payment already processed" }); + return; + } + + // Re-verify with Paystack API (belt and suspenders) + const client = new PaystackClient(parsedKeys.data.secret_key); + const verification = await client.verifyTransaction(reference); + + if (verification.status !== "success") { + log.error("Paystack verification failed", { reference, status: verification.status }); + throw new HttpCode({ statusCode: 400, message: "Payment verification failed" }); + } + + // Confirm the booking + const traceContext = distributedTracing.createTrace("paystack_webhook", { + meta: { reference, bookingId: payment.bookingId }, + }); + + await handlePaymentSuccess({ + paymentId: payment.id, + bookingId: payment.bookingId, + appSlug: "paystack", + traceContext, + }); + } catch (_err) { + const err = getServerErrorFromUnknown(_err); + log.error(`Webhook Error: ${err.message}`, safeStringify(err)); + res.status(err.statusCode).send({ + message: err.message, + stack: IS_PRODUCTION ? undefined : err.cause?.stack, + }); + return; + } + + res.status(200).json({ received: true }); +} diff --git a/packages/app-store/paystack/components/EventTypeAppCardInterface.tsx b/packages/app-store/paystack/components/EventTypeAppCardInterface.tsx new file mode 100644 index 00000000000000..1aa66f6c2cd8c5 --- /dev/null +++ b/packages/app-store/paystack/components/EventTypeAppCardInterface.tsx @@ -0,0 +1,59 @@ +import { usePathname, useSearchParams } from "next/navigation"; +import { useState, useMemo } from "react"; + +import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; +import AppCard from "@calcom/app-store/_components/AppCard"; +import 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 type { appDataSchema } from "../zod"; +import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface"; + +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ + app, + eventType, + eventTypeFormMetadata, + onAppInstallSuccess, +}) { + const searchParams = useSearchParams(); + const pathname = usePathname(); + const asPath = useMemo( + () => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`, + [pathname, searchParams] + ); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); + const [requirePayment, setRequirePayment] = useState(getAppData("enabled")); + const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata); + const { t } = useLocale(); + + const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled; + + return ( + { + setRequirePayment(enabled); + setAppData("enabled", enabled); + }} + description={<>{t("paystack_app_description")}} + disableSwitch={shouldDisableSwitch} + switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}> + <> + + + + ); +}; + +export default EventTypeAppCard; diff --git a/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx new file mode 100644 index 00000000000000..500201e68bc5ee --- /dev/null +++ b/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx @@ -0,0 +1,167 @@ +import * as RadioGroup from "@radix-ui/react-radio-group"; +import { useState, useEffect } from "react"; + +import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types"; +import { + convertToSmallestCurrencyUnit, + convertFromSmallestToPresentableCurrencyUnit, +} from "@calcom/lib/currencyConversions"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { RefundPolicy } from "@calcom/lib/payment/types"; +import classNames from "@calcom/ui/classNames"; +import { Alert } from "@calcom/ui/components/alert"; +import { Select } from "@calcom/ui/components/form"; +import { TextField } from "@calcom/ui/components/form"; +import { RadioField } from "@calcom/ui/components/radio"; + +import { currencyOptions, currencySymbols, isAcceptedCurrencyCode } from "../lib/currencyOptions"; + +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 requirePayment = getAppData("enabled"); + + const { t } = useLocale(); + const recurringEventDefined = eventType.recurringEvent?.count !== undefined; + + const getCurrencySymbol = (curr: string) => + isAcceptedCurrencyCode(curr) ? currencySymbols[curr] : ""; + + useEffect(() => { + if (requirePayment) { + if (!getAppData("currency")) { + setAppData("currency", currencyOptions[0].value); + } + if (!getAppData("paymentOption")) { + setAppData("paymentOption", "ON_BOOKING"); + } + } + if (!getAppData("refundPolicy")) { + setAppData("refundPolicy", RefundPolicy.NEVER); + } + }, [requirePayment, getAppData, setAppData]); + + const dayTypeOptions = [ + { value: 0, label: t("business_days") }, + { value: 1, label: t("calendar_days") }, + ]; + + const getSelectedDayType = () => + dayTypeOptions.find((opt) => opt.value === (getAppData("refundCountCalendarDays") === true ? 1 : 0)); + + return ( + <> + {recurringEventDefined && ( + + )} + {!recurringEventDefined && requirePayment && ( + <> +
+ {getCurrencySymbol(selectedCurrency.value)}} + addOnSuffix={currency.toUpperCase()} + addOnClassname="h-[38px]" + step="0.01" + min="0.5" + type="number" + required + placeholder="Price" + disabled={disabled} + onChange={(e) => { + setAppData("price", convertToSmallestCurrencyUnit(Number(e.target.value), currency)); + }} + value={price > 0 ? convertFromSmallestToPresentableCurrencyUnit(price, currency) : undefined} + /> +
+
+ + setAppData("refundCountCalendarDays", option?.value === 1)} + value={getSelectedDayType()} + defaultValue={getSelectedDayType()} + /> +  {t("before")} +
+
+ + + + )} + + ); +}; + +export default EventTypeAppSettingsInterface; diff --git a/packages/app-store/paystack/components/PaystackPaymentComponent.tsx b/packages/app-store/paystack/components/PaystackPaymentComponent.tsx new file mode 100644 index 00000000000000..aee316a6eed4b2 --- /dev/null +++ b/packages/app-store/paystack/components/PaystackPaymentComponent.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { Payment } from "@calcom/prisma/client"; +import { Button } from "@calcom/ui/components/button"; + +interface PaystackPaymentData { + access_code: string; + authorization_url: string; + publicKey: string; + reference: string; +} + +interface PaystackPaymentComponentProps { + payment: Payment & { + data: PaystackPaymentData; + }; + clientId: string; + bookingUid: string; + bookingTitle: string; + amount: number; + currency: string; +} + +export default function PaystackPaymentComponent({ + payment, + bookingUid, + bookingTitle, + amount, + currency, +}: PaystackPaymentComponentProps) { + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + const [errorMessage, setErrorMessage] = useState(""); + const { t } = useLocale(); + + const paymentData = payment.data as unknown as PaystackPaymentData; + + const formattedAmount = new Intl.NumberFormat("en", { + style: "currency", + currency: currency.toUpperCase(), + }).format(amount / 100); + + const handlePayment = async () => { + setStatus("loading"); + setErrorMessage(""); + + try { + const PaystackPop = (await import("@paystack/inline-js")).default; + const popup = new PaystackPop(); + + popup.resumeTransaction(paymentData.access_code, { + onSuccess: async () => { + setStatus("success"); + + // Backup verification — call our verify endpoint + try { + await fetch( + `/api/integrations/paystack/verify?reference=${paymentData.reference}` + ); + } catch { + // Webhook will handle it if this fails + } + + // Redirect to booking confirmation + setTimeout(() => { + window.location.href = `/booking/${bookingUid}`; + }, 2000); + }, + onCancel: () => { + setStatus("idle"); + }, + onError: () => { + setStatus("error"); + setErrorMessage(t("payment_failed_try_again")); + }, + }); + } catch { + setStatus("error"); + setErrorMessage(t("payment_failed_try_again")); + } + }; + + if (status === "success") { + return ( +
+
{t("payment_successful")}
+

{t("redirecting_to_booking_confirmation")}

+
+ ); + } + + return ( +
+

{bookingTitle}

+

{formattedAmount}

+ + {errorMessage &&

{errorMessage}

} + + + + {status === "idle" && ( +

{t("paystack_payment_prompt")}

+ )} +
+ ); +} diff --git a/packages/app-store/paystack/config.json b/packages/app-store/paystack/config.json new file mode 100644 index 00000000000000..a0428d445850c0 --- /dev/null +++ b/packages/app-store/paystack/config.json @@ -0,0 +1,20 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Paystack", + "slug": "paystack", + "type": "paystack_payment", + "logo": "icon.svg", + "url": "https://paystack.com", + "variant": "payment", + "categories": ["payment"], + "publisher": "Cal.com", + "email": "support@cal.com", + "description": "Accept payments via Paystack for your Cal.com bookings", + "extendsFeature": "EventType", + "isTemplate": false, + "__createdUsingCli": true, + "imageSrc": "icon.svg", + "__template": "event-type-app-card", + "dirName": "paystack", + "isOAuth": true +} diff --git a/packages/app-store/paystack/index.ts b/packages/app-store/paystack/index.ts new file mode 100644 index 00000000000000..e2e9d7b029c031 --- /dev/null +++ b/packages/app-store/paystack/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/paystack/lib/PaymentService.ts b/packages/app-store/paystack/lib/PaymentService.ts new file mode 100644 index 00000000000000..99c1a7cfc76cba --- /dev/null +++ b/packages/app-store/paystack/lib/PaymentService.ts @@ -0,0 +1,207 @@ +import { v4 as uuidv4 } from "uuid"; + +import prisma from "@calcom/prisma"; +import type { Booking, Payment, Prisma, PaymentOption } from "@calcom/prisma/client"; +import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; + +import { appKeysSchema } from "../zod"; +import { PaystackClient } from "./PaystackClient"; + +class PaystackPaymentService implements IAbstractPaymentService { + private client: PaystackClient | null; + private credentials: { public_key: string; secret_key: string } | null; + + constructor(credentials: { key: Prisma.JsonValue }) { + const parsed = appKeysSchema.safeParse(credentials.key); + if (parsed.success) { + this.credentials = parsed.data; + this.client = new PaystackClient(parsed.data.secret_key); + } else { + this.credentials = null; + this.client = null; + } + } + + async create( + payment: Pick, + bookingId: Booking["id"], + _userId: Booking["userId"], + _username: string | null, + _bookerName: string | null, + _paymentOption: PaymentOption, + bookerEmail: string, + _bookerPhoneNumber?: string | null, + eventTitle?: string, + _bookingTitle?: string + ): Promise { + const booking = await prisma.booking.findUnique({ + select: { uid: true, title: true }, + where: { id: bookingId }, + }); + + if (!booking) { + throw new Error("Booking not found"); + } + + if (!this.client || !this.credentials) { + throw new Error("Paystack credentials not configured"); + } + + const uid = uuidv4(); + const reference = `cal_${bookingId}_${uid.slice(0, 8)}`; + + const paystackResponse = await this.client.initializeTransaction({ + email: bookerEmail, + amount: payment.amount, + currency: payment.currency.toUpperCase(), + reference, + callback_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/paystack/verify`, + metadata: { + bookingId, + eventTitle: eventTitle || booking.title, + }, + }); + + const paymentData = await prisma.payment.create({ + data: { + uid, + app: { + connect: { + slug: "paystack", + }, + }, + booking: { + connect: { + id: bookingId, + }, + }, + amount: payment.amount, + externalId: reference, + currency: payment.currency, + data: { + access_code: paystackResponse.access_code, + authorization_url: paystackResponse.authorization_url, + publicKey: this.credentials.public_key, + reference, + } as unknown as Prisma.InputJsonValue, + fee: 0, + refunded: false, + success: false, + paymentOption: "ON_BOOKING", + }, + }); + + return paymentData; + } + + async collectCard( + _payment: Pick, + _bookingId: Booking["id"], + _paymentOption: PaymentOption, + _bookerEmail: string, + _bookerPhoneNumber?: string | null + ): Promise { + throw new Error("Paystack does not support card hold. Only ON_BOOKING payment is available."); + } + + async chargeCard( + _payment: Pick, + _bookingId?: Booking["id"] + ): Promise { + throw new Error("Paystack does not support card hold. Only ON_BOOKING payment is available."); + } + + async update( + paymentId: Payment["id"], + data: Partial + ): Promise { + return await prisma.payment.update({ + where: { id: paymentId }, + data, + }); + } + + async refund(paymentId: Payment["id"]): Promise { + const payment = await prisma.payment.findUnique({ + where: { id: paymentId }, + select: { + id: true, + success: true, + refunded: true, + externalId: true, + }, + }); + + if (!payment) { + return null; + } + + if (payment.refunded) { + return await prisma.payment.findUnique({ where: { id: paymentId } }); + } + + if (!payment.success) { + return await prisma.payment.findUnique({ where: { id: paymentId } }); + } + + if (!this.client) { + throw new Error("Paystack credentials not configured"); + } + + await this.client.createRefund({ + transaction: payment.externalId, + }); + + return await prisma.payment.update({ + where: { id: paymentId }, + data: { refunded: true }, + }); + } + + async getPaymentPaidStatus(): Promise { + return "paid"; + } + + async getPaymentDetails(): Promise { + throw new Error("Method not implemented."); + } + + async afterPayment( + _event: CalendarEvent, + _booking: { + user: { email: string | null; name: string | null; timeZone: string } | null; + id: number; + startTime: { toISOString: () => string }; + uid: string; + }, + _paymentData: Payment + ): Promise { + // No post-payment actions needed for Paystack + return Promise.resolve(); + } + + async deletePayment(paymentId: Payment["id"]): Promise { + try { + await prisma.payment.delete({ + where: { id: paymentId }, + }); + return true; + } catch { + return false; + } + } + + isSetupAlready(): boolean { + return !!(this.credentials?.public_key && this.credentials?.secret_key); + } +} + +/** + * Factory function that creates a Paystack Payment service instance. + * Exported instead of the class to prevent internal types from leaking + * into the emitted .d.ts file. + */ +export function BuildPaymentService(credentials: { key: Prisma.JsonValue }): IAbstractPaymentService { + return new PaystackPaymentService(credentials); +} diff --git a/packages/app-store/paystack/lib/PaystackClient.ts b/packages/app-store/paystack/lib/PaystackClient.ts new file mode 100644 index 00000000000000..14e97902d58b94 --- /dev/null +++ b/packages/app-store/paystack/lib/PaystackClient.ts @@ -0,0 +1,84 @@ +const PAYSTACK_BASE_URL = "https://api.paystack.co"; + +export class PaystackClient { + private secretKey: string; + + constructor(secretKey: string) { + this.secretKey = secretKey; + } + + async initializeTransaction(params: { + email: string; + amount: number; + currency: string; + reference: string; + callback_url: string; + metadata?: Record; + }): Promise<{ + authorization_url: string; + access_code: string; + reference: string; + }> { + const response = await fetch(`${PAYSTACK_BASE_URL}/transaction/initialize`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.secretKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + const json = await response.json(); + + if (!response.ok || !json.status) { + throw new Error(`Paystack API error: ${json.message || "Unknown error"}`); + } + + return json.data; + } + + async verifyTransaction(reference: string): Promise<{ + status: string; + amount: number; + currency: string; + reference: string; + paid_at: string | null; + }> { + const response = await fetch(`${PAYSTACK_BASE_URL}/transaction/verify/${reference}`, { + method: "GET", + headers: { + Authorization: `Bearer ${this.secretKey}`, + }, + }); + + const json = await response.json(); + + if (!response.ok || !json.status) { + throw new Error(`Paystack API error: ${json.message || "Unknown error"}`); + } + + return json.data; + } + + async createRefund(params: { + transaction: string; + amount?: number; + }): Promise<{ status: boolean; data: unknown }> { + const response = await fetch(`${PAYSTACK_BASE_URL}/refund`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.secretKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + const json = await response.json(); + + if (!response.ok || !json.status) { + throw new Error(`Paystack API error: ${json.message || "Unknown error"}`); + } + + return json; + } +} diff --git a/packages/app-store/paystack/lib/__tests__/PaystackClient.test.ts b/packages/app-store/paystack/lib/__tests__/PaystackClient.test.ts new file mode 100644 index 00000000000000..ace6a810eccde4 --- /dev/null +++ b/packages/app-store/paystack/lib/__tests__/PaystackClient.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +import { PaystackClient } from "../PaystackClient"; + +describe("PaystackClient", () => { + let client: PaystackClient; + + beforeEach(() => { + client = new PaystackClient("sk_test_xxxxx"); + }); + + describe("initializeTransaction", () => { + it("sends correct params and returns parsed response", async () => { + const mockResponse = { + status: true, + message: "Authorization URL created", + data: { + authorization_url: "https://checkout.paystack.com/abc123", + access_code: "abc123", + reference: "cal_42_ref123", + }, + }; + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }) + ); + + const result = await client.initializeTransaction({ + email: "test@example.com", + amount: 500000, + currency: "NGN", + reference: "cal_42_ref123", + callback_url: "https://cal.com/payment/callback", + metadata: { bookingId: 42 }, + }); + + expect(fetch).toHaveBeenCalledWith("https://api.paystack.co/transaction/initialize", { + method: "POST", + headers: { + Authorization: "Bearer sk_test_xxxxx", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: "test@example.com", + amount: 500000, + currency: "NGN", + reference: "cal_42_ref123", + callback_url: "https://cal.com/payment/callback", + metadata: { bookingId: 42 }, + }), + }); + + expect(result).toEqual({ + authorization_url: "https://checkout.paystack.com/abc123", + access_code: "abc123", + reference: "cal_42_ref123", + }); + }); + + it("throws on API error response", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: false, + status: 400, + json: () => Promise.resolve({ status: false, message: "Invalid amount" }), + }) + ); + + await expect( + client.initializeTransaction({ + email: "test@example.com", + amount: 0, + currency: "NGN", + reference: "cal_42_ref123", + callback_url: "https://cal.com/payment/callback", + }) + ).rejects.toThrow("Paystack API error: Invalid amount"); + }); + }); + + describe("verifyTransaction", () => { + it("returns parsed verification result", async () => { + const mockResponse = { + status: true, + data: { + status: "success", + amount: 500000, + currency: "NGN", + reference: "cal_42_ref123", + paid_at: "2026-04-04T12:00:00.000Z", + }, + }; + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }) + ); + + const result = await client.verifyTransaction("cal_42_ref123"); + + expect(fetch).toHaveBeenCalledWith( + "https://api.paystack.co/transaction/verify/cal_42_ref123", + { + method: "GET", + headers: { + Authorization: "Bearer sk_test_xxxxx", + }, + } + ); + + expect(result).toEqual({ + status: "success", + amount: 500000, + currency: "NGN", + reference: "cal_42_ref123", + paid_at: "2026-04-04T12:00:00.000Z", + }); + }); + }); + + describe("createRefund", () => { + it("sends refund request with transaction reference", async () => { + const mockResponse = { + status: true, + data: { + status: "pending", + transaction: { reference: "cal_42_ref123" }, + }, + }; + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }) + ); + + const result = await client.createRefund({ transaction: "cal_42_ref123" }); + + expect(fetch).toHaveBeenCalledWith("https://api.paystack.co/refund", { + method: "POST", + headers: { + Authorization: "Bearer sk_test_xxxxx", + "Content-Type": "application/json", + }, + body: JSON.stringify({ transaction: "cal_42_ref123" }), + }); + + expect(result.status).toBe(true); + }); + }); +}); diff --git a/packages/app-store/paystack/lib/__tests__/verifyWebhookSignature.test.ts b/packages/app-store/paystack/lib/__tests__/verifyWebhookSignature.test.ts new file mode 100644 index 00000000000000..1936684e7cf83e --- /dev/null +++ b/packages/app-store/paystack/lib/__tests__/verifyWebhookSignature.test.ts @@ -0,0 +1,28 @@ +import crypto from "crypto"; + +import { describe, it, expect } from "vitest"; + +import { verifyWebhookSignature } from "../verifyWebhookSignature"; + +describe("verifyWebhookSignature", () => { + const secretKey = "sk_test_secretkey123"; + const body = JSON.stringify({ event: "charge.success", data: { reference: "ref123" } }); + const validSignature = crypto.createHmac("sha512", secretKey).update(body).digest("hex"); + + it("returns true for valid signature", () => { + expect(verifyWebhookSignature(body, validSignature, secretKey)).toBe(true); + }); + + it("returns false for tampered body", () => { + const tamperedBody = JSON.stringify({ event: "charge.success", data: { reference: "TAMPERED" } }); + expect(verifyWebhookSignature(tamperedBody, validSignature, secretKey)).toBe(false); + }); + + it("returns false for wrong secret key", () => { + expect(verifyWebhookSignature(body, validSignature, "wrong_secret")).toBe(false); + }); + + it("returns false for empty signature", () => { + expect(verifyWebhookSignature(body, "", secretKey)).toBe(false); + }); +}); diff --git a/packages/app-store/paystack/lib/currencyOptions.ts b/packages/app-store/paystack/lib/currencyOptions.ts new file mode 100644 index 00000000000000..02825261a7f23e --- /dev/null +++ b/packages/app-store/paystack/lib/currencyOptions.ts @@ -0,0 +1,19 @@ +export const currencyOptions = [ + { label: "Nigerian naira (NGN)", value: "ngn" }, + { label: "Ghanaian cedi (GHS)", value: "ghs" }, + { label: "South African rand (ZAR)", value: "zar" }, + { label: "Kenyan shilling (KES)", value: "kes" }, + { label: "United States dollar (USD)", value: "usd" }, +]; + +export const currencySymbols: Record = { + ngn: "₦", + ghs: "GH₵", + zar: "R", + kes: "KSh", + usd: "$", +}; + +export const isAcceptedCurrencyCode = (code: string): code is keyof typeof currencySymbols => { + return code in currencySymbols; +}; diff --git a/packages/app-store/paystack/lib/verifyWebhookSignature.ts b/packages/app-store/paystack/lib/verifyWebhookSignature.ts new file mode 100644 index 00000000000000..8c3d24043f2e49 --- /dev/null +++ b/packages/app-store/paystack/lib/verifyWebhookSignature.ts @@ -0,0 +1,13 @@ +import crypto from "crypto"; + +export function verifyWebhookSignature(body: string, signature: string, secretKey: string): boolean { + if (!signature) return false; + + const hash = crypto.createHmac("sha512", secretKey).update(body).digest("hex"); + + try { + return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature)); + } catch { + return false; + } +} diff --git a/packages/app-store/paystack/package.json b/packages/app-store/paystack/package.json new file mode 100644 index 00000000000000..244cc297392e8d --- /dev/null +++ b/packages/app-store/paystack/package.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/paystack", + "version": "0.0.0", + "main": "./index.ts", + "scripts": { + "test": "TZ=UTC vitest run" + }, + "dependencies": { + "@calcom/lib": "workspace:*", + "@paystack/inline-js": "^2.0.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@calcom/types": "workspace:*", + "@types/uuid": "^9.0.0" + }, + "description": "Paystack payment integration for Cal.com" +} diff --git a/packages/app-store/paystack/pages/setup/_getServerSideProps.ts b/packages/app-store/paystack/pages/setup/_getServerSideProps.ts new file mode 100644 index 00000000000000..fb322eb278b3c0 --- /dev/null +++ b/packages/app-store/paystack/pages/setup/_getServerSideProps.ts @@ -0,0 +1,40 @@ +import type { GetServerSidePropsContext } from "next"; + +import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import prisma from "@calcom/prisma"; + +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const notFound = { notFound: true } as const; + + if (typeof ctx.params?.slug !== "string") return notFound; + + const { req, query } = ctx; + const session = await getServerSession({ req }); + + if (!session?.user?.id) { + return { redirect: { permanent: false, destination: "/auth/login" } } as const; + } + + const teamId = query.teamId ? Number(query.teamId) : null; + + await throwIfNotHaveAdminAccessToTeam({ teamId, userId: session.user.id }); + const installForObject = teamId ? { teamId } : { userId: session.user.id }; + + const credential = await prisma.credential.findFirst({ + where: { + type: "paystack_payment", + ...installForObject, + }, + select: { + id: true, + key: true, + }, + }); + + return { + props: { + credentialId: credential?.id ?? null, + }, + }; +}; diff --git a/packages/app-store/paystack/static/icon.svg b/packages/app-store/paystack/static/icon.svg new file mode 100644 index 00000000000000..b1556dad70f44c --- /dev/null +++ b/packages/app-store/paystack/static/icon.svg @@ -0,0 +1 @@ + diff --git a/packages/app-store/paystack/zod.ts b/packages/app-store/paystack/zod.ts new file mode 100644 index 00000000000000..aa4bca386fc15e --- /dev/null +++ b/packages/app-store/paystack/zod.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; +import { RefundPolicy } from "@calcom/lib/payment/types"; + +export const appDataSchema = eventTypeAppCardZod.merge( + z.object({ + price: z.number(), + currency: z.string(), + paymentOption: z.literal("ON_BOOKING").optional(), + refundPolicy: z.nativeEnum(RefundPolicy).optional(), + refundDaysCount: z.number().optional(), + refundCountCalendarDays: z.boolean().optional(), + }) +); + +export const appKeysSchema = z.object({ + public_key: z.string().min(1), + secret_key: z.string().min(1), +}); diff --git a/packages/app-store/redirect-apps.generated.ts b/packages/app-store/redirect-apps.generated.ts index abe13cf533913f..a035a8d21a0e8d 100644 --- a/packages/app-store/redirect-apps.generated.ts +++ b/packages/app-store/redirect-apps.generated.ts @@ -2,34 +2,5 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -export const REDIRECT_APPS = [ - "amie", - "autocheckin", - "baa-for-hipaa", - "bolna", - "caretta", - "chatbase", - "clic", - "cron", - "deel", - "elevenlabs", - "famulor", - "fonio-ai", - "framer", - "granola", - "greetmate-ai", - "lindy", - "linear", - "millis-ai", - "monobot", - "n8n", - "pipedream", - "raycast", - "retell-ai", - "synthflow", - "telli", - "vimcal", - "wordpress", - "zapier", -] as const; -export type RedirectApp = (typeof REDIRECT_APPS)[number]; +export const REDIRECT_APPS = ["amie","autocheckin","baa-for-hipaa","bolna","caretta","chatbase","clic","cron","deel","elevenlabs","famulor","fonio-ai","framer","granola","greetmate-ai","lindy","linear","millis-ai","monobot","n8n","pipedream","raycast","retell-ai","synthflow","telli","vimcal","wordpress","zapier"] as const; +export type RedirectApp = typeof REDIRECT_APPS[number]; \ No newline at end of file diff --git a/packages/app-store/video.adapters.generated.ts b/packages/app-store/video.adapters.generated.ts index f1e0f05ffe7f78..e069a2ae8fdc9d 100644 --- a/packages/app-store/video.adapters.generated.ts +++ b/packages/app-store/video.adapters.generated.ts @@ -2,20 +2,17 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -export const VideoApiAdapterMap = - process.env.NEXT_PUBLIC_IS_E2E === "1" - ? {} - : { - dailyvideo: import("./dailyvideo/lib/VideoApiAdapter"), - huddle01video: import("./huddle01video/lib/VideoApiAdapter"), - jelly: import("./jelly/lib/VideoApiAdapter"), - jitsivideo: import("./jitsivideo/lib/VideoApiAdapter"), - lyra: import("./lyra/lib/VideoApiAdapter"), - nextcloudtalk: import("./nextcloudtalk/lib/VideoApiAdapter"), - office365video: import("./office365video/lib/VideoApiAdapter"), - shimmervideo: import("./shimmervideo/lib/VideoApiAdapter"), - sylapsvideo: import("./sylapsvideo/lib/VideoApiAdapter"), - tandemvideo: import("./tandemvideo/lib/VideoApiAdapter"), - webex: import("./webex/lib/VideoApiAdapter"), - zoomvideo: import("./zoomvideo/lib/VideoApiAdapter"), - }; +export const VideoApiAdapterMap = process.env.NEXT_PUBLIC_IS_E2E === '1' ? {} : { +"dailyvideo": import("./dailyvideo/lib/VideoApiAdapter"), +"huddle01video": import("./huddle01video/lib/VideoApiAdapter"), +"jelly": import("./jelly/lib/VideoApiAdapter"), +"jitsivideo": import("./jitsivideo/lib/VideoApiAdapter"), +"lyra": import("./lyra/lib/VideoApiAdapter"), +"nextcloudtalk": import("./nextcloudtalk/lib/VideoApiAdapter"), +"office365video": import("./office365video/lib/VideoApiAdapter"), +"shimmervideo": import("./shimmervideo/lib/VideoApiAdapter"), +"sylapsvideo": import("./sylapsvideo/lib/VideoApiAdapter"), +"tandemvideo": import("./tandemvideo/lib/VideoApiAdapter"), +"webex": import("./webex/lib/VideoApiAdapter"), +"zoomvideo": import("./zoomvideo/lib/VideoApiAdapter"), +}; \ No newline at end of file diff --git a/packages/i18n/locales/en/common.json b/packages/i18n/locales/en/common.json index ef962e53778e2c..d54e6fc3a35194 100644 --- a/packages/i18n/locales/en/common.json +++ b/packages/i18n/locales/en/common.json @@ -3750,6 +3750,17 @@ "resume_onboarding": "Resume onboarding", "resume_onboarding_description": "Continue with your organization onboarding process", "payment_successful": "Payment successful!", + "payment_failed_try_again": "Payment failed. Please try again.", + "redirecting_to_booking_confirmation": "Redirecting to your booking confirmation...", + "pay_with_paystack": "Pay with Paystack", + "paystack_payment_prompt": "You will be prompted to enter your payment details", + "paystack_app_description": "Accept payments via Paystack for your events", + "paystack_public_key": "Public Key", + "paystack_secret_key": "Secret Key", + "paystack_getting_started_description": "To use Paystack with Cal.com, you need a Paystack account. Get your API keys from your", + "paystack_dashboard": "Paystack Dashboard", + "paystack_webhook_setup": "Webhook Setup", + "paystack_webhook_setup_description": "Add this webhook URL in your Paystack dashboard under Settings > API Keys & Webhooks:", "handover_onboarding_page_title": "Handover onboarding", "handover_onboarding_page_description": "Handover onboarding to the user", "organization_already_exists_with_this_slug": "Organization already exists with this slug", @@ -4568,6 +4579,7 @@ "location_applied_to_hosts_other": "Location applied to {{count}} hosts", "booking_audit_action": { "created": "Booked with {{host}}", + "created_awaiting_host": "Booked (awaiting host)", "created_with_seat": "Seat Booked with {{host}}", "cancelled": "Cancelled", "rescheduled": "Rescheduled {{oldDate}} -> <0>{{newDate}}", diff --git a/scripts/seed-app-store.ts b/scripts/seed-app-store.ts index 45b1f36ed95b43..26d581647b59ef 100644 --- a/scripts/seed-app-store.ts +++ b/scripts/seed-app-store.ts @@ -235,6 +235,9 @@ export default async function main() { }); } + // Paystack doesn't require env vars at seed time — keys are entered via Setup UI + await createApp("paystack", "paystack", ["payment"], "paystack_payment"); + if (process.env.CLOSECOM_CLIENT_ID && process.env.CLOSECOM_CLIENT_SECRET) { await createApp("closecom", "closecom", ["crm"], "closecom_crm", { client_id: process.env.CLOSECOM_CLIENT_ID, diff --git a/yarn.lock b/yarn.lock index 838a26c3f11d78..0671b849d82257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2881,6 +2881,18 @@ __metadata: languageName: unknown linkType: soft +"@calcom/paystack@workspace:packages/app-store/paystack": + version: 0.0.0-use.local + resolution: "@calcom/paystack@workspace:packages/app-store/paystack" + dependencies: + "@calcom/lib": "workspace:*" + "@calcom/types": "workspace:*" + "@paystack/inline-js": "npm:^2.0.0" + "@types/uuid": "npm:^9.0.0" + uuid: "npm:^9.0.0" + languageName: unknown + linkType: soft + "@calcom/ping@workspace:packages/app-store/ping": version: 0.0.0-use.local resolution: "@calcom/ping@workspace:packages/app-store/ping" @@ -10476,6 +10488,13 @@ __metadata: languageName: node linkType: hard +"@paystack/inline-js@npm:^2.0.0": + version: 2.22.8 + resolution: "@paystack/inline-js@npm:2.22.8" + checksum: 10/925a9e1d5d90fcde4d8316493afef2c988d5d8f503bc5c439669aa345348cf383fa083c3e2c4f83ecad45d0bd943977e050464ce85042f06a2117da02fa13cdd + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -16468,7 +16487,7 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.1": +"@types/uuid@npm:^9.0.0, @types/uuid@npm:^9.0.1": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 From acacbb9f6bb57ccc2e3f3197589577fd632c9b6a Mon Sep 17 00:00:00 2001 From: "Marvel.Codes" Date: Sat, 4 Apr 2026 23:06:17 +0100 Subject: [PATCH 2/3] fix: address code review issues in Paystack integration - Webhook: atomic idempotency via updateMany WHERE success=false - Webhook: rollback success flag if Paystack API verification fails - EventTypeSettings: guard parseInt("") returning NaN on cleared input - PaymentComponent: use convertFromSmallestToPresentableCurrencyUnit for zero-decimal currencies - Add handler: validate teamId is numeric before access check - Add handler: consolidate owner filter, return 409 for duplicate installs --- packages/app-store/paystack/api/add.ts | 14 +++++++++++--- packages/app-store/paystack/api/webhook.ts | 15 +++++++++++++-- .../components/EventTypeAppSettingsInterface.tsx | 5 ++++- .../components/PaystackPaymentComponent.tsx | 3 ++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/app-store/paystack/api/add.ts b/packages/app-store/paystack/api/add.ts index 1c3543a30c8073..6128bc986d20f8 100644 --- a/packages/app-store/paystack/api/add.ts +++ b/packages/app-store/paystack/api/add.ts @@ -14,15 +14,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { teamId } = req.query; const teamIdNumber = teamId ? Number(teamId) : null; + if (teamIdNumber !== null && Number.isNaN(teamIdNumber)) { + return res.status(400).json({ message: "Invalid teamId" }); + } + await throwIfNotHaveAdminAccessToTeam({ teamId: teamIdNumber, userId: req.session.user.id }); - const installForObject = teamIdNumber ? { teamId: teamIdNumber } : { userId: req.session.user.id }; const appType = config.type; + const ownerFilter = teamIdNumber ? { teamId: teamIdNumber } : { userId: req.session.user.id }; + try { const alreadyInstalled = await prisma.credential.findFirst({ where: { type: appType, - ...installForObject, + ...ownerFilter, }, }); if (alreadyInstalled) { @@ -33,10 +38,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) type: appType, key: {}, appId: "paystack", - ...(teamIdNumber ? { teamId: teamIdNumber } : { userId: req.session.user.id }), + ...ownerFilter, }, }); } catch (error: unknown) { + if (error instanceof Error && error.message === "Already installed") { + return res.status(409).json({ message: error.message }); + } const httpError = getServerErrorFromUnknown(error); return res.status(httpError.statusCode).json({ message: httpError.message }); } diff --git a/packages/app-store/paystack/api/webhook.ts b/packages/app-store/paystack/api/webhook.ts index 085e1899641793..2a28b40d36babf 100644 --- a/packages/app-store/paystack/api/webhook.ts +++ b/packages/app-store/paystack/api/webhook.ts @@ -108,8 +108,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - // Idempotency: if already successful, skip - if (payment.success) { + // Atomic idempotency: only proceed if we can flip success from false to true + const updated = await prisma.payment.updateMany({ + where: { id: payment.id, success: false }, + data: { success: true }, + }); + + if (updated.count === 0) { + // Another request already processed this payment res.status(200).json({ message: "Payment already processed" }); return; } @@ -119,6 +125,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const verification = await client.verifyTransaction(reference); if (verification.status !== "success") { + // Rollback the success flag since verification failed + await prisma.payment.update({ + where: { id: payment.id }, + data: { success: false }, + }); log.error("Paystack verification failed", { reference, status: verification.status }); throw new HttpCode({ statusCode: 400, message: "Payment verification failed" }); } diff --git a/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx b/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx index 500201e68bc5ee..d7b85f504c03ce 100644 --- a/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx +++ b/packages/app-store/paystack/components/EventTypeAppSettingsInterface.tsx @@ -143,7 +143,10 @@ const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ defaultValue={getAppData("refundDaysCount")} required={getAppData("refundPolicy") === RefundPolicy.DAYS} value={getAppData("refundDaysCount") ?? ""} - onChange={(e) => setAppData("refundDaysCount", parseInt(e.currentTarget.value))} + onChange={(e) => { + const parsed = parseInt(e.currentTarget.value); + setAppData("refundDaysCount", Number.isNaN(parsed) ? undefined : parsed); + }} />