From 6483182ef6c57112188606dd40822c5c3d86c16c Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Wed, 11 May 2022 06:58:10 +0200 Subject: [PATCH] add invite link to Zapier setup page (#2696) * add invite link and toaster to zapier setup page * create env variable for invite link and save in database * fetch invite link form getStaticProps * add getStaticPath method * clean code * Moves app setup and index page * Moves Loader to ui * Trying new way to handle dynamic app store pages * Cleanup * Update tailwind.config.js * zapier invite link fixes * Tests fixes Co-authored-by: CarinaWolli Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars --- .env.appStore.example | 5 +++ apps/web/components/Loader.tsx | 8 +--- apps/web/ee/lib/stripe/server.ts | 9 +++- .../apps/{[slug].tsx => [slug]/index.tsx} | 0 apps/web/pages/apps/[slug]/setup.tsx | 45 +++++++++++++++++++ apps/web/pages/apps/setup/[appName].tsx | 38 ---------------- apps/web/public/static/locales/en/common.json | 2 +- apps/web/tailwind.config.js | 2 +- .../_components/DynamicComponent.tsx | 9 ++++ .../_pages/setup/_getStaticProps.tsx | 20 +++++++++ packages/app-store/_pages/setup/index.tsx | 13 ++++++ packages/app-store/zapier/README.md | 4 +- packages/app-store/zapier/README.mdx | 3 +- packages/app-store/zapier/api/add.ts | 2 +- packages/app-store/zapier/components/index.ts | 1 - .../zapier/pages/setup/_getStaticProps.tsx | 20 +++++++++ .../zapierSetup.tsx => pages/setup/index.tsx} | 27 +++++++---- packages/prisma/seed-app-store.ts | 7 ++- packages/ui/Loader.tsx | 7 +++ packages/ui/index.tsx | 1 + 20 files changed, 159 insertions(+), 64 deletions(-) rename apps/web/pages/apps/{[slug].tsx => [slug]/index.tsx} (100%) create mode 100644 apps/web/pages/apps/[slug]/setup.tsx delete mode 100644 apps/web/pages/apps/setup/[appName].tsx create mode 100644 packages/app-store/_components/DynamicComponent.tsx create mode 100644 packages/app-store/_pages/setup/_getStaticProps.tsx create mode 100644 packages/app-store/_pages/setup/index.tsx create mode 100644 packages/app-store/zapier/pages/setup/_getStaticProps.tsx rename packages/app-store/zapier/{components/zapierSetup.tsx => pages/setup/index.tsx} (85%) create mode 100644 packages/ui/Loader.tsx diff --git a/.env.appStore.example b/.env.appStore.example index 9f6825c32f..667b5bf877 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 29ac50d738..1eb4ef8161 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 1302fdea0a..625b985f6a 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 0000000000..f621802ee6 --- /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 b8bdc7c6f6..0000000000 --- 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 f9ab715349..db1a3deda0 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 3a93df3593..e1f9b8e7bc 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 0000000000..25ba7288a6 --- /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 0000000000..0e8b631199 --- /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 0000000000..b4e14e8139 --- /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 f00a3752c1..80abd1165f 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 52d0240a08..ca51a99e12 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 0c01b593cc..9f1cf72ba0 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 5f2c7965c9..e191e97eec 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 0000000000..b73a0786cc --- /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 362135c8cf..5c28b2671b 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 905eb00907..b990c333b6 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 0000000000..29ac50d738 --- /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 c1523e7817..f466516ef2 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";