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 <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Carina Wollendorfer 2022-05-11 06:58:10 +02:00 committed by GitHub
parent 784a91709c
commit 6483182ef6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 159 additions and 64 deletions

View File

@ -81,4 +81,9 @@ VITAL_WEBHOOK_SECRET=
VITAL_DEVELOPMENT_MODE="sandbox" VITAL_DEVELOPMENT_MODE="sandbox"
# "us" | "eu" # "us" | "eu"
VITAL_REGION="us" 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=""
# ********************************************************************************************************* # *********************************************************************************************************

View File

@ -1,7 +1 @@
export default function Loader() { export { default } from "@calcom/ui/Loader";
return (
<div className="loader border-brand dark:border-darkmodebrand">
<span className="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
);
}

View File

@ -2,6 +2,7 @@ import { PaymentType, Prisma } from "@prisma/client";
import Stripe from "stripe"; import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { createPaymentLink } from "@calcom/stripe/client"; import { createPaymentLink } from "@calcom/stripe/client";
@ -16,8 +17,8 @@ export type PaymentInfo = {
id?: string | null; id?: string | null;
}; };
const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!; let paymentFeePercentage: number | undefined;
const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!; let paymentFeeFixed: number | undefined;
export async function handlePayment( export async function handlePayment(
evt: CalendarEvent, evt: CalendarEvent,
@ -33,6 +34,10 @@ export async function handlePayment(
uid: string; 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( const paymentFee = Math.round(
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`) selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
); );

View File

@ -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<typeof getStaticProps>) {
const router = useRouter();
const slug = router.query.slug as string;
const { status } = useSession();
if (status === "loading") {
return (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
<Loader />
</div>
);
}
if (status === "unauthenticated") {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `/apps/${slug}/setup`,
},
});
}
return <AppSetupPage slug={slug} {...props} />;
}
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 };

View File

@ -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 (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
<Loader />
</div>
);
}
if (status === "unauthenticated") {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `/apps/setup/${appName}`,
},
});
}
if (appName === _zapierMetadata.name.toLowerCase() && status === "authenticated") {
return <ZapierSetup trpc={trpc}></ZapierSetup>;
}
return null;
}

View File

@ -817,7 +817,7 @@
"generate_api_key": "Generate Api Key", "generate_api_key": "Generate Api Key",
"your_unique_api_key": "Your unique 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.", "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.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>", "zapier_setup_instructions": "<0>Go to: <1>Zapier Invite Link</1></0><1>Log into your Zapier account and create a new Zap.</1><2>Select Cal.com as your Trigger app. Also choose a Trigger event.</2><3>Choose your account and then enter your Unique API Key.</3><4>Test your Trigger.</4><5>You're set!</5>",
"install_zapier_app": "Please first install the Zapier App in the app store.", "install_zapier_app": "Please first install the Zapier App in the app store.",
"go_to_app_store": "Go to App Store", "go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions", "calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",

View File

@ -4,6 +4,6 @@ module.exports = {
content: [ content: [
...base.content, ...base.content,
"../../packages/ui/**/*.{js,ts,jsx,tsx}", "../../packages/ui/**/*.{js,ts,jsx,tsx}",
"../../packages/app-store/**/components/*.{js,ts,jsx,tsx}", "../../packages/app-store/**/{components,pages}/**/*.{js,ts,jsx,tsx}",
], ],
}; };

View File

@ -0,0 +1,9 @@
export function DynamicComponent<T extends Record<string, any>>(props: { componentMap: T; slug: string }) {
const { componentMap, slug, ...rest } = props;
if (!componentMap[slug]) return null;
const Component = componentMap[slug];
return <Component {...rest} />;
}

View File

@ -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;
};

View File

@ -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 <DynamicComponent<typeof AppSetupMap> componentMap={AppSetupMap} {...props} />;
};
export default AppSetupPage;

View File

@ -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) 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 ## Localhost

View File

@ -2,5 +2,4 @@ Workflow automation for everyone. Use the Cal.com Zapier app to trigger your wor
<br /> <br />
**After Installation:** You lost your generated API key? Here you can generate a new key and find all information **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: <a href="/apps/setup/zapier">Zapier App Setup</a> on how to use the installed app: <a href="/apps/zapier/setup">Zapier App Setup</a>

View File

@ -35,5 +35,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(500); return res.status(500);
} }
return res.status(200).json({ url: "/apps/setup/zapier" }); return res.status(200).json({ url: "/apps/zapier/setup" });
} }

View File

@ -1,3 +1,2 @@
export { default as InstallAppButton } from "./InstallAppButton"; export { default as InstallAppButton } from "./InstallAppButton";
export { default as ZapierSetup } from "./zapierSetup";
export { default as Icon } from "./icon"; export { default as Icon } from "./icon";

View File

@ -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,
},
};
};

View File

@ -2,28 +2,31 @@ import { ClipboardCopyIcon } from "@heroicons/react/solid";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui"; import { Button, Loader, Tooltip } from "@calcom/ui";
import { Tooltip } from "@calcom/ui/Tooltip";
import Loader from "@calcom/web/components/Loader";
import Icon from "./icon"; /** TODO: Maybe extract this into a package to prevent circular dependencies */
import { trpc } from "@calcom/web/lib/trpc";
interface IZapierSetupProps { import Icon from "../../components/icon";
trpc: any;
export interface IZapierSetupProps {
inviteLink: string;
} }
const ZAPIER = "zapier"; const ZAPIER = "zapier";
export default function ZapierSetup(props: IZapierSetupProps) { export default function ZapierSetup(props: IZapierSetupProps) {
const { trpc } = props;
const [newApiKey, setNewApiKey] = useState(""); const [newApiKey, setNewApiKey] = useState("");
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const integrations = trpc.useQuery(["viewer.integrations"]); const integrations = trpc.useQuery(["viewer.integrations"]);
// @ts-ignore
const oldApiKey = trpc.useQuery(["viewer.apiKeys.findKeyOfType", { appId: ZAPIER }]); const oldApiKey = trpc.useQuery(["viewer.apiKeys.findKeyOfType", { appId: ZAPIER }]);
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete"); const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete");
const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.other?.items.find( const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.other?.items.find(
(item: { type: string }) => item.type === "zapier_other" (item: { type: string }) => item.type === "zapier_other"
@ -33,6 +36,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
async function createApiKey() { async function createApiKey() {
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER }; const event = { note: "Zapier", expiresAt: null, appId: ZAPIER };
// @ts-ignore
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event); const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
if (oldApiKey.data) { if (oldApiKey.data) {
deleteApiKey.mutate({ deleteApiKey.mutate({
@ -91,8 +95,14 @@ export default function ZapierSetup(props: IZapierSetupProps) {
</> </>
)} )}
<ol className="mt-5 mb-5 mr-5 list-decimal"> <ol className="mt-5 mb-5 ml-5 mr-5 list-decimal">
<Trans i18nKey="zapier_setup_instructions"> <Trans i18nKey="zapier_setup_instructions">
<li>
Go to:
<a href={props.inviteLink} className="text-orange-600 underline">
Zapier Invite Link
</a>
</li>
<li>Log into your Zapier account and create a new Zap.</li> <li>Log into your Zapier account and create a new Zap.</li>
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li> <li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
<li>Choose your account and then enter your Unique API Key.</li> <li>Choose your account and then enter your Unique API Key.</li>
@ -116,6 +126,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
</div> </div>
</div> </div>
)} )}
<Toaster position="bottom-right" />
</div> </div>
); );
} }

View File

@ -95,7 +95,12 @@ async function main() {
webhook_secret: process.env.VITAL_WEBHOOK_SECRET, 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 // Web3 apps
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video"); await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
await createApp("metamask", "metamask", ["web3"], "metamask_web3"); await createApp("metamask", "metamask", ["web3"], "metamask_web3");

7
packages/ui/Loader.tsx Normal file
View File

@ -0,0 +1,7 @@
export default function Loader() {
return (
<div className="loader border-brand dark:border-darkmodebrand">
<span className="loader-inner bg-brand dark:bg-darkmodebrand"></span>
</div>
);
}

View File

@ -1,6 +1,7 @@
export { default as Button } from "./Button"; export { default as Button } from "./Button";
export { default as EmptyScreen } from "./EmptyScreen"; export { default as EmptyScreen } from "./EmptyScreen";
export { default as Select } from "./form/Select"; export { default as Select } from "./form/Select";
export { default as Loader } from "./Loader";
export * from "./skeleton"; export * from "./skeleton";
export { default as Switch } from "./Switch"; export { default as Switch } from "./Switch";
export { default as Tooltip } from "./Tooltip"; export { default as Tooltip } from "./Tooltip";