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_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
|
||||
|
||||
# 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
|
||||
imageSrc: integrations/zoom.svg
|
||||
description: Video Conferencing
|
||||
- installed: true
|
||||
type: tandem_video
|
||||
credential: null
|
||||
title: Tandem
|
||||
imageSrc: integrations/tandem.svg
|
||||
description: Virtual Office | Video Conferencing
|
||||
- installed: true
|
||||
type: caldav_calendar
|
||||
credential: null
|
||||
|
@ -753,6 +759,18 @@ paths:
|
|||
summary: Gets and stores the OAuth token
|
||||
tags:
|
||||
- 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:
|
||||
patch:
|
||||
description: Updates a user's profile.
|
||||
|
|
|
@ -145,6 +145,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
[LocationType.Zoom]: "Zoom Video",
|
||||
[LocationType.Daily]: "Daily.co Video",
|
||||
[LocationType.Huddle01]: "Huddle01 Video",
|
||||
[LocationType.Tandem]: "Tandem Video",
|
||||
};
|
||||
|
||||
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 CALENDSO_ENCRYPTION_KEY: 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";
|
||||
};
|
||||
|
||||
export const isTandem = (location: string): boolean => {
|
||||
return location === "integrations:tandem";
|
||||
};
|
||||
|
||||
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) => {
|
||||
|
@ -62,7 +66,8 @@ export const getLocationRequestFromIntegration = (location: string) => {
|
|||
location === LocationType.GoogleMeet.valueOf() ||
|
||||
location === LocationType.Zoom.valueOf() ||
|
||||
location === LocationType.Daily.valueOf() ||
|
||||
location === LocationType.Huddle01.valueOf()
|
||||
location === LocationType.Huddle01.valueOf() ||
|
||||
location === LocationType.Tandem.valueOf()
|
||||
) {
|
||||
const requestId = uuidv5(location, uuidv5.URL);
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ export function getIntegrationName(name: string) {
|
|||
return "Daily";
|
||||
case "huddle01_video":
|
||||
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"
|
||||
| "zoom_video"
|
||||
| "daily_video"
|
||||
| "tandem_video"
|
||||
| "caldav_calendar"
|
||||
| "apple_calendar"
|
||||
| "stripe_payment"
|
||||
|
@ -72,6 +73,14 @@ export const ALL_INTEGRATIONS = [
|
|||
description: "Video 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,
|
||||
type: "caldav_calendar",
|
||||
|
|
|
@ -5,4 +5,5 @@ export enum LocationType {
|
|||
Zoom = "integrations:zoom",
|
||||
Daily = "integrations:daily",
|
||||
Huddle01 = "integrations:huddle01",
|
||||
Tandem = "integrations:tandem",
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApi
|
|||
import logger from "@lib/logger";
|
||||
|
||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||
import TandemVideoApiAdapter from "./integrations/Tandem/TandemVideoApiAdapter";
|
||||
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||
|
||||
|
@ -48,6 +49,9 @@ const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
|||
case "huddle01_video":
|
||||
acc.push(Huddle01VideoApiAdapter());
|
||||
break;
|
||||
case "tandem_video":
|
||||
acc.push(TandemVideoApiAdapter(cred));
|
||||
break;
|
||||
default:
|
||||
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>;
|
||||
case LocationType.Huddle01:
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
@ -522,6 +524,30 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<span className="ltr:ml-2 rtl:mr-2text-sm">Zoom Video</span>
|
||||
</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">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1592,6 +1618,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
if (hasIntegration(integrations, "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 =
|
||||
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
|
||||
?.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_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_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_huddle01_meeting_url": "Cal stellt eine tägliche Huddle01-Web3-Meeting-URL zur Verfügung.",
|
||||
"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_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_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_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
||||
"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_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_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_huddle01_meeting_url": "Cal proporcionará una URL de reunión de Huddle01 Web3 Video.",
|
||||
"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_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_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_huddle01_meeting_url": "Cal fournira une URL de réunion Huddle01 web3 video.",
|
||||
"require_payment": "Exiger un paiement",
|
||||
|
|
|
@ -517,6 +517,7 @@
|
|||
"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_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",
|
||||
"commission_per_transaction": "commissione per transazione",
|
||||
"event_type_updated_successfully_description": "Il tuo team è stato aggiornato con successo.",
|
||||
|
|
|
@ -487,6 +487,7 @@
|
|||
"cal_invitee_phone_number_scheduling": "Calは、予約する前に招待者に電話番号を入力するように依頼します。",
|
||||
"cal_provide_google_meet_location": "CalはGoogleMeetの場所を提供します。",
|
||||
"cal_provide_zoom_meeting_url": "カルはZoomミーティングURLを提供します。",
|
||||
"cal_provide_tandem_meeting_url": "カルはTandemミーティングURLを提供します。",
|
||||
"cal_provide_video_meeting_url": "カルは毎日のビデオミーティングのURLを提供します。",
|
||||
"cal_provide_huddle01_meeting_url": "カルはHuddle01 Web3ミーティングURLを提供します。",
|
||||
"require_payment": "お支払いが必要です",
|
||||
|
|
|
@ -510,6 +510,7 @@
|
|||
"cal_invitee_phone_number_scheduling": "Cal은 일정을 잡기 전에 초대받은 사람에게 전화번호를 요청합니다.",
|
||||
"cal_provide_google_meet_location": "Cal은 Google Meet 위치를 제공합니다.",
|
||||
"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_huddle01_meeting_url": "Cal은 Huddle01 Web3 회의 URL을 제공합니다.",
|
||||
"require_payment": "지불 요청",
|
||||
|
|
|
@ -480,6 +480,7 @@
|
|||
"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_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_huddle01_meeting_url": "Cal zal een Huddle01 web3 meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"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_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_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_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião do Huddle01 Web3 video.",
|
||||
"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_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_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_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Huddle01 Web3.",
|
||||
"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_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_tandem_meeting_url": "Cal va oferi un URL pentru ședința de Tandem.",
|
||||
"require_payment": "Solicită plata",
|
||||
"commission_per_transaction": "comision per tranzacție",
|
||||
"event_type_updated_successfully_description": "Tipul de eveniment a fost actualizat cu succes.",
|
||||
|
|
|
@ -527,6 +527,7 @@
|
|||
"cal_invitee_phone_number_scheduling": "Cal попросит участника указать номер телефона перед началом бронирования.",
|
||||
"cal_provide_google_meet_location": "Cal создаст ссылку на встречу в Google Meet.",
|
||||
"cal_provide_zoom_meeting_url": "Cal создаст ссылку на встречу в Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal создаст ссылку на встречу в Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal создаст ссылку на встречу в Daily.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal создаст ссылку на встречу в Huddle01 Web3.",
|
||||
"require_payment": "Требуется оплата",
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue