feat: Fix Proton Calendar with ICS (#15454)

* feat: fix proton calendar with ics

* ics-feed: add note & clarify checkbox

* fix review

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
This commit is contained in:
Achraf
2024-07-16 12:08:54 +02:00
committed by GitHub
co-authored by Keith Williams Amit Sharma
parent 3f4a0eff7f
commit 7046f6f80a
4 changed files with 40 additions and 6 deletions
@@ -2494,7 +2494,9 @@
"USER_PENDING_MEMBER_OF_THE_ORG": "User is a pending member of the organization",
"USER_ALREADY_INVITED_OR_MEMBER": "User is already invited or a member",
"USER_MEMBER_OF_OTHER_ORGANIZATION": "User is member of an organization that this team is not a part of.",
"skip_writing_to_calendar": "Do not write to the ICS feed",
"rescheduling_not_possible": "Rescheduling is not possible as the event has expired",
"event_expired": "This event is expired",
"skip_writing_to_calendar_note": "If your ICS link is read-only (e.g., Proton Calendar), check the box above to avoid errors. You'll also need to manually update your calendar for changes.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -10,7 +10,7 @@ import { CalendarService } from "../lib";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
const { urls } = req.body;
const { urls, skipWriting } = req.body;
// Get user
const user = await prisma.user.findFirstOrThrow({
where: {
@@ -24,7 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const data = {
type: appConfig.type,
key: symmetricEncrypt(JSON.stringify({ urls }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
key: symmetricEncrypt(JSON.stringify({ urls, skipWriting }), process.env.CALENDSO_ENCRYPTION_KEY || ""),
userId: user.id,
teamId: null,
appId: appConfig.slug,
@@ -40,18 +40,28 @@ const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
export default class ICSFeedCalendarService implements Calendar {
private urls: string[] = [];
private skipWriting = false;
protected integrationName = "ics-feed_calendar";
constructor(credential: CredentialPayload) {
const { urls } = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
const { urls, skipWriting } = JSON.parse(
symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY)
);
this.urls = urls;
this.skipWriting = skipWriting;
}
createEvent(_event: CalendarEvent, _credentialId: number): Promise<NewCalendarEventType> {
if (this.skipWriting) {
return Promise.reject(new Error("Event creation is disabled for this calendar."));
}
throw new Error("createEvent called on read-only ICS feed");
}
deleteEvent(_uid: string, _event: CalendarEvent, _externalCalendarId?: string): Promise<unknown> {
if (this.skipWriting) {
return Promise.reject(new Error("Event creation is disabled for this calendar."));
}
throw new Error("deleteEvent called on read-only ICS feed");
}
@@ -60,6 +70,9 @@ export default class ICSFeedCalendarService implements Calendar {
_event: CalendarEvent,
_externalCalendarId?: string
): Promise<NewCalendarEventType | NewCalendarEventType[]> {
if (this.skipWriting) {
return Promise.reject(new Error("Event creation is disabled for this calendar."));
}
throw new Error("updateEvent called on read-only ICS feed");
}
@@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, Form, TextField } from "@calcom/ui";
import { Alert, Button, Form, TextField, CheckboxField } from "@calcom/ui";
import { Icon } from "@calcom/ui";
export default function ICSFeedSetup() {
@@ -17,6 +17,7 @@ export default function ICSFeedSetup() {
const [urls, setUrls] = useState<string[]>([""]);
const [errorMessage, setErrorMessage] = useState("");
const [errorActionUrl, setErrorActionUrl] = useState("");
const [skipWriting, setSkipWriting] = useState(false); // track if user opts out of writing to any calendar
return (
<div className="bg-emphasis flex h-screen">
@@ -40,7 +41,7 @@ export default function ICSFeedSetup() {
setErrorMessage("");
const res = await fetch("/api/integrations/ics-feedcalendar/add", {
method: "POST",
body: JSON.stringify({ urls }),
body: JSON.stringify({ urls, skipWriting }),
headers: {
"Content-Type": "application/json",
},
@@ -88,9 +89,27 @@ export default function ICSFeedSetup() {
onClick={() => {
setUrls((urls) => urls.concat(""));
}}>
{t("add")} <Icon name="plus" size={16} />
{t("add")} <Icon className="inline" name="plus" size={16} />
</button>
<div className="mt-3 flex items-center">
<CheckboxField
type="checkbox"
id="skipWriting"
checked={skipWriting}
onChange={(e) => setSkipWriting(e.target.checked)}
className="mr-2"
description={t("skip_writing_to_calendar")}
/>
</div>
<Alert
className="mt-3"
severity="info"
title={t("notes")}
message={t("skip_writing_to_calendar_note")}
/>
{errorMessage && (
<Alert
severity="error"