diff --git a/.env.appStore.example b/.env.appStore.example index 9f6825c3..667b5bf8 100644 --- a/.env.appStore.example +++ b/.env.appStore.example @@ -81,4 +81,9 @@ VITAL_WEBHOOK_SECRET= VITAL_DEVELOPMENT_MODE="sandbox" # "us" | "eu" VITAL_REGION="us" + +# - ZAPIER +# Used for the Zapier integration +# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md +ZAPIER_INVITE_LINK="" # ********************************************************************************************************* diff --git a/apps/web/components/Loader.tsx b/apps/web/components/Loader.tsx index 29ac50d7..1eb4ef81 100644 --- a/apps/web/components/Loader.tsx +++ b/apps/web/components/Loader.tsx @@ -1,7 +1 @@ -export default function Loader() { - return ( -
- -
- ); -} +export { default } from "@calcom/ui/Loader"; diff --git a/apps/web/ee/lib/stripe/server.ts b/apps/web/ee/lib/stripe/server.ts index 1302fdea..625b985f 100644 --- a/apps/web/ee/lib/stripe/server.ts +++ b/apps/web/ee/lib/stripe/server.ts @@ -2,6 +2,7 @@ import { PaymentType, Prisma } from "@prisma/client"; import Stripe from "stripe"; import { v4 as uuidv4 } from "uuid"; +import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import prisma from "@calcom/prisma"; import { createPaymentLink } from "@calcom/stripe/client"; @@ -16,8 +17,8 @@ export type PaymentInfo = { id?: string | null; }; -const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!; -const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!; +let paymentFeePercentage: number | undefined; +let paymentFeeFixed: number | undefined; export async function handlePayment( evt: CalendarEvent, @@ -33,6 +34,10 @@ export async function handlePayment( uid: string; } ) { + const appKeys = await getAppKeysFromSlug("stripe"); + if (typeof appKeys.payment_fee_fixed === "number") paymentFeePercentage = appKeys.payment_fee_fixed; + if (typeof appKeys.payment_fee_percentage === "number") paymentFeeFixed = appKeys.payment_fee_percentage; + const paymentFee = Math.round( selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`) ); diff --git a/apps/web/pages/apps/[slug].tsx b/apps/web/pages/apps/[slug]/index.tsx similarity index 100% rename from apps/web/pages/apps/[slug].tsx rename to apps/web/pages/apps/[slug]/index.tsx diff --git a/apps/web/pages/apps/[slug]/setup.tsx b/apps/web/pages/apps/[slug]/setup.tsx new file mode 100644 index 00000000..f621802e --- /dev/null +++ b/apps/web/pages/apps/[slug]/setup.tsx @@ -0,0 +1,45 @@ +import { InferGetStaticPropsType } from "next"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/router"; + +import { AppSetupPage } from "@calcom/app-store/_pages/setup"; +import { AppSetupPageMap, getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps"; +import prisma from "@calcom/prisma"; +import Loader from "@calcom/ui/Loader"; + +export default function SetupInformation(props: InferGetStaticPropsType) { + const router = useRouter(); + const slug = router.query.slug as string; + const { status } = useSession(); + + if (status === "loading") { + return ( +
+ +
+ ); + } + + if (status === "unauthenticated") { + router.replace({ + pathname: "/auth/login", + query: { + callbackUrl: `/apps/${slug}/setup`, + }, + }); + } + + return ; +} + +export const getStaticPaths = async () => { + const appStore = await prisma.app.findMany({ select: { slug: true } }); + const paths = appStore.filter((a) => a.slug in AppSetupPageMap).map((app) => app.slug); + + return { + paths: paths.map((slug) => ({ params: { slug } })), + fallback: false, + }; +}; + +export { getStaticProps }; diff --git a/apps/web/pages/apps/setup/[appName].tsx b/apps/web/pages/apps/setup/[appName].tsx deleted file mode 100644 index b8bdc7c6..00000000 --- a/apps/web/pages/apps/setup/[appName].tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useSession } from "next-auth/react"; -import { useRouter } from "next/router"; - -import _zapierMetadata from "@calcom/app-store/zapier/_metadata"; -import { ZapierSetup } from "@calcom/app-store/zapier/components"; - -import { trpc } from "@lib/trpc"; - -import Loader from "@components/Loader"; - -export default function SetupInformation() { - const router = useRouter(); - const appName = router.query.appName; - const { status } = useSession(); - - if (status === "loading") { - return ( -
- -
- ); - } - - if (status === "unauthenticated") { - router.replace({ - pathname: "/auth/login", - query: { - callbackUrl: `/apps/setup/${appName}`, - }, - }); - } - - if (appName === _zapierMetadata.name.toLowerCase() && status === "authenticated") { - return ; - } - - return null; -} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index f9ab7153..db1a3ded 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -817,7 +817,7 @@ "generate_api_key": "Generate Api Key", "your_unique_api_key": "Your unique API key", "copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.", - "zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.<1>Select Cal.com as your Trigger app. Also choose a Trigger event.<2>Choose your account and then enter your Unique API Key.<3>Test your Trigger.<4>You're set!", + "zapier_setup_instructions": "<0>Go to: <1>Zapier Invite Link<1>Log into your Zapier account and create a new Zap.<2>Select Cal.com as your Trigger app. Also choose a Trigger event.<3>Choose your account and then enter your Unique API Key.<4>Test your Trigger.<5>You're set!", "install_zapier_app": "Please first install the Zapier App in the app store.", "go_to_app_store": "Go to App Store", "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 3a93df35..e1f9b8e7 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,6 +4,6 @@ module.exports = { content: [ ...base.content, "../../packages/ui/**/*.{js,ts,jsx,tsx}", - "../../packages/app-store/**/components/*.{js,ts,jsx,tsx}", + "../../packages/app-store/**/{components,pages}/**/*.{js,ts,jsx,tsx}", ], }; diff --git a/packages/app-store/_components/DynamicComponent.tsx b/packages/app-store/_components/DynamicComponent.tsx new file mode 100644 index 00000000..25ba7288 --- /dev/null +++ b/packages/app-store/_components/DynamicComponent.tsx @@ -0,0 +1,9 @@ +export function DynamicComponent>(props: { componentMap: T; slug: string }) { + const { componentMap, slug, ...rest } = props; + + if (!componentMap[slug]) return null; + + const Component = componentMap[slug]; + + return ; +} diff --git a/packages/app-store/_pages/setup/_getStaticProps.tsx b/packages/app-store/_pages/setup/_getStaticProps.tsx new file mode 100644 index 00000000..0e8b6311 --- /dev/null +++ b/packages/app-store/_pages/setup/_getStaticProps.tsx @@ -0,0 +1,20 @@ +import { GetStaticPropsContext } from "next"; + +export const AppSetupPageMap = { + zapier: import("../../zapier/pages/setup/_getStaticProps"), +}; + +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + const { slug } = ctx.params || {}; + if (typeof slug !== "string") return { notFound: true } as const; + + if (!(slug in AppSetupPageMap)) return { props: {} }; + + const page = await AppSetupPageMap[slug as keyof typeof AppSetupPageMap]; + + if (!page.getStaticProps) return { props: {} }; + + const props = await page.getStaticProps(ctx); + + return props; +}; diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx new file mode 100644 index 00000000..b4e14e81 --- /dev/null +++ b/packages/app-store/_pages/setup/index.tsx @@ -0,0 +1,13 @@ +import dynamic from "next/dynamic"; + +import { DynamicComponent } from "../../_components/DynamicComponent"; + +export const AppSetupMap = { + zapier: dynamic(() => import("../../zapier/pages/setup")), +}; + +export const AppSetupPage = (props: { slug: string }) => { + return componentMap={AppSetupMap} {...props} />; +}; + +export default AppSetupPage; diff --git a/packages/app-store/zapier/README.md b/packages/app-store/zapier/README.md index f00a3752..80abd116 100644 --- a/packages/app-store/zapier/README.md +++ b/packages/app-store/zapier/README.md @@ -56,9 +56,9 @@ Booking created, Booking rescheduled, Booking cancelled Create the other two triggers (booking rescheduled, booking cancelled) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created) -### Testing integration +### Set ZAPIER_INVITE_LINK -Use the sharing link under Manage → Sharing to create your first Cal.com trigger in Zapier +The invite link can be found under under Manage → Sharing. ## Localhost diff --git a/packages/app-store/zapier/README.mdx b/packages/app-store/zapier/README.mdx index 52d0240a..ca51a99e 100644 --- a/packages/app-store/zapier/README.mdx +++ b/packages/app-store/zapier/README.mdx @@ -2,5 +2,4 @@ Workflow automation for everyone. Use the Cal.com Zapier app to trigger your wor
**After Installation:** You lost your generated API key? Here you can generate a new key and find all information -on how to use the installed app: Zapier App Setup - +on how to use the installed app: Zapier App Setup diff --git a/packages/app-store/zapier/api/add.ts b/packages/app-store/zapier/api/add.ts index 0c01b593..9f1cf72b 100644 --- a/packages/app-store/zapier/api/add.ts +++ b/packages/app-store/zapier/api/add.ts @@ -35,5 +35,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(500); } - return res.status(200).json({ url: "/apps/setup/zapier" }); + return res.status(200).json({ url: "/apps/zapier/setup" }); } diff --git a/packages/app-store/zapier/components/index.ts b/packages/app-store/zapier/components/index.ts index 5f2c7965..e191e97e 100644 --- a/packages/app-store/zapier/components/index.ts +++ b/packages/app-store/zapier/components/index.ts @@ -1,3 +1,2 @@ export { default as InstallAppButton } from "./InstallAppButton"; -export { default as ZapierSetup } from "./zapierSetup"; export { default as Icon } from "./icon"; diff --git a/packages/app-store/zapier/pages/setup/_getStaticProps.tsx b/packages/app-store/zapier/pages/setup/_getStaticProps.tsx new file mode 100644 index 00000000..b73a0786 --- /dev/null +++ b/packages/app-store/zapier/pages/setup/_getStaticProps.tsx @@ -0,0 +1,20 @@ +import { GetStaticPropsContext } from "next"; + +import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug"; + +export interface IZapierSetupProps { + inviteLink: string; +} + +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + if (typeof ctx.params?.slug !== "string") return { notFound: true } as const; + let inviteLink = ""; + const appKeys = await getAppKeysFromSlug("zapier"); + if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link; + + return { + props: { + inviteLink, + }, + }; +}; diff --git a/packages/app-store/zapier/components/zapierSetup.tsx b/packages/app-store/zapier/pages/setup/index.tsx similarity index 85% rename from packages/app-store/zapier/components/zapierSetup.tsx rename to packages/app-store/zapier/pages/setup/index.tsx index 362135c8..5c28b267 100644 --- a/packages/app-store/zapier/components/zapierSetup.tsx +++ b/packages/app-store/zapier/pages/setup/index.tsx @@ -2,28 +2,31 @@ import { ClipboardCopyIcon } from "@heroicons/react/solid"; import { Trans } from "next-i18next"; import Link from "next/link"; import { useState } from "react"; +import { Toaster } from "react-hot-toast"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; -import { Button } from "@calcom/ui"; -import { Tooltip } from "@calcom/ui/Tooltip"; -import Loader from "@calcom/web/components/Loader"; +import { Button, Loader, Tooltip } from "@calcom/ui"; -import Icon from "./icon"; +/** TODO: Maybe extract this into a package to prevent circular dependencies */ +import { trpc } from "@calcom/web/lib/trpc"; -interface IZapierSetupProps { - trpc: any; +import Icon from "../../components/icon"; + +export interface IZapierSetupProps { + inviteLink: string; } const ZAPIER = "zapier"; export default function ZapierSetup(props: IZapierSetupProps) { - const { trpc } = props; const [newApiKey, setNewApiKey] = useState(""); const { t } = useLocale(); const utils = trpc.useContext(); const integrations = trpc.useQuery(["viewer.integrations"]); + // @ts-ignore const oldApiKey = trpc.useQuery(["viewer.apiKeys.findKeyOfType", { appId: ZAPIER }]); + const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete"); const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.other?.items.find( (item: { type: string }) => item.type === "zapier_other" @@ -33,6 +36,7 @@ export default function ZapierSetup(props: IZapierSetupProps) { async function createApiKey() { const event = { note: "Zapier", expiresAt: null, appId: ZAPIER }; + // @ts-ignore const apiKey = await utils.client.mutation("viewer.apiKeys.create", event); if (oldApiKey.data) { deleteApiKey.mutate({ @@ -91,8 +95,14 @@ export default function ZapierSetup(props: IZapierSetupProps) { )} -
    +
      +
    1. + Go to: + + Zapier Invite Link + +
    2. Log into your Zapier account and create a new Zap.
    3. Select Cal.com as your Trigger app. Also choose a Trigger event.
    4. Choose your account and then enter your Unique API Key.
    5. @@ -116,6 +126,7 @@ export default function ZapierSetup(props: IZapierSetupProps) { )} + ); } diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 905eb009..b990c333 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -95,7 +95,12 @@ async function main() { webhook_secret: process.env.VITAL_WEBHOOK_SECRET, }); } - await createApp("zapier", "zapier", ["other"], "zapier_other"); + + if (process.env.ZAPIER_INVITE_LINK) { + await createApp("zapier", "zapier", ["other"], "zapier_other", { + invite_link: process.env.ZAPIER_INVITE_LINK, + }); + } // Web3 apps await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video"); await createApp("metamask", "metamask", ["web3"], "metamask_web3"); diff --git a/packages/ui/Loader.tsx b/packages/ui/Loader.tsx new file mode 100644 index 00000000..29ac50d7 --- /dev/null +++ b/packages/ui/Loader.tsx @@ -0,0 +1,7 @@ +export default function Loader() { + return ( +
      + +
      + ); +} diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index c1523e78..f466516e 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -1,6 +1,7 @@ export { default as Button } from "./Button"; export { default as EmptyScreen } from "./EmptyScreen"; export { default as Select } from "./form/Select"; +export { default as Loader } from "./Loader"; export * from "./skeleton"; export { default as Switch } from "./Switch"; export { default as Tooltip } from "./Tooltip";