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"
# "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=""
# *********************************************************************************************************

View File

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

View File

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

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",
"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.</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.",
"go_to_app_store": "Go to App Store",
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",

View File

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

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

View File

@ -2,5 +2,4 @@ Workflow automation for everyone. Use the Cal.com Zapier app to trigger your wor
<br />
**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(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 ZapierSetup } from "./zapierSetup";
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 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) {
</>
)}
<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">
<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>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>
@ -116,6 +126,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
</div>
</div>
)}
<Toaster position="bottom-right" />
</div>
);
}

View File

@ -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");

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 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";