diff --git a/packages/app-store/intercom/api/configure.ts b/packages/app-store/intercom/api/configure.ts index 1bd0d65bf3..f964e254c3 100644 --- a/packages/app-store/intercom/api/configure.ts +++ b/packages/app-store/intercom/api/configure.ts @@ -1,136 +1,29 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import prisma from "@calcom/prisma"; - -import type { - NewCanvas, - ListComponent, - ListItem, - SpacerComponent, - TextComponent, - InputComponent, -} from "../lib"; -import { isValidCalURL } from "../lib/isValidCalURL"; +import { handleButtonAndInvitationStep } from "../lib/configure/button"; +import { handleLinkStep } from "../lib/configure/link"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { admin, input_values, component_id } = req.body; + const { input_values, current_canvas } = req.body; - let isValid: boolean | TextComponent = true; - if (component_id || input_values?.submit_booking_url) { - const url = component_id === "submit_booking_url" ? input_values?.submit_booking_url : component_id; - isValid = await isValidCalURL(url); + console.dir(req.body); - if (isValid === true) return res.status(200).json({ results: { submit_booking_url: url } }); + const linkStepResult = current_canvas?.stored_data?.submit_booking_url + ? current_canvas.stored_data.submit_booking_url + : await handleLinkStep(req); + if (typeof linkStepResult !== "string") { + return res.status(200).json(linkStepResult); + } + const buttonAndInvitationStepResult = await handleButtonAndInvitationStep(req); + console.log(buttonAndInvitationStepResult); + if (buttonAndInvitationStepResult) { + return res.status(200).json(buttonAndInvitationStepResult); } - const input: InputComponent = { - type: "input", - id: "submit_booking_url", - label: "Enter your Cal.com link", - placeholder: "https://cal.com/valentinchmara/30min", - save_state: "unsaved", - action: { - type: "submit", - }, - aria_label: "Enter your Cal.com link", - }; - - const defaultCanvasData: NewCanvas = { - canvas: { - content: { - components: isValid === true ? [input] : [isValid, input], - }, - }, - }; - - if (!admin?.id) return res.status(200).json(defaultCanvasData); - - const credential = await prisma.credential.findFirst({ - where: { - appId: "intercom", - key: { - string_contains: admin.id, - }, + return res.status(200).json({ + results: { + ...input_values, + submit_booking_url: current_canvas?.stored_data?.submit_booking_url, }, }); - - if (!credential) return res.status(200).json(defaultCanvasData); - - const team = credential.teamId - ? await prisma.team.findUnique({ - where: { - id: credential.teamId, - }, - }) - : null; - - const userId = credential.userId; - - const user = userId - ? await prisma.user.findUnique({ - where: { - id: userId, - }, - }) - : null; - - const eventTypes = await prisma.eventType.findMany({ - where: { - userId, - hidden: false, - }, - }); - - if (!eventTypes) return res.status(200).json(defaultCanvasData); - if (!user && !team) return res.status(200).json(defaultCanvasData); - - const list: ListItem[] = eventTypes.map((eventType) => { - let slug; - if (team && team.slug) { - slug = `team/${team.slug}`; - } else if (user && user.username) { - slug = user.username; - } - - return { - id: `${WEBAPP_URL}/${slug}/${eventType.slug}`, - type: "item", - title: eventType.title, - subtitle: `${slug}/${eventType.slug}`, - rounded_image: false, - disabled: false, - action: { - type: "submit", - }, - }; - }); - - const components: ListComponent = { - type: "list", - items: list, - }; - - const spacer: SpacerComponent = { - type: "spacer", - size: "m", - }; - - const text: TextComponent = { - type: "text", - text: "Or choose another Cal.com link:", - style: "muted", - align: "left", - }; - - const canvasData: NewCanvas = { - canvas: { - content: { - components: - isValid === true ? [components, spacer, text, input] : [components, spacer, text, input, isValid], - }, - }, - }; - - return res.status(200).json(canvasData); } diff --git a/packages/app-store/intercom/api/get.ts b/packages/app-store/intercom/api/get.ts index ba0a018513..f06012a89c 100644 --- a/packages/app-store/intercom/api/get.ts +++ b/packages/app-store/intercom/api/get.ts @@ -66,11 +66,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - /* Not functionnal Cal("on", { - action: "eventTypeSelected", - callback: (e) => INTERCOM_MESSENGER_SHEET_LIBRARY.submitSheet({...e.detail}), - });*/ + action: "bookingSuccessful", + callback: (e) => { + console.log("bookingSuccessful", e) + try { + INTERCOM_MESSENGER_SHEET_LIBRARY.submitSheet(e.detail.data) + } catch(error) { + console.log("Error Intercom sheet", error) + } + } + }); + diff --git a/packages/app-store/intercom/api/index.ts b/packages/app-store/intercom/api/index.ts index a06372443d..818b652669 100644 --- a/packages/app-store/intercom/api/index.ts +++ b/packages/app-store/intercom/api/index.ts @@ -3,3 +3,4 @@ export { default as callback } from "./callback"; export { default as initialize } from "./initialize"; export { default as configure } from "./configure"; export { default as get } from "./get"; +export { default as sheet } from "./sheet"; diff --git a/packages/app-store/intercom/api/initialize.ts b/packages/app-store/intercom/api/initialize.ts index 525e6fada1..49cbf9102d 100644 --- a/packages/app-store/intercom/api/initialize.ts +++ b/packages/app-store/intercom/api/initialize.ts @@ -15,10 +15,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) canvas: { content: { components: [ + { + type: "text", + text: card_creation_options?.invitation_input ?? "Schedule a meeting with me", + align: "left", + style: "header", + }, { type: "button", id: "submit-issue-form", - label: "Book a meeting", + label: card_creation_options?.booking_button_input ?? "Booking button text", style: "primary", action: { type: "sheet", diff --git a/packages/app-store/intercom/api/sheet.ts b/packages/app-store/intercom/api/sheet.ts new file mode 100644 index 0000000000..0b9e910ab8 --- /dev/null +++ b/packages/app-store/intercom/api/sheet.ts @@ -0,0 +1,58 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import type { NewCanvas, TextComponent, SpacerComponent } from "../lib"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { sheet_values } = req.body; + + if (!sheet_values) { + return res.status(400).json({ error: "Invalid input data" }); + } + + const { booking, eventType, organizer } = sheet_values; + + // Prepare date and time information + const startTime = new Date(booking.startTime); + const options = { + weekday: "long" as const, + year: "numeric" as const, + month: "long" as const, + day: "numeric" as const, + hour: "numeric" as const, + minute: "numeric" as const, + timeZone: organizer.timeZone, + timeZoneName: "short" as const, + }; + const formattedDate = startTime.toLocaleDateString("en-US", options); + + // Create text components for the recap + const confirmedText: TextComponent = { + type: "text", + text: `Confirmed: ${eventType.title}`, + style: "header", + align: "left", + }; + + const detailsText: TextComponent = { + type: "text", + text: `You are scheduled with ${organizer.name} on ${formattedDate}`, + style: "paragraph", + align: "left", + }; + + const spacer: SpacerComponent = { + type: "spacer", + size: "m", + }; + + // Build the canvas data + const canvasData: NewCanvas = { + canvas: { + content: { + components: [confirmedText, spacer, detailsText], + }, + }, + }; + + return res.status(200).json(canvasData); +} diff --git a/packages/app-store/intercom/lib/configure/button.ts b/packages/app-store/intercom/lib/configure/button.ts new file mode 100644 index 0000000000..1a5a760836 --- /dev/null +++ b/packages/app-store/intercom/lib/configure/button.ts @@ -0,0 +1,82 @@ +import type { NextApiRequest } from "next"; + +import type { NewCanvas, InputComponent, SpacerComponent, TextComponent, ButtonComponent } from "../../lib"; + +export async function handleButtonAndInvitationStep(req: NextApiRequest): Promise { + const { input_values, current_canvas, component_id } = req.body; + + const errors = []; + + if (input_values?.invitation_text && input_values.invitation_text.length > 100) { + errors.push("Invitation text must be less than 100 characters."); + } + + if (input_values?.booking_button_text && input_values.booking_button_text.length > 20) { + errors.push("Booking button text must be less than 20 characters."); + } + + if (errors.length === 0 && Object.keys(input_values).includes("booking_button_input")) return; + + const invitationTextInput: InputComponent = { + type: "input", + id: "invitation_input", + label: "Invitation text", + placeholder: "Schedule a meeting with me", + value: "Schedule a meeting with me", + aria_label: "Enter invitation text", + }; + + const bookingButtonTextInput: InputComponent = { + type: "input", + id: "booking_button_input", + label: "Booking button text", + placeholder: "Book now", + value: "Book now", + aria_label: "Enter booking button text", + }; + + const spacer: SpacerComponent = { + type: "spacer", + size: "m", + }; + + const button: ButtonComponent = { + type: "button", + id: "booking_button", + label: "Create an invite", + action: { + type: "submit", + }, + }; + + const submit_booking_url = current_canvas?.stored_data?.submit_booking_url + ? current_canvas?.stored_data?.submit_booking_url + : component_id === "submit_booking_url" + ? input_values?.submit_booking_url + : component_id; + + return { + canvas: { + content: { + components: [ + spacer, + invitationTextInput, + bookingButtonTextInput, + button, + ...errors?.map( + (e) => + ({ + type: "text", + text: e, + style: "error", + align: "left", + } as TextComponent) + ), + ], + }, + stored_data: { + submit_booking_url, + }, + }, + }; +} diff --git a/packages/app-store/intercom/lib/configure/link.ts b/packages/app-store/intercom/lib/configure/link.ts new file mode 100644 index 0000000000..61c0b9e142 --- /dev/null +++ b/packages/app-store/intercom/lib/configure/link.ts @@ -0,0 +1,134 @@ +import type { NextApiRequest } from "next"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; + +import type { + NewCanvas, + ListComponent, + ListItem, + SpacerComponent, + TextComponent, + InputComponent, +} from "../../lib"; +import { isValidCalURL } from "../../lib/isValidCalURL"; + +export async function handleLinkStep(req: NextApiRequest): Promise { + const { admin, component_id, input_values } = req.body; + + const url = component_id === "submit_booking_url" ? input_values?.submit_booking_url : component_id; + const { isValid, error } = url ? await isValidCalURL(url) : { isValid: false, error: undefined }; + + if (isValid) return url; + + const input: InputComponent = { + type: "input", + id: "submit_booking_url", + label: "Enter your Cal.com link", + placeholder: "https://cal.com/valentinchmara/30min", + save_state: "unsaved", + action: { + type: "submit", + }, + aria_label: "Enter your Cal.com link", + }; + + const defaultCanvasData: NewCanvas = { + canvas: { + content: { + components: !error ? [input] : [error, input], + }, + }, + }; + + if (!admin?.id) return defaultCanvasData; + + const credential = await prisma.credential.findFirst({ + where: { + appId: "intercom", + key: { + string_contains: admin.id, + }, + }, + }); + + if (!credential) return defaultCanvasData; + + const team = credential.teamId + ? await prisma.team.findUnique({ + where: { + id: credential.teamId, + }, + }) + : null; + + const userId = credential.userId; + + const user = userId + ? await prisma.user.findUnique({ + where: { + id: userId, + }, + }) + : null; + + const eventTypes = await prisma.eventType.findMany({ + where: { + userId, + hidden: false, + teamId: team ? team.id : undefined, + }, + }); + + if (eventTypes && eventTypes?.length === 0) return defaultCanvasData; + if (!user && !team) return defaultCanvasData; + + // Limit to 10 Events types + const list: ListItem[] = eventTypes.slice(0, 10).map((eventType) => { + let slug; + if (team && team.slug) { + slug = `team/${team.slug}`; + } else if (user && user.username) { + slug = user.username; + } + + return { + id: `${WEBAPP_URL}/${slug}/${eventType.slug}`, + type: "item", + title: eventType.title, + subtitle: `${slug}/${eventType.slug}`, + rounded_image: false, + disabled: false, + action: { + type: "submit", + }, + }; + }); + + const components: ListComponent = { + type: "list", + items: list, + }; + + const spacer: SpacerComponent = { + type: "spacer", + size: "m", + }; + + const text: TextComponent = { + type: "text", + text: "Or choose another Cal.com link:", + style: "muted", + align: "left", + }; + + const canvasData: NewCanvas = { + canvas: { + content: { + components: !error ? [components, spacer, text, input] : [components, spacer, text, input, error], + }, + }, + }; + + return canvasData; +} diff --git a/packages/app-store/intercom/lib/index.ts b/packages/app-store/intercom/lib/index.ts index 21f4326a5f..86633584dc 100644 --- a/packages/app-store/intercom/lib/index.ts +++ b/packages/app-store/intercom/lib/index.ts @@ -3,23 +3,29 @@ export interface CanvasComponent { disabled?: boolean; } +export interface IsValid { + isValid: boolean; + error?: TextComponent; +} + export interface InputComponent extends CanvasComponent { type: "input"; id: string; label: string; placeholder: string; - save_state: "unsaved" | "saved"; - action: { + value?: string; + save_state?: "unsaved" | "saved"; + action?: { type: "submit"; }; aria_label: string; } -interface ButtonComponent extends CanvasComponent { +export interface ButtonComponent extends CanvasComponent { type: "button"; id: string; label: string; - style: "primary" | "secondary" | "link"; + style?: "primary" | "secondary" | "link"; action: { type: "submit" | "sheet" | "url"; url?: string; @@ -62,5 +68,7 @@ export interface CanvasContent { export interface NewCanvas { canvas: { content: CanvasContent; + content_url?: string; + stored_data?: Record; }; } diff --git a/packages/app-store/intercom/lib/isValidCalURL.ts b/packages/app-store/intercom/lib/isValidCalURL.ts index 3589c260d4..8eee313798 100644 --- a/packages/app-store/intercom/lib/isValidCalURL.ts +++ b/packages/app-store/intercom/lib/isValidCalURL.ts @@ -6,7 +6,7 @@ import type { TextComponent } from "../lib"; /** * Check if the url is a valid cal.com url * @param url - * @returns boolean + * @returns IsValid */ export async function isValidCalURL(url: string) { const regex = new RegExp(`^${WEBAPP_URL}/`, `i`); @@ -18,7 +18,11 @@ export async function isValidCalURL(url: string) { align: "left", }; - if (!regex.test(url)) return error; + if (!regex.test(url)) + return { + isValid: false, + error, + }; const urlWithoutCal = url.replace(regex, ""); @@ -26,7 +30,11 @@ export async function isValidCalURL(url: string) { const usernameOrTeamSlug = urlParts[0]; const eventTypeSlug = urlParts[1]; - if (!usernameOrTeamSlug || !eventTypeSlug) return error; + if (!usernameOrTeamSlug || !eventTypeSlug) + return { + isValid: false, + error, + }; // Find all potential users with the given username const potentialUsers = await prisma.user.findMany({ @@ -64,11 +72,19 @@ export async function isValidCalURL(url: string) { // Check if any team has the matching eventTypeSlug const matchingTeam = potentialTeams.find((team) => team.eventTypes.length > 0); - if (!matchingUser && !matchingTeam) return error; + if (!matchingUser && !matchingTeam) + return { + isValid: false, + error, + }; const userOrTeam = matchingUser || matchingTeam; - if (!userOrTeam) return error; + if (!userOrTeam) + return { + isValid: false, + error, + }; // Retrieve the correct user or team const userOrTeamId = userOrTeam.id; @@ -81,7 +97,13 @@ export async function isValidCalURL(url: string) { }, }); - if (!eventType) return error; + if (!eventType) + return { + isValid: false, + error, + }; - return true; + return { + isValid: true, + }; }