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:
parent
784a91709c
commit
6483182ef6
|
@ -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=""
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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}`)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 };
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -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} />;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue
Block a user