feat: add Proton Calendar integration#29552
Conversation
|
Welcome to Cal.diy, @singhaditya21! Thanks for opening this pull request. A few things to keep in mind:
A maintainer will review your PR soon. Thanks for contributing! |
📝 WalkthroughWalkthroughThis PR adds complete Proton Calendar integration to Cal.com. Users can now connect their Proton Calendar ICS subscription feeds for read-only availability checking. The implementation includes new platform constants identifying the app, URL validation enforcing Proton Calendar's specific ICS endpoints, a calendar service factory wrapping the existing ICS feed infrastructure with a configurable integration name, a POST/GET API handler validating and persisting credentials with encrypted URLs, a React setup component for user input, and registration wiring across app store metadata, handler registries, and service maps. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
apps/web/components/apps/protoncalendar/Setup.tsx (2)
71-71: 💤 Low valueUse template literals for conditional className.
String concatenation for conditional class names is error-prone. Consider using template literals or a utility like
clsx/cn.♻️ Proposed refactor
- containerClassName={`w-full ${i === 0 ? "mr-6" : ""}`} + containerClassName={`w-full ${i === 0 ? "mr-6" : ""}`}Or with template literals:
- containerClassName={`w-full ${i === 0 ? "mr-6" : ""}`} + containerClassName={i === 0 ? "w-full mr-6" : "w-full"}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/components/apps/protoncalendar/Setup.tsx` at line 71, The conditional className currently built with ad-hoc concatenation should be rewritten using a safer utility or cleaner template handling: update the containerClassName assignment in the Setup component to use a classnames helper (e.g., cn or clsx) or a trimmed template literal so the empty string case doesn't produce extra spaces; target the containerClassName usage where i is checked (i === 0) in the Setup.tsx component and replace it with cn('w-full', { 'mr-6': i === 0 }) or an equivalent trimmed template literal expression.
91-98: 💤 Low valueUse
Buttoncomponent for consistency.The "Add" button uses a plain
<button>element while other actions use theButtoncomponent from@calcom/ui. UsingButtonconsistently improves maintainability and ensures uniform styling.♻️ Proposed refactor
- <button - className="text-sm" + <Button + color="minimal" type="button" onClick={() => { setUrls((urls) => urls.concat("")); }}> - {t("add")} <PlusIcon className="inline" size={16} /> - </button> + <PlusIcon className="inline mr-1" size={16} /> + {t("add")} + </Button>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/components/apps/protoncalendar/Setup.tsx` around lines 91 - 98, Replace the plain <button> with the shared Button component from `@calcom/ui`: import Button if missing and use <Button type="button" onClick={() => setUrls(urls => urls.concat(""))} className="text-sm"> {t("add")} <PlusIcon className="inline" size={16} /> </Button>; keep the same onClick behavior and visual content (t("add") + PlusIcon), preserve any accessibility props (type, aria-label if used elsewhere) and adjust or remove redundant class props if the Button exposes size/variant props to maintain consistent styling.packages/app-store/protoncalendar/api/add.ts (2)
34-42: 💤 Low valuePrefer
findUniqueOrThrowfor unique field lookups.Since
idis a unique field,findUniqueOrThrowis more semantically correct and slightly more efficient thanfindFirstOrThrow.♻️ Proposed refactor
- const user = await prisma.user.findFirstOrThrow({ + const user = await prisma.user.findUniqueOrThrow({ where: { id: userId, },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app-store/protoncalendar/api/add.ts` around lines 34 - 42, Replace the call to prisma.user.findFirstOrThrow with prisma.user.findUniqueOrThrow when querying by the unique id; specifically update the invocation in the add handler where prisma.user.findFirstOrThrow({ where: { id: userId }, select: {...} }) is used to instead call prisma.user.findUniqueOrThrow({ where: { id: userId }, select: {...} }) so the lookup is semantically correct and slightly more efficient while keeping the same select payload (id, email).
54-60: ⚖️ Poor tradeoffConsider extracting credential shape construction.
The credential object passed to
BuildCalendarServiceuses dummy values (id: 0,encryptedKey: null) for validation purposes. While functional, extracting this into a helper function would clarify intent and improve maintainability.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app-store/protoncalendar/api/add.ts` around lines 54 - 60, The credential object passed into BuildCalendarService is constructed inline with dummy values (id: 0, encryptedKey: null); extract this into a small helper (e.g., buildValidationCredential or makeCalendarCredential) that takes the real data and returns the credential shape used for validation, then replace the inline object in add.ts with a call to that helper; update BuildCalendarService invocation to use the helper result and keep id/encryptedKey defaults inside the helper so intent is clear and maintainability improves.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/components/apps/protoncalendar/Setup.tsx`:
- Around line 59-61: The catch block currently swallows errors; change it to
capture the error (e.g., catch (err)) and log the error details before calling
setErrorMessage(t("something_went_wrong")). Update the catch in the function
containing setErrorMessage to call console.error (or the app logger) with a
descriptive message plus the caught error, then preserve the existing
setErrorMessage call so the UI still shows the generic message.
- Line 51: The current expression "const json = await res.json().catch(() =>
({}));" swallows JSON parse errors; change it to first check the response status
(res.ok) and then parse JSON inside a try/catch so parse failures are not
converted into an empty object: attempt "await res.json()" in a try block, on
failure read and include the raw response text and the original error in a log
or thrown Error, and rethrow the error instead of returning {} so callers can
surface API problems; use the existing "res" variable and the "const json"
assignment as the location to implement this behavior.
- Line 58: Validate json.url before calling router.push: ensure the response
JSON has a non-empty string url (e.g. check json && typeof json.url === 'string'
&& json.url.trim() !== '') before invoking router.push(json.url); if validation
fails, log or surface an error and avoid navigation. Update the code path around
router.push(json.url) (in the Setup component / the submit/handler function
where router.push is called) to perform this check and handle the
invalid/malformed response gracefully.
- Around line 19-20: Replace the local useState usage for urls and errorMessage
with react-hook-form controlled fields: remove const [urls, setUrls] and const
[errorMessage, setErrorMessage], register the urls array with your form (or use
useFieldArray for dynamic URL inputs) inside the Setup component, and update the
submit handler to read values from data.urls and report validation/problems with
form.setError rather than setErrorMessage; ensure any UI that previously read
urls now reads form.watch('urls') or the field array, and any handlers that
modified setUrls use form methods (append/remove/update) from useFieldArray.
In `@packages/app-store/protoncalendar/api/add.ts`:
- Line 13: The CALENDSO_ENCRYPTION_KEY constant is defaulting to an empty string
which allows encryption with no key; change the initialization to require the
env var and fail fast: validate process.env.CALENDSO_ENCRYPTION_KEY and if
missing throw a clear Error (or call process.exit(1)) with a descriptive message
so the application will not start with an empty key; update the initialization
of CALENDSO_ENCRYPTION_KEY in add.ts (and any other usages) to rely on the
validated value instead of "".
- Around line 70-74: The catch block in add.ts currently checks error instanceof
Error; update it to use ErrorWithCode for non-tRPC error handling: change the
type guard to error instanceof ErrorWithCode (and import ErrorWithCode at the
top of the file), then log the error.code and error.message via logger.error in
the logger call inside the catch (alongside the existing context) and keep
returning res.status(500).json as before; ensure you add the necessary import
for ErrorWithCode and adjust the logged payload to include error.code when
available.
---
Nitpick comments:
In `@apps/web/components/apps/protoncalendar/Setup.tsx`:
- Line 71: The conditional className currently built with ad-hoc concatenation
should be rewritten using a safer utility or cleaner template handling: update
the containerClassName assignment in the Setup component to use a classnames
helper (e.g., cn or clsx) or a trimmed template literal so the empty string case
doesn't produce extra spaces; target the containerClassName usage where i is
checked (i === 0) in the Setup.tsx component and replace it with cn('w-full', {
'mr-6': i === 0 }) or an equivalent trimmed template literal expression.
- Around line 91-98: Replace the plain <button> with the shared Button component
from `@calcom/ui`: import Button if missing and use <Button type="button"
onClick={() => setUrls(urls => urls.concat(""))} className="text-sm"> {t("add")}
<PlusIcon className="inline" size={16} /> </Button>; keep the same onClick
behavior and visual content (t("add") + PlusIcon), preserve any accessibility
props (type, aria-label if used elsewhere) and adjust or remove redundant class
props if the Button exposes size/variant props to maintain consistent styling.
In `@packages/app-store/protoncalendar/api/add.ts`:
- Around line 34-42: Replace the call to prisma.user.findFirstOrThrow with
prisma.user.findUniqueOrThrow when querying by the unique id; specifically
update the invocation in the add handler where prisma.user.findFirstOrThrow({
where: { id: userId }, select: {...} }) is used to instead call
prisma.user.findUniqueOrThrow({ where: { id: userId }, select: {...} }) so the
lookup is semantically correct and slightly more efficient while keeping the
same select payload (id, email).
- Around line 54-60: The credential object passed into BuildCalendarService is
constructed inline with dummy values (id: 0, encryptedKey: null); extract this
into a small helper (e.g., buildValidationCredential or makeCalendarCredential)
that takes the real data and returns the credential shape used for validation,
then replace the inline object in add.ts with a call to that helper; update
BuildCalendarService invocation to use the helper result and keep
id/encryptedKey defaults inside the helper so intent is clear and
maintainability improves.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b866bffd-06ae-49e7-9e2c-210a85f300ee
⛔ Files ignored due to path filters (2)
packages/app-store/protoncalendar/static/icon.svgis excluded by!**/*.svgyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (18)
apps/web/components/apps/AppSetupPage.tsxapps/web/components/apps/protoncalendar/Setup.tsxpackages/app-store/apps.metadata.generated.tspackages/app-store/apps.server.generated.tspackages/app-store/calendar.services.generated.tspackages/app-store/ics-feedcalendar/lib/CalendarService.tspackages/app-store/protoncalendar/DESCRIPTION.mdpackages/app-store/protoncalendar/api/add.tspackages/app-store/protoncalendar/api/index.tspackages/app-store/protoncalendar/config.jsonpackages/app-store/protoncalendar/index.tspackages/app-store/protoncalendar/lib/CalendarService.tspackages/app-store/protoncalendar/lib/__tests__/validateProtonCalendarUrl.test.tspackages/app-store/protoncalendar/lib/index.tspackages/app-store/protoncalendar/lib/validateProtonCalendarUrl.tspackages/app-store/protoncalendar/package.jsonpackages/i18n/locales/en/common.jsonpackages/platform/constants/apps.ts
| const [urls, setUrls] = useState<string[]>([""]); | ||
| const [errorMessage, setErrorMessage] = useState(""); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
State management bypasses react-hook-form.
The component uses react-hook-form but manages urls and errorMessage in separate useState hooks. This breaks the react-hook-form pattern and loses its validation, error handling, and performance benefits.
♻️ Recommended refactor using react-hook-form properly
+import { useFieldArray } from "react-hook-form";
+
export default function ProtonCalendarSetup() {
const { t } = useLocale();
const router = useRouter();
- const form = useForm({
- defaultValues: {},
+ const form = useForm<{ urls: { value: string }[] }>({
+ defaultValues: {
+ urls: [{ value: "" }],
+ },
});
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: "urls",
+ });
- const [urls, setUrls] = useState<string[]>([""]);
- const [errorMessage, setErrorMessage] = useState("");Then update the submit handler to use form.setError for errors and access data.urls directly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/components/apps/protoncalendar/Setup.tsx` around lines 19 - 20,
Replace the local useState usage for urls and errorMessage with react-hook-form
controlled fields: remove const [urls, setUrls] and const [errorMessage,
setErrorMessage], register the urls array with your form (or use useFieldArray
for dynamic URL inputs) inside the Setup component, and update the submit
handler to read values from data.urls and report validation/problems with
form.setError rather than setErrorMessage; ensure any UI that previously read
urls now reads form.watch('urls') or the field array, and any handlers that
modified setUrls use form methods (append/remove/update) from useFieldArray.
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
| const json = await res.json().catch(() => ({})); |
There was a problem hiding this comment.
Silent JSON parsing error masks API issues.
The .catch(() => ({})) silently converts JSON parse errors to an empty object. This makes it harder to diagnose API response problems.
🔍 Proposed fix
- const json = await res.json().catch(() => ({}));
+ const json = await res.json().catch((err) => {
+ console.error("Failed to parse API response:", err);
+ return {};
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const json = await res.json().catch(() => ({})); | |
| const json = await res.json().catch((err) => { | |
| console.error("Failed to parse API response:", err); | |
| return {}; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/components/apps/protoncalendar/Setup.tsx` at line 51, The current
expression "const json = await res.json().catch(() => ({}));" swallows JSON
parse errors; change it to first check the response status (res.ok) and then
parse JSON inside a try/catch so parse failures are not converted into an empty
object: attempt "await res.json()" in a try block, on failure read and include
the raw response text and the original error in a log or thrown Error, and
rethrow the error instead of returning {} so callers can surface API problems;
use the existing "res" variable and the "const json" assignment as the location
to implement this behavior.
| return; | ||
| } | ||
|
|
||
| router.push(json.url); |
There was a problem hiding this comment.
Validate json.url before navigation.
router.push is called without verifying that json.url exists. If the API returns a malformed response, this will push undefined to the router.
🛡️ Proposed fix
if (!res.ok) {
setErrorMessage(json?.message || t("something_went_wrong"));
return;
}
+ if (!json?.url) {
+ setErrorMessage(t("something_went_wrong"));
+ return;
+ }
router.push(json.url);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/components/apps/protoncalendar/Setup.tsx` at line 58, Validate
json.url before calling router.push: ensure the response JSON has a non-empty
string url (e.g. check json && typeof json.url === 'string' && json.url.trim()
!== '') before invoking router.push(json.url); if validation fails, log or
surface an error and avoid navigation. Update the code path around
router.push(json.url) (in the Setup component / the submit/handler function
where router.push is called) to perform this check and handle the
invalid/malformed response gracefully.
| } catch { | ||
| setErrorMessage(t("something_went_wrong")); | ||
| } |
There was a problem hiding this comment.
Log caught errors for debugging.
The generic catch block suppresses all error details, making it difficult to diagnose network or unexpected failures in production.
📋 Proposed fix
- } catch {
+ } catch (error) {
+ console.error("Proton Calendar setup error:", error);
setErrorMessage(t("something_went_wrong"));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch { | |
| setErrorMessage(t("something_went_wrong")); | |
| } | |
| } catch (error) { | |
| console.error("Proton Calendar setup error:", error); | |
| setErrorMessage(t("something_went_wrong")); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/components/apps/protoncalendar/Setup.tsx` around lines 59 - 61, The
catch block currently swallows errors; change it to capture the error (e.g.,
catch (err)) and log the error details before calling
setErrorMessage(t("something_went_wrong")). Update the catch in the function
containing setErrorMessage to call console.error (or the app logger) with a
descriptive message plus the caught error, then preserve the existing
setErrorMessage call so the UI still shows the generic message.
| import BuildCalendarService from "../lib/CalendarService"; | ||
| import { isValidProtonCalendarUrl, normalizeProtonCalendarUrl } from "../lib/validateProtonCalendarUrl"; | ||
|
|
||
| const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || ""; |
There was a problem hiding this comment.
Critical: encryption key must not default to empty string.
If CALENDSO_ENCRYPTION_KEY is unset, credentials will be encrypted with an empty key, which provides no security. The application should fail fast rather than silently accept weak encryption.
🔒 Proposed fix
-const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
+const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY;
+if (!CALENDSO_ENCRYPTION_KEY) {
+ throw new Error("CALENDSO_ENCRYPTION_KEY environment variable is required");
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || ""; | |
| const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY; | |
| if (!CALENDSO_ENCRYPTION_KEY) { | |
| throw new Error("CALENDSO_ENCRYPTION_KEY environment variable is required"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/protoncalendar/api/add.ts` at line 13, The
CALENDSO_ENCRYPTION_KEY constant is defaulting to an empty string which allows
encryption with no key; change the initialization to require the env var and
fail fast: validate process.env.CALENDSO_ENCRYPTION_KEY and if missing throw a
clear Error (or call process.exit(1)) with a descriptive message so the
application will not start with an empty key; update the initialization of
CALENDSO_ENCRYPTION_KEY in add.ts (and any other usages) to rely on the
validated value instead of "".
| } catch (error) { | ||
| logger.error("Could not add Proton Calendar feeds", { | ||
| message: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| return res.status(500).json({ message: "Could not add Proton Calendar feeds" }); |
There was a problem hiding this comment.
Use ErrorWithCode for non-tRPC errors.
As per coding guidelines, non-tRPC files should use ErrorWithCode instead of generic Error instances. This ensures consistent error handling across the codebase.
📝 Proposed fix
+import { ErrorWithCode } from "`@calcom/lib/errors`";
+
// ...
} catch (error) {
logger.error("Could not add Proton Calendar feeds", {
message: error instanceof Error ? error.message : "Unknown error",
});
- return res.status(500).json({ message: "Could not add Proton Calendar feeds" });
+ const err = error instanceof ErrorWithCode ? error : new ErrorWithCode("Could not add Proton Calendar feeds");
+ return res.status(500).json({ message: err.message });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| logger.error("Could not add Proton Calendar feeds", { | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| return res.status(500).json({ message: "Could not add Proton Calendar feeds" }); | |
| import { ErrorWithCode } from "`@calcom/lib/errors`"; | |
| } catch (error) { | |
| logger.error("Could not add Proton Calendar feeds", { | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| const err = error instanceof ErrorWithCode ? error : new ErrorWithCode("Could not add Proton Calendar feeds"); | |
| return res.status(500).json({ message: err.message }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/protoncalendar/api/add.ts` around lines 70 - 74, The catch
block in add.ts currently checks error instanceof Error; update it to use
ErrorWithCode for non-tRPC error handling: change the type guard to error
instanceof ErrorWithCode (and import ErrorWithCode at the top of the file), then
log the error.code and error.message via logger.error in the logger call inside
the catch (alongside the existing context) and keep returning
res.status(500).json as before; ensure you add the necessary import for
ErrorWithCode and adjust the logged payload to include error.code when
available.
Source: Coding guidelines
bandhan-majumder
left a comment
There was a problem hiding this comment.
please attach a demo of ur changes!
|
@singhaditya21 it should be end to end working demo. Also as a side note, bounties are excluded from this issue. |
|
Hi @bandhan-majumder, I have recorded and uploaded a fully functional end-to-end working demo of the Proton Calendar integration setup flow (logging in, entering multiple mock subscription URLs, saving, and verifying redirection back to the installed calendar apps). You can view the recorded demo video here: I also added a development/testing fetch fallback in |
|
Hi maintainers! The Proton Calendar integration is complete and tested. Could you please label this PR with |
|
Hi maintainers! Just checking in on this PR. The CI is currently showing as failed because it is waiting for the 'run-ci' label. I've verified that all tests, linters, and type-checks pass locally. Could you please label the PR so that the checks can run? |
|
Hi maintainers! Just checking in. The Proton Calendar integration is fully complete, all Biome linting issues have been resolved, and local tests pass. Could you please label it with 'run-ci' to trigger the check builds? Thank you! |
|
|
5bfa76a to
3545675
Compare

This PR integrates Proton Calendar into Cal.diy.
Demo Video:
https://github.com/singhaditya21/cal.diy/raw/bounty-proton-calendar-5756/packages/app-store/protoncalendar/static/proton_calendar_demo.webm
Details
https://calendar.proton.me/api/calendar/v1/url/...).ics-feedcalendarcodebase and registersprotoncalendarin the App Store configurations and platform constants./claim #5756