Tandem Video (#1671)
* Tandem Video * Updating some copy * adding some instructions for getting client id + secret * PR Feedback * removing spurious tsconfig file
This commit is contained in:
parent
dedf001237
commit
ae5d5e1261
27 changed files with 336 additions and 3 deletions
|
@ -55,6 +55,11 @@ ZOOM_CLIENT_SECRET=
|
||||||
DAILY_API_KEY=
|
DAILY_API_KEY=
|
||||||
DAILY_SCALE_PLAN=''
|
DAILY_SCALE_PLAN=''
|
||||||
|
|
||||||
|
# Used for the Tandem integration -- contact support@tandem.chat to for API access.
|
||||||
|
TANDEM_CLIENT_ID=""
|
||||||
|
TANDEM_CLIENT_SECRET=""
|
||||||
|
TANDEM_BASE_URL="https://tandem.chat"
|
||||||
|
|
||||||
# E-mail settings
|
# E-mail settings
|
||||||
|
|
||||||
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to
|
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to
|
||||||
|
|
|
@ -590,6 +590,12 @@ paths:
|
||||||
title: Zoom
|
title: Zoom
|
||||||
imageSrc: integrations/zoom.svg
|
imageSrc: integrations/zoom.svg
|
||||||
description: Video Conferencing
|
description: Video Conferencing
|
||||||
|
- installed: true
|
||||||
|
type: tandem_video
|
||||||
|
credential: null
|
||||||
|
title: Tandem
|
||||||
|
imageSrc: integrations/tandem.svg
|
||||||
|
description: Virtual Office | Video Conferencing
|
||||||
- installed: true
|
- installed: true
|
||||||
type: caldav_calendar
|
type: caldav_calendar
|
||||||
credential: null
|
credential: null
|
||||||
|
@ -753,6 +759,18 @@ paths:
|
||||||
summary: Gets and stores the OAuth token
|
summary: Gets and stores the OAuth token
|
||||||
tags:
|
tags:
|
||||||
- Integrations
|
- Integrations
|
||||||
|
/api/integrations/tandemvideo/add:
|
||||||
|
get:
|
||||||
|
description: Gets the OAuth URL for a Tandem integration.
|
||||||
|
summary: Gets the OAuth URL
|
||||||
|
tags:
|
||||||
|
- Integrations
|
||||||
|
/api/integrations/tandemvideo/callback:
|
||||||
|
post:
|
||||||
|
description: Gets and stores the OAuth token for a Tandem integration.
|
||||||
|
summary: Gets and stores the OAuth token
|
||||||
|
tags:
|
||||||
|
- Integrations
|
||||||
/api/user/profile:
|
/api/user/profile:
|
||||||
patch:
|
patch:
|
||||||
description: Updates a user's profile.
|
description: Updates a user's profile.
|
||||||
|
|
|
@ -145,6 +145,7 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
[LocationType.Zoom]: "Zoom Video",
|
[LocationType.Zoom]: "Zoom Video",
|
||||||
[LocationType.Daily]: "Daily.co Video",
|
[LocationType.Daily]: "Daily.co Video",
|
||||||
[LocationType.Huddle01]: "Huddle01 Video",
|
[LocationType.Huddle01]: "Huddle01 Video",
|
||||||
|
[LocationType.Tandem]: "Tandem Video",
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = () => {
|
const defaultValues = () => {
|
||||||
|
|
3
environment.d.ts
vendored
3
environment.d.ts
vendored
|
@ -26,5 +26,8 @@ declare namespace NodeJS {
|
||||||
readonly PAYMENT_FEE_FIXED: number | undefined;
|
readonly PAYMENT_FEE_FIXED: number | undefined;
|
||||||
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
|
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
|
||||||
readonly NEXT_PUBLIC_INTERCOM_APP_ID: string | undefined;
|
readonly NEXT_PUBLIC_INTERCOM_APP_ID: string | undefined;
|
||||||
|
readonly TANDEM_CLIENT_ID: string | undefined;
|
||||||
|
readonly TANDEM_CLIENT_SECRET: string | undefined;
|
||||||
|
readonly TANDEM_BASE_URL: string | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,8 +53,12 @@ export const isHuddle01 = (location: string): boolean => {
|
||||||
return location === "integrations:huddle01";
|
return location === "integrations:huddle01";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isTandem = (location: string): boolean => {
|
||||||
|
return location === "integrations:tandem";
|
||||||
|
};
|
||||||
|
|
||||||
export const isDedicatedIntegration = (location: string): boolean => {
|
export const isDedicatedIntegration = (location: string): boolean => {
|
||||||
return isZoom(location) || isDaily(location) || isHuddle01(location);
|
return isZoom(location) || isDaily(location) || isHuddle01(location) || isTandem(location);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLocationRequestFromIntegration = (location: string) => {
|
export const getLocationRequestFromIntegration = (location: string) => {
|
||||||
|
@ -62,7 +66,8 @@ export const getLocationRequestFromIntegration = (location: string) => {
|
||||||
location === LocationType.GoogleMeet.valueOf() ||
|
location === LocationType.GoogleMeet.valueOf() ||
|
||||||
location === LocationType.Zoom.valueOf() ||
|
location === LocationType.Zoom.valueOf() ||
|
||||||
location === LocationType.Daily.valueOf() ||
|
location === LocationType.Daily.valueOf() ||
|
||||||
location === LocationType.Huddle01.valueOf()
|
location === LocationType.Huddle01.valueOf() ||
|
||||||
|
location === LocationType.Tandem.valueOf()
|
||||||
) {
|
) {
|
||||||
const requestId = uuidv5(location, uuidv5.URL);
|
const requestId = uuidv5(location, uuidv5.URL);
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ export function getIntegrationName(name: string) {
|
||||||
return "Daily";
|
return "Daily";
|
||||||
case "huddle01_video":
|
case "huddle01_video":
|
||||||
return "Huddle01";
|
return "Huddle01";
|
||||||
|
case "tandem_video":
|
||||||
|
return "Tandem";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
143
lib/integrations/Tandem/TandemVideoApiAdapter.ts
Normal file
143
lib/integrations/Tandem/TandemVideoApiAdapter.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { Credential } from "@prisma/client";
|
||||||
|
|
||||||
|
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||||
|
import { PartialReference } from "@lib/events/EventManager";
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||||
|
|
||||||
|
import { CalendarEvent } from "../calendar/interfaces/Calendar";
|
||||||
|
|
||||||
|
interface TandemToken {
|
||||||
|
expires_in?: number;
|
||||||
|
expiry_date: number;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: "Bearer";
|
||||||
|
access_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_id = process.env.TANDEM_CLIENT_ID as string;
|
||||||
|
const client_secret = process.env.TANDEM_CLIENT_SECRET as string;
|
||||||
|
const TANDEM_BASE_URL = process.env.TANDEM_BASE_URL as string;
|
||||||
|
|
||||||
|
const tandemAuth = (credential: Credential) => {
|
||||||
|
const credentialKey = credential.key as unknown as TandemToken;
|
||||||
|
const isTokenValid = (token: TandemToken) => token && token.access_token && token.expiry_date < Date.now();
|
||||||
|
|
||||||
|
const refreshAccessToken = (refreshToken: string) => {
|
||||||
|
fetch(`${TANDEM_BASE_URL}/api/v1/oauth/v2/token`, {
|
||||||
|
method: "POST",
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
code: refreshToken,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(handleErrorsJson)
|
||||||
|
.then(async (responseBody) => {
|
||||||
|
// set expiry date as offset from current time.
|
||||||
|
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||||
|
delete responseBody.expires_in;
|
||||||
|
// Store new tokens in database.
|
||||||
|
await prisma.credential.update({
|
||||||
|
where: {
|
||||||
|
id: credential.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
key: responseBody,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
credentialKey.expiry_date = responseBody.expiry_date;
|
||||||
|
credentialKey.access_token = responseBody.access_token;
|
||||||
|
credentialKey.refresh_token = responseBody.refresh_token;
|
||||||
|
return credentialKey.access_token;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getToken: () =>
|
||||||
|
!isTokenValid(credentialKey)
|
||||||
|
? Promise.resolve(credentialKey.access_token)
|
||||||
|
: refreshAccessToken(credentialKey.refresh_token),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TandemVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
||||||
|
const auth = tandemAuth(credential);
|
||||||
|
|
||||||
|
const _parseDate = (date: string) => {
|
||||||
|
return Date.parse(date) / 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _translateEvent = (event: CalendarEvent, param: string): string => {
|
||||||
|
return JSON.stringify({
|
||||||
|
[param]: {
|
||||||
|
title: event.title,
|
||||||
|
starts_at: _parseDate(event.startTime),
|
||||||
|
ends_at: _parseDate(event.endTime),
|
||||||
|
description: event.description || "",
|
||||||
|
conference_solution: "tandem",
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _translateResult = (result: { data: { id: string; event_link: string } }) => {
|
||||||
|
return {
|
||||||
|
type: "tandem_video",
|
||||||
|
id: result.data.id as string,
|
||||||
|
password: "",
|
||||||
|
url: result.data.event_link,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Tandem doesn't need to return busy times, so we return empty */
|
||||||
|
getAvailability: () => {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
|
||||||
|
const accessToken = await auth.getToken();
|
||||||
|
|
||||||
|
const result = await fetch(`${TANDEM_BASE_URL}/api/v1/meetings`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: _translateEvent(event, "meeting"),
|
||||||
|
}).then(handleErrorsJson);
|
||||||
|
|
||||||
|
return Promise.resolve(_translateResult(result));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteMeeting: async (uid: string): Promise<void> => {
|
||||||
|
const accessToken = await auth.getToken();
|
||||||
|
|
||||||
|
await fetch(`${TANDEM_BASE_URL}/api/v1/meetings/${uid}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
},
|
||||||
|
}).then(handleErrorsRaw);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> => {
|
||||||
|
const accessToken = await auth.getToken();
|
||||||
|
|
||||||
|
const result = await fetch(`${TANDEM_BASE_URL}/api/v1/meetings/${bookingRef.meetingId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: _translateEvent(event, "updates"),
|
||||||
|
}).then(handleErrorsJson);
|
||||||
|
|
||||||
|
return Promise.resolve(_translateResult(result));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TandemVideoApiAdapter;
|
|
@ -20,6 +20,7 @@ export type Integration = {
|
||||||
| "office365_calendar"
|
| "office365_calendar"
|
||||||
| "zoom_video"
|
| "zoom_video"
|
||||||
| "daily_video"
|
| "daily_video"
|
||||||
|
| "tandem_video"
|
||||||
| "caldav_calendar"
|
| "caldav_calendar"
|
||||||
| "apple_calendar"
|
| "apple_calendar"
|
||||||
| "stripe_payment"
|
| "stripe_payment"
|
||||||
|
@ -72,6 +73,14 @@ export const ALL_INTEGRATIONS = [
|
||||||
description: "Video Conferencing",
|
description: "Video Conferencing",
|
||||||
variant: "conferencing",
|
variant: "conferencing",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
installed: !!(process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET),
|
||||||
|
type: "tandem_video",
|
||||||
|
title: "Tandem Video",
|
||||||
|
imageSrc: "integrations/tandem.svg",
|
||||||
|
description: "Virtual Office | Video Conferencing",
|
||||||
|
variant: "conferencing",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
installed: true,
|
installed: true,
|
||||||
type: "caldav_calendar",
|
type: "caldav_calendar",
|
||||||
|
|
|
@ -5,4 +5,5 @@ export enum LocationType {
|
||||||
Zoom = "integrations:zoom",
|
Zoom = "integrations:zoom",
|
||||||
Daily = "integrations:daily",
|
Daily = "integrations:daily",
|
||||||
Huddle01 = "integrations:huddle01",
|
Huddle01 = "integrations:huddle01",
|
||||||
|
Tandem = "integrations:tandem",
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApi
|
||||||
import logger from "@lib/logger";
|
import logger from "@lib/logger";
|
||||||
|
|
||||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||||
|
import TandemVideoApiAdapter from "./integrations/Tandem/TandemVideoApiAdapter";
|
||||||
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
||||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
||||||
case "huddle01_video":
|
case "huddle01_video":
|
||||||
acc.push(Huddle01VideoApiAdapter());
|
acc.push(Huddle01VideoApiAdapter());
|
||||||
break;
|
break;
|
||||||
|
case "tandem_video":
|
||||||
|
acc.push(TandemVideoApiAdapter(cred));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
42
pages/api/integrations/tandemvideo/add.ts
Normal file
42
pages/api/integrations/tandemvideo/add.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { stringify } from "querystring";
|
||||||
|
|
||||||
|
import { getSession } from "@lib/auth";
|
||||||
|
import { BASE_URL } from "@lib/config/constants";
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
const client_id = process.env.TANDEM_CLIENT_ID;
|
||||||
|
const TANDEM_BASE_URL = process.env.TANDEM_BASE_URL;
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
// Check that user is authenticated
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
await prisma.user.findFirst({
|
||||||
|
rejectOnNotFound: true,
|
||||||
|
where: {
|
||||||
|
id: session?.user?.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const redirect_uri = encodeURI(BASE_URL + "/api/integrations/tandemvideo/callback");
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
client_id,
|
||||||
|
redirect_uri,
|
||||||
|
};
|
||||||
|
const query = stringify(params);
|
||||||
|
const url = `${TANDEM_BASE_URL}/oauth/approval?${query}`;
|
||||||
|
res.status(200).json({ url });
|
||||||
|
}
|
||||||
|
}
|
56
pages/api/integrations/tandemvideo/callback.ts
Normal file
56
pages/api/integrations/tandemvideo/callback.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { getSession } from "@lib/auth";
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
const client_id = process.env.TANDEM_CLIENT_ID as string;
|
||||||
|
const client_secret = process.env.TANDEM_CLIENT_SECRET as string;
|
||||||
|
const TANDEM_BASE_URL = (process.env.TANDEM_BASE_URL as string) || "https://tandem.chat";
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.query.code) {
|
||||||
|
res.status(401).json({ message: "Missing code" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = req.query.code as string;
|
||||||
|
|
||||||
|
// Check that user is authenticated
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetch(`${TANDEM_BASE_URL}/api/v1/oauth/v2/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code, client_id, client_secret }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseBody = await result.json();
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||||
|
delete responseBody.expires_in;
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
credentials: {
|
||||||
|
create: {
|
||||||
|
type: "tandem_video",
|
||||||
|
key: responseBody,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect("/integrations");
|
||||||
|
}
|
|
@ -263,6 +263,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
|
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
|
||||||
case LocationType.Huddle01:
|
case LocationType.Huddle01:
|
||||||
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
||||||
|
case LocationType.Tandem:
|
||||||
|
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -522,6 +524,30 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<span className="ltr:ml-2 rtl:mr-2text-sm">Zoom Video</span>
|
<span className="ltr:ml-2 rtl:mr-2text-sm">Zoom Video</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{location.type === LocationType.Tandem && (
|
||||||
|
<div className="flex items-center flex-grow">
|
||||||
|
<svg
|
||||||
|
width="1.25em"
|
||||||
|
height="1.25em"
|
||||||
|
viewBox="0 0 400 400"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M167.928 256.163L64 324V143.835L167.928 76V256.163Z"
|
||||||
|
fill="#4341DC"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M335.755 256.163L231.827 324V143.835L335.755 76V256.163Z"
|
||||||
|
fill="#00B6B6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="ml-2 text-sm">Tandem Video</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1592,6 +1618,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
if (hasIntegration(integrations, "huddle01_video")) {
|
if (hasIntegration(integrations, "huddle01_video")) {
|
||||||
locationOptions.push({ value: LocationType.Huddle01, label: "Huddle01 Video" });
|
locationOptions.push({ value: LocationType.Huddle01, label: "Huddle01 Video" });
|
||||||
}
|
}
|
||||||
|
if (hasIntegration(integrations, "tandem_video")) {
|
||||||
|
locationOptions.push({ value: LocationType.Tandem, label: "Tandem Video" });
|
||||||
|
}
|
||||||
const currency =
|
const currency =
|
||||||
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
|
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
|
||||||
?.default_currency || "usd";
|
?.default_currency || "usd";
|
||||||
|
|
4
public/integrations/tandem.svg
Normal file
4
public/integrations/tandem.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M167.928 256.163L64 324V143.835L167.928 76V256.163Z" fill="#4341DC"/>
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M335.755 256.163L231.827 324V143.835L335.755 76V256.163Z" fill="#00B6B6"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 346 B |
|
@ -543,6 +543,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal wird Ihren Teilnehmer bitten, vor der Planung eine Telefonnummer einzugeben.",
|
"cal_invitee_phone_number_scheduling": "Cal wird Ihren Teilnehmer bitten, vor der Planung eine Telefonnummer einzugeben.",
|
||||||
"cal_provide_google_meet_location": "Cal wird einen Google Meet Termin zur Verfügung stellen.",
|
"cal_provide_google_meet_location": "Cal wird einen Google Meet Termin zur Verfügung stellen.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal stellt eine Zoom Meeting-URL zur Verfügung.",
|
"cal_provide_zoom_meeting_url": "Cal stellt eine Zoom Meeting-URL zur Verfügung.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal stellt eine Tandem Meeting-URL zur Verfügung.",
|
||||||
"cal_provide_video_meeting_url": "Cal stellt eine tägliche Video-Meeting-URL zur Verfügung.",
|
"cal_provide_video_meeting_url": "Cal stellt eine tägliche Video-Meeting-URL zur Verfügung.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal stellt eine tägliche Huddle01-Web3-Meeting-URL zur Verfügung.",
|
"cal_provide_huddle01_meeting_url": "Cal stellt eine tägliche Huddle01-Web3-Meeting-URL zur Verfügung.",
|
||||||
"require_payment": "Zahlung erforderlich",
|
"require_payment": "Zahlung erforderlich",
|
||||||
|
|
|
@ -543,6 +543,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal will ask your invitee to enter a phone number before scheduling.",
|
"cal_invitee_phone_number_scheduling": "Cal will ask your invitee to enter a phone number before scheduling.",
|
||||||
"cal_provide_google_meet_location": "Cal will provide a Google Meet location.",
|
"cal_provide_google_meet_location": "Cal will provide a Google Meet location.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal will provide a Zoom meeting URL.",
|
"cal_provide_zoom_meeting_url": "Cal will provide a Zoom meeting URL.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal will provide a Tandem meeting URL.",
|
||||||
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
|
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
||||||
"require_payment": "Require Payment",
|
"require_payment": "Require Payment",
|
||||||
|
|
|
@ -523,6 +523,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal le pedirá a su invitado que introduzca un número de teléfono antes de programar.",
|
"cal_invitee_phone_number_scheduling": "Cal le pedirá a su invitado que introduzca un número de teléfono antes de programar.",
|
||||||
"cal_provide_google_meet_location": "Cal proporcionará una URL de reunión de Google Meet.",
|
"cal_provide_google_meet_location": "Cal proporcionará una URL de reunión de Google Meet.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal proporcionará una URL de reunión de Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal proporcionará una URL de reunión de Zoom.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal proporcionará una URL de reunión de Tandem.",
|
||||||
"cal_provide_video_meeting_url": "Cal proporcionará una URL de reunión de Daily Video.",
|
"cal_provide_video_meeting_url": "Cal proporcionará una URL de reunión de Daily Video.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal proporcionará una URL de reunión de Huddle01 Web3 Video.",
|
"cal_provide_huddle01_meeting_url": "Cal proporcionará una URL de reunión de Huddle01 Web3 Video.",
|
||||||
"require_payment": "Requiere Pago",
|
"require_payment": "Requiere Pago",
|
||||||
|
|
|
@ -489,6 +489,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal demandera à votre invité. e d'entrer un numéro de téléphone avant de faire une réservation.",
|
"cal_invitee_phone_number_scheduling": "Cal demandera à votre invité. e d'entrer un numéro de téléphone avant de faire une réservation.",
|
||||||
"cal_provide_google_meet_location": "Cal fournira un lien Google Meet.",
|
"cal_provide_google_meet_location": "Cal fournira un lien Google Meet.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal fournira une URL de réunion Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal fournira une URL de réunion Zoom.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal fournira une URL de réunion Tandem.",
|
||||||
"cal_provide_video_meeting_url": "Cal fournira une URL de réunion Daily video.",
|
"cal_provide_video_meeting_url": "Cal fournira une URL de réunion Daily video.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal fournira une URL de réunion Huddle01 web3 video.",
|
"cal_provide_huddle01_meeting_url": "Cal fournira une URL de réunion Huddle01 web3 video.",
|
||||||
"require_payment": "Exiger un paiement",
|
"require_payment": "Exiger un paiement",
|
||||||
|
|
|
@ -517,6 +517,7 @@
|
||||||
"cal_provide_zoom_meeting_url": "Cal fornirà un URL di riunione Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal fornirà un URL di riunione Zoom.",
|
||||||
"cal_provide_video_meeting_url": "Cal fornirà un URL di riunione Daily video.",
|
"cal_provide_video_meeting_url": "Cal fornirà un URL di riunione Daily video.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal fornirà un URL di riunione Huddle01 web3 video.",
|
"cal_provide_huddle01_meeting_url": "Cal fornirà un URL di riunione Huddle01 web3 video.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal fornirà un URL di riunione Tandem.",
|
||||||
"require_payment": "Richiedi Pagamento",
|
"require_payment": "Richiedi Pagamento",
|
||||||
"commission_per_transaction": "commissione per transazione",
|
"commission_per_transaction": "commissione per transazione",
|
||||||
"event_type_updated_successfully_description": "Il tuo team è stato aggiornato con successo.",
|
"event_type_updated_successfully_description": "Il tuo team è stato aggiornato con successo.",
|
||||||
|
|
|
@ -487,6 +487,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Calは、予約する前に招待者に電話番号を入力するように依頼します。",
|
"cal_invitee_phone_number_scheduling": "Calは、予約する前に招待者に電話番号を入力するように依頼します。",
|
||||||
"cal_provide_google_meet_location": "CalはGoogleMeetの場所を提供します。",
|
"cal_provide_google_meet_location": "CalはGoogleMeetの場所を提供します。",
|
||||||
"cal_provide_zoom_meeting_url": "カルはZoomミーティングURLを提供します。",
|
"cal_provide_zoom_meeting_url": "カルはZoomミーティングURLを提供します。",
|
||||||
|
"cal_provide_tandem_meeting_url": "カルはTandemミーティングURLを提供します。",
|
||||||
"cal_provide_video_meeting_url": "カルは毎日のビデオミーティングのURLを提供します。",
|
"cal_provide_video_meeting_url": "カルは毎日のビデオミーティングのURLを提供します。",
|
||||||
"cal_provide_huddle01_meeting_url": "カルはHuddle01 Web3ミーティングURLを提供します。",
|
"cal_provide_huddle01_meeting_url": "カルはHuddle01 Web3ミーティングURLを提供します。",
|
||||||
"require_payment": "お支払いが必要です",
|
"require_payment": "お支払いが必要です",
|
||||||
|
|
|
@ -510,6 +510,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal은 일정을 잡기 전에 초대받은 사람에게 전화번호를 요청합니다.",
|
"cal_invitee_phone_number_scheduling": "Cal은 일정을 잡기 전에 초대받은 사람에게 전화번호를 요청합니다.",
|
||||||
"cal_provide_google_meet_location": "Cal은 Google Meet 위치를 제공합니다.",
|
"cal_provide_google_meet_location": "Cal은 Google Meet 위치를 제공합니다.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal은 Zoom 회의 URL을 제공합니다.",
|
"cal_provide_zoom_meeting_url": "Cal은 Zoom 회의 URL을 제공합니다.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal은 Tandem 회의 URL을 제공합니다.",
|
||||||
"cal_provide_video_meeting_url": "Cal은 일일 화상 회의 URL을 제공합니다.",
|
"cal_provide_video_meeting_url": "Cal은 일일 화상 회의 URL을 제공합니다.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal은 Huddle01 Web3 회의 URL을 제공합니다.",
|
"cal_provide_huddle01_meeting_url": "Cal은 Huddle01 Web3 회의 URL을 제공합니다.",
|
||||||
"require_payment": "지불 요청",
|
"require_payment": "지불 요청",
|
||||||
|
|
|
@ -480,6 +480,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal zal de genodigde om een telefoonnummer vragen.",
|
"cal_invitee_phone_number_scheduling": "Cal zal de genodigde om een telefoonnummer vragen.",
|
||||||
"cal_provide_google_meet_location": "Cal zal een Google Meet meeting-URL meegeven in de afspraak bevestiging.",
|
"cal_provide_google_meet_location": "Cal zal een Google Meet meeting-URL meegeven in de afspraak bevestiging.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal zal een Zoom meeting-URL meegeven in de afspraak bevestiging.",
|
"cal_provide_zoom_meeting_url": "Cal zal een Zoom meeting-URL meegeven in de afspraak bevestiging.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal zal een Tandem meeting-URL meegeven in de afspraak bevestiging.",
|
||||||
"cal_provide_video_meeting_url": "Cal zal een Daily meeting-URL meegeven in de afspraak bevestiging.",
|
"cal_provide_video_meeting_url": "Cal zal een Daily meeting-URL meegeven in de afspraak bevestiging.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal zal een Huddle01 web3 meeting-URL meegeven in de afspraak bevestiging.",
|
"cal_provide_huddle01_meeting_url": "Cal zal een Huddle01 web3 meeting-URL meegeven in de afspraak bevestiging.",
|
||||||
"require_payment": "Betaling vereisen",
|
"require_payment": "Betaling vereisen",
|
||||||
|
|
|
@ -526,6 +526,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal solicitará ao seu convidado que insira um número de telefone antes de agendar.",
|
"cal_invitee_phone_number_scheduling": "Cal solicitará ao seu convidado que insira um número de telefone antes de agendar.",
|
||||||
"cal_provide_google_meet_location": "Cal fornecerá um link para o Google Meet.",
|
"cal_provide_google_meet_location": "Cal fornecerá um link para o Google Meet.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião do Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião do Zoom.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal fornecerá uma URL de reunião do Tandem.",
|
||||||
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
||||||
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião do Huddle01 Web3 video.",
|
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião do Huddle01 Web3 video.",
|
||||||
"require_payment": "Requerer Pagamento",
|
"require_payment": "Requerer Pagamento",
|
||||||
|
|
|
@ -543,6 +543,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal solicitará ao seu convidado que insira um número de telefone antes de agendar.",
|
"cal_invitee_phone_number_scheduling": "Cal solicitará ao seu convidado que insira um número de telefone antes de agendar.",
|
||||||
"cal_provide_google_meet_location": "Cal fornecerá um local para o Google Meet.",
|
"cal_provide_google_meet_location": "Cal fornecerá um local para o Google Meet.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião Zoom .",
|
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião Zoom .",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal fornecerá uma URL de reunião Tandem .",
|
||||||
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
||||||
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Huddle01 Web3.",
|
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Huddle01 Web3.",
|
||||||
"require_payment": "Requer Pagamento",
|
"require_payment": "Requer Pagamento",
|
||||||
|
|
|
@ -489,6 +489,7 @@
|
||||||
"cal_provide_zoom_meeting_url": "Cal va oferi un URL pentru ședința de Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal va oferi un URL pentru ședința de Zoom.",
|
||||||
"cal_provide_video_meeting_url": "Cal va oferi un URL pentru ședința de Daily.",
|
"cal_provide_video_meeting_url": "Cal va oferi un URL pentru ședința de Daily.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal va oferi un URL pentru ședința de Huddle01 Web3.",
|
"cal_provide_huddle01_meeting_url": "Cal va oferi un URL pentru ședința de Huddle01 Web3.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal va oferi un URL pentru ședința de Tandem.",
|
||||||
"require_payment": "Solicită plata",
|
"require_payment": "Solicită plata",
|
||||||
"commission_per_transaction": "comision per tranzacție",
|
"commission_per_transaction": "comision per tranzacție",
|
||||||
"event_type_updated_successfully_description": "Tipul de eveniment a fost actualizat cu succes.",
|
"event_type_updated_successfully_description": "Tipul de eveniment a fost actualizat cu succes.",
|
||||||
|
|
|
@ -527,6 +527,7 @@
|
||||||
"cal_invitee_phone_number_scheduling": "Cal попросит участника указать номер телефона перед началом бронирования.",
|
"cal_invitee_phone_number_scheduling": "Cal попросит участника указать номер телефона перед началом бронирования.",
|
||||||
"cal_provide_google_meet_location": "Cal создаст ссылку на встречу в Google Meet.",
|
"cal_provide_google_meet_location": "Cal создаст ссылку на встречу в Google Meet.",
|
||||||
"cal_provide_zoom_meeting_url": "Cal создаст ссылку на встречу в Zoom.",
|
"cal_provide_zoom_meeting_url": "Cal создаст ссылку на встречу в Zoom.",
|
||||||
|
"cal_provide_tandem_meeting_url": "Cal создаст ссылку на встречу в Tandem.",
|
||||||
"cal_provide_video_meeting_url": "Cal создаст ссылку на встречу в Daily.",
|
"cal_provide_video_meeting_url": "Cal создаст ссылку на встречу в Daily.",
|
||||||
"cal_provide_huddle01_meeting_url": "Cal создаст ссылку на встречу в Huddle01 Web3.",
|
"cal_provide_huddle01_meeting_url": "Cal создаст ссылку на встречу в Huddle01 Web3.",
|
||||||
"require_payment": "Требуется оплата",
|
"require_payment": "Требуется оплата",
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue