From 75aef0933827587e9396f067d15cc758af3abb9b Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Mon, 9 Jan 2023 21:01:57 -0500 Subject: [PATCH] Google Meet - installable app (#5904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Abstract app category navigation * Send key schema to frontend Co-authored-by: Omar López * Render keys for apps on admin * Add enabled col for apps * Save app keys to DB * Add checks for admin role * Abstract setup components * Add AdminAppsList to setup wizard * Migrate to v10 tRPC * Default hide keys * Display enabled apps * Merge branch 'main' into admin-apps-ui * Toggle calendars * WIP * Add params and include AppCategoryNavigation * Refactor getEnabledApps * Add warning for disabling apps * Fallback to cal video when a video app is disabled * WIP send disabled email * Send email to all users of event types with payment app * Disable Stripe when app is disabled * Disable apps in event types * Send email to users on disabled apps * Send email based on what app was disabled * WIP type fix * Disable navigation to apps list if already setup * UI import fixes * Waits for session data before redirecting * Updates admin seeded password To comply with admin password requirements * Update yarn.lock * Flex fixes * Adds admin middleware * Clean up * WIP * WIP * NTS * Add dirName to app metadata * Upsert app if not in db * Upsert app if not in db * Add dirName to app metadata * Add keys to app packages w/ keys * Merge with main * Toggle show keys & on enable * Fix empty keys * Fix lark calendar metadata * Fix some type errors * Fix Lark metadata & check for category when upserting * More type fixes * Fix types & add keys to google cal * WIP * WIP * WIP * More type fixes * Fix type errors * Fix type errors * More type fixes * More type fixes * More type fixes * Feedback * Fixes default value * Feedback * Migrate credential invalid col default value "false" * Upsert app on saving keys * Clean up * Validate app keys on frontend * Add nonempty to app keys schemas * Add web3 * Listlocale filter on categories / category * Grab app metadata via category or categories * Show empty screen if no apps are enabled * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Replace .nonempty() w/ .min(1) * Fix type error * Address feedback * Draft Google Meet install button * Add install button and warning dialog * WIP * WIP * Display warning when Meet is selected * Display Google Meet warning on email to organizer * Fix email * Fix type errors * Fix type error * Add connected account component * Add warning message * Address comments * Address feedback * Clean up & add MeetLocationType * Use useApp hook * Translate to new API approach * Remove console.log * Refactor * Fix missing backup Cal video link * WIP * Address feedback * Update submodules * Feedback * Submodule sync Co-authored-by: Omar López Co-authored-by: Peer Richelsen Co-authored-by: zomars Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen --- .../components/eventtype/EventSetupTab.tsx | 24 ++++++- apps/web/pages/api/email.ts | 22 ++++++- apps/web/pages/api/integrations/[...args].ts | 2 +- apps/web/pages/apps/[slug]/index.tsx | 8 ++- apps/web/public/static/locales/en/common.json | 9 ++- .../app-store/_utils/useAddAppMutation.ts | 8 ++- packages/app-store/apps.browser.generated.tsx | 1 + packages/app-store/apps.server.generated.ts | 1 + .../app-store/googlecalendar/api/callback.ts | 30 ++++++++- .../googlecalendar/lib/CalendarService.ts | 7 +- packages/app-store/googlevideo/DESCRIPTION.md | 2 + packages/app-store/googlevideo/_metadata.ts | 2 +- packages/app-store/googlevideo/api/_getAdd.ts | 40 ++++++++++++ packages/app-store/googlevideo/api/add.ts | 5 ++ packages/app-store/googlevideo/api/index.ts | 1 + .../components/ExistingGoogleCal.tsx | 43 +++++++++++++ .../components/InstallAppButton.tsx | 64 +++++++++++++++++++ .../app-store/googlevideo/components/index.ts | 1 + packages/app-store/locations.ts | 2 + packages/app-store/types.d.ts | 1 + packages/core/EventManager.ts | 9 ++- packages/emails/src/components/AppsStatus.tsx | 2 +- .../features/bookings/lib/handleNewBooking.ts | 34 ++++++++++ packages/trpc/server/routers/viewer/apps.tsx | 11 +++- 24 files changed, 312 insertions(+), 17 deletions(-) create mode 100644 packages/app-store/googlevideo/api/_getAdd.ts create mode 100644 packages/app-store/googlevideo/api/add.ts create mode 100644 packages/app-store/googlevideo/api/index.ts create mode 100644 packages/app-store/googlevideo/components/ExistingGoogleCal.tsx create mode 100644 packages/app-store/googlevideo/components/InstallAppButton.tsx create mode 100644 packages/app-store/googlevideo/components/index.ts diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index 82972e0e44..2d5f588fa4 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -1,16 +1,18 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { isValidPhoneNumber } from "libphonenumber-js"; +import { Trans } from "next-i18next"; +import Link from "next/link"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useState } from "react"; import { Controller, useForm, useFormContext } from "react-hook-form"; import { MultiValue } from "react-select"; import { z } from "zod"; -import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; +import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations"; import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui"; +import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import { slugify } from "@lib/slugify"; @@ -195,6 +197,23 @@ export const EventSetupTab = ( ); })} + {validLocations.some((location) => location.type === MeetLocationType) && ( +
+ + +

+ The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work. + Change it{" "} + + here. + {" "} + We will fall back to Cal video if you do not change it. +

+
+
+ )} {validLocations.length > 0 && validLocations.length !== locationOptions.length && (
  • + + + + + ); +} diff --git a/packages/app-store/googlevideo/components/index.ts b/packages/app-store/googlevideo/components/index.ts new file mode 100644 index 0000000000..0d6008d4ca --- /dev/null +++ b/packages/app-store/googlevideo/components/index.ts @@ -0,0 +1 @@ +export { default as InstallAppButton } from "./InstallAppButton"; diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 5789eb830e..47b50c8b5e 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -40,6 +40,8 @@ export type EventLocationType = DefaultEventLocationType | EventLocationTypeFrom export const DailyLocationType = "integrations:daily"; +export const MeetLocationType = "integrations:google:meet"; + export enum DefaultEventLocationTypeEnum { /** * Booker Address diff --git a/packages/app-store/types.d.ts b/packages/app-store/types.d.ts index 081887a80c..b586522960 100644 --- a/packages/app-store/types.d.ts +++ b/packages/app-store/types.d.ts @@ -7,6 +7,7 @@ import { ButtonProps } from "@calcom/ui"; export type IntegrationOAuthCallbackState = { returnTo: string; + installGoogleVideo?: boolean; }; export interface InstallAppButtonProps { diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 2a9c15888b..af95df3bc6 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; +import { MeetLocationType } from "@calcom/app-store/locations"; import getApps from "@calcom/app-store/utils"; import prisma from "@calcom/prisma"; import { Attendee } from "@calcom/prisma/client"; @@ -28,7 +29,7 @@ import { createEvent, updateEvent } from "./CalendarManager"; import { createMeeting, updateMeeting } from "./videoClient"; export const isDedicatedIntegration = (location: string): boolean => { - return location !== "integrations:google:meet" && location.includes("integrations:"); + return location !== MeetLocationType && location.includes("integrations:"); }; export const getLocationRequestFromIntegration = (location: string) => { @@ -102,9 +103,15 @@ export default class EventManager { const evt = processLocation(event); // Fallback to cal video if no location is set if (!evt.location) evt["location"] = "integrations:daily"; + + // Fallback to Cal Video if Google Meet is selected w/o a Google Cal + if (evt.location === MeetLocationType && evt.destinationCalendar?.integration !== "google_calendar") { + evt["location"] = "integrations:daily"; + } const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null; const results: Array>> = []; + // If and only if event type is a dedicated meeting, create a dedicated video meeting. if (isDedicated) { const result = await this.createVideoEvent(evt); diff --git a/packages/emails/src/components/AppsStatus.tsx b/packages/emails/src/components/AppsStatus.tsx index 67f43488ad..d7b35960e6 100644 --- a/packages/emails/src/components/AppsStatus.tsx +++ b/packages/emails/src/components/AppsStatus.tsx @@ -16,6 +16,7 @@ export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) =>
  • {status.appName}{" "} {status.success >= 1 && `✅ ${status.success > 1 ? `(x${status.success})` : ""}`} + {status.failures >= 1 && `❌ ${status.failures > 1 ? `(x${status.failures})` : ""}`} {status.warnings && status.warnings.length >= 1 && (
      {status.warnings.map((warning, i) => ( @@ -23,7 +24,6 @@ export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) => ))}
    )} - {status.failures >= 1 && `❌ ${status.failures > 1 ? `(x${status.failures})` : ""}`} {status.errors.length >= 1 && (
      {status.errors.map((error, i) => ( diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 77ba0b2d65..74afb2bece 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -14,7 +14,9 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; import z from "zod"; +import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata"; import { getLocationValueForDB, LocationObject } from "@calcom/app-store/locations"; +import { MeetLocationType } from "@calcom/app-store/locations"; import { handleEthSignature } from "@calcom/app-store/rainbow/utils/ethereum"; import { handlePayment } from "@calcom/app-store/stripepayment/lib/server"; import { getEventTypeAppData } from "@calcom/app-store/utils"; @@ -911,6 +913,38 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { const metadata: AdditionalInformation = {}; if (results.length) { + // Handle Google Meet results + // We use the original booking location since the evt location changes to daily + if (bookingLocation === MeetLocationType) { + const googleMeetResult = { + appName: GoogleMeetMetadata.name, + type: "conferencing", + uid: results[0].uid, + originalEvent: results[0].originalEvent, + }; + + const googleCalResult = results.find((result) => result.type === "google_calendar"); + + if (!googleCalResult) { + results.push({ + ...googleMeetResult, + success: false, + calWarnings: [tOrganizer("google_meet_warning")], + }); + } + + if (googleCalResult?.createdEvent?.hangoutLink) { + results.push({ + ...googleMeetResult, + success: true, + }); + } else if (googleCalResult && !googleCalResult.createdEvent?.hangoutLink) { + results.push({ + ...googleMeetResult, + success: false, + }); + } + } // TODO: Handle created event metadata more elegantly metadata.hangoutLink = results[0].createdEvent?.hangoutLink; metadata.conferenceData = results[0].createdEvent?.conferenceData; diff --git a/packages/trpc/server/routers/viewer/apps.tsx b/packages/trpc/server/routers/viewer/apps.tsx index f6097950dc..09f54bb453 100644 --- a/packages/trpc/server/routers/viewer/apps.tsx +++ b/packages/trpc/server/routers/viewer/apps.tsx @@ -10,7 +10,7 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { TRPCError } from "@trpc/server"; -import { authedAdminProcedure, router } from "../../trpc"; +import { authedAdminProcedure, authedProcedure, router } from "../../trpc"; interface FilteredApp { name: string; @@ -268,4 +268,13 @@ export const appsRouter = router({ }, }); }), + checkForGCal: authedProcedure.query(async ({ ctx }) => { + const gCalPresent = await ctx.prisma.credential.findFirst({ + where: { + type: "google_calendar", + userId: ctx.user.id, + }, + }); + return !!gCalPresent; + }), });