Compare commits

...

4 Commits

Author SHA1 Message Date
Dhruwang 4018d558a1 tweaks 2026-04-20 18:20:34 +05:30
Dhruwang c9a5d1a81e Merge branch 'main' of https://github.com/formbricks/formbricks into fix/cal-embed-credentialless 2026-04-20 18:07:44 +05:30
Nox d7eb158ccd fix: address CodeRabbit review — MutationObserver + fallback onClick
- Replace 5s-only timeout with MutationObserver watching for iframe insertion
- Add load/error event listeners on discovered iframe
- Keep 5s timeout as fallback when no iframe appears
- Call onSuccessfulBooking() when user clicks fallback booking link
- Clean up observer and timer properly to avoid leaks
2026-03-15 16:54:32 +07:00
NOX Ventures 0729798537 fix: add error handling for cal.com embeds in credentialless environments
- Add 5-second timeout to detect embed load failures
- Show error message with fallback direct booking link
- Handle COEP/Cross-Origin Embedder Policy blocks gracefully

Fixes #7457
2026-03-15 00:17:27 +07:00
22 changed files with 107 additions and 14 deletions
-10
View File
@@ -315,7 +315,6 @@ checksums:
common/other: 79acaa6cd481262bea4e743a422529d2
common/other_filters: 20b09213c131db47eb8b23e72d0c4bea
common/other_placeholder: f3a0fa2eaaf75aa92b290449c928c081
common/others: 39160224ce0e35eb4eb252c997edf4d8
common/overlay_color: 4b72073285d13fff93d094aabffe05ac
common/overview: 30c54e4dc4ce599b87d94be34a8617f5
common/password: 223a61cf906ab9c40d22612c588dff48
@@ -333,7 +332,6 @@ checksums:
common/please_upgrade_your_plan: 03d54a21ecd27723c72a13644837e5ed
common/powered_by_formbricks: 1c3e19894583292bfaf686cac84a4960
common/preview: 3173ee1f0f1d4e50665ca4a84c38e15d
common/preview_survey: 7409e9c118e3e5d5f2a86201c2b354f2
common/privacy: 7459744a63ef8af4e517a09024bd7c08
common/product_manager: dfeadc96e6d3de22a884ee97974b505e
common/production: 226e0ce83b49700bc1b1c08c4c3ed23a
@@ -468,7 +466,6 @@ checksums:
common/workspace_name_placeholder: 8a9e30ab01666af13c44a73b82c37ec1
common/workspaces: 8ba082a84aa35cf851af1cf874b853e2
common/years: eb4f5fdd2b320bf13e200fd6a6c1abff
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
common/you_are_downgraded_to_the_community_edition: e3ae56502ff787109cae0997519f628e
common/you_are_not_authorized_to_perform_this_action: 1b3255ab740582ddff016a399f8bf302
common/you_have_reached_your_limit_of_workspace_limit: 54d754c3267036742f23fb05fd3fcc45
@@ -1239,12 +1236,7 @@ checksums:
environments/settings/teams/you_are_a_member: cf5af638d5371c8fbc337e92519e5150
environments/surveys/all_set_time_to_create_first_survey: 21d3bb74c3b9642b3195d17c17346399
environments/surveys/alphabetical: 5fcfeff9c5fd28714f0a390e0ddaaaee
environments/surveys/copy_survey: de8142b45e7bca61f2dca0069a62b417
environments/surveys/copy_survey_description: 66d0aadf192ad5790fbf3f55f3bb5485
environments/surveys/copy_survey_error: 74cab7d84ea8b669e106d4c326cac005
environments/surveys/copy_survey_link_to_clipboard: 77387e3d3de4be07a2a34963f73cd7e8
environments/surveys/copy_survey_partially_success: a436a5fb7167b95c2308794d35aab070
environments/surveys/copy_survey_success: a829e645fe034b3e712d0b8572a5edc4
environments/surveys/delete_survey_and_responses_warning: 3320c91c1fd27378b7f3d6abc003f2ae
environments/surveys/edit/activate_translations: af127c1bed2b47e2012e3a23e489ecb8
environments/surveys/edit/add: 5196f5cd4ba3a6ac8edef91345e17f66
@@ -1965,7 +1957,6 @@ checksums:
environments/surveys/summary/downloading_qr_code: 3c46bf636e617848a4fca9b6c5b51dac
environments/surveys/summary/drop_offs: 605ee950f82110132d6c5780926af109
environments/surveys/summary/drop_offs_tooltip: 2a01683380be45f17636365886cf3452
environments/surveys/summary/failed_to_copy_link: 4e891c757c80e770674e8e74d1c08487
environments/surveys/summary/filter_added_successfully: e247f65020cd87454bcec0da6f0fd034
environments/surveys/summary/filter_updated_successfully: 01146bc7e6394e271836be2f1b3a257b
environments/surveys/summary/filtered_responses_csv: aad66a98be6a09cac8bef9e4db4a75cf
@@ -2050,7 +2041,6 @@ checksums:
environments/surveys/summary/youre_not_plugged_in_yet: f19da3cd474b9a3cf28e956fd811fb00
environments/surveys/survey_deleted_successfully: a6b654cc914b344a4475fd2fd4a98cc5
environments/surveys/survey_duplicated_successfully: 91e244f1e7a33640bb4817166a01ff46
environments/surveys/survey_duplication_error: 35994330aed844ce37d8b4f09df24581
environments/surveys/templates/all_channels: 6be67a82fc7326dc2304b23ab3348b87
environments/surveys/templates/all_industries: c7354412fe34585526ff2232aadace41
environments/surveys/templates/all_roles: 6582ccd0a2349c162a7ae1574cdf76be
@@ -17,9 +17,7 @@ export const ProjectLimitModal = ({ open, setOpen, projectLimit, buttons }: Proj
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogTitle className="sr-only">
{t("common.unlock_more_workspaces_with_a_higher_plan")}
</DialogTitle>
<DialogTitle className="sr-only">{t("common.unlock_more_workspaces_with_a_higher_plan")}</DialogTitle>
<UpgradePrompt
title={t("common.unlock_more_workspaces_with_a_higher_plan")}
description={t("common.you_have_reached_your_limit_of_workspace_limit", { projectLimit })}
+2
View File
@@ -7,10 +7,12 @@ checksums:
common/back: f541015a827e37cb3b1234e56bc2aa3c
common/close_survey: 36e6aaa19051cb253aa155ad69a9edbc
common/company_logo: 82d5c0d5994508210ee02d684819f4b8
common/failed_to_load_booking_widget: 6fcdeae283dc6c08cc8186c3751ecf24
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/language_switch: fd72a9ada13f672f4fd5da863b22cc46
common/next: 89ddbcf710eba274963494f312bdc8a9
common/no_results_found: 5518f2865757dc73900aa03ef8be6934
common/open_booking_page_directly_at: 5f51eb388be802279f52b0eda32985e7
common/open_in_new_tab: 6844e4922a7a40a7ee25c10ea109cdeb
common/people_responded: b685fb877090d8658db724ad07a0dbd8
common/please_retry_now_or_try_again_later: 949a3841e2eb01fa249790a42bf23aa5
+2
View File
@@ -6,10 +6,12 @@
"back": "رجوع",
"close_survey": "إغلاق الاستبيان",
"company_logo": "شعار الشركة",
"failed_to_load_booking_widget": "فشل تحميل نافذة الحجز. قد تكون بيئتك تحظر الموارد عبر النطاقات.",
"finish": "إنهاء",
"language_switch": "تبديل اللغة",
"next": "التالي",
"no_results_found": "لم يتم العثور على نتائج",
"open_booking_page_directly_at": "جرب فتح صفحة الحجز مباشرة على",
"open_in_new_tab": "فتح في علامة تبويب جديدة",
"people_responded": "{count, plural, one {شخص واحد استجاب} two {شخصان استجابا} few {{count} أشخاص استجابوا} many {{count} شخصًا استجابوا} other {{count} شخص استجابوا}}",
"please_retry_now_or_try_again_later": "يرجى إعادة المحاولة الآن أو المحاولة مرة أخرى لاحقًا.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Tilbage",
"close_survey": "Luk undersøgelse",
"company_logo": "Firmalogo",
"failed_to_load_booking_widget": "Kunne ikke indlæse bookingwidget. Dit miljø blokerer muligvis ressourcer på tværs af domæner.",
"finish": "Afslut",
"language_switch": "Sprogskift",
"next": "Næste",
"no_results_found": "Ingen resultater fundet",
"open_booking_page_directly_at": "Prøv at åbne bookingsiden direkte på",
"open_in_new_tab": "Åbn i ny fane",
"people_responded": "{count, plural, one {1 person har svaret} other {{count} personer har svaret}}",
"please_retry_now_or_try_again_later": "Prøv igen nu eller prøv senere.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Zurück",
"close_survey": "Umfrage schließen",
"company_logo": "Firmenlogo",
"failed_to_load_booking_widget": "Das Buchungs-Widget konnte nicht geladen werden. Deine Umgebung blockiert möglicherweise Cross-Origin-Ressourcen.",
"finish": "Fertig",
"language_switch": "Sprachwechsel",
"next": "Weiter",
"no_results_found": "Keine Ergebnisse gefunden",
"open_booking_page_directly_at": "Versuch, die Buchungsseite direkt zu öffnen unter",
"open_in_new_tab": "In neuem Tab öffnen",
"people_responded": "{count, plural, one {1 Person hat geantwortet} other {{count} Personen haben geantwortet}}",
"please_retry_now_or_try_again_later": "Bitte versuchen Sie es jetzt erneut oder später noch einmal.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Back",
"close_survey": "Close survey",
"company_logo": "Company Logo",
"failed_to_load_booking_widget": "Failed to load booking widget. Your environment may be blocking cross-origin resources.",
"finish": "Finish",
"language_switch": "Language switch",
"next": "Next",
"no_results_found": "No results found",
"open_booking_page_directly_at": "Try opening the booking page directly at",
"open_in_new_tab": "Open in new tab",
"people_responded": "{count, plural, one {1 person responded} other {{count} people responded}}",
"please_retry_now_or_try_again_later": "Please retry now or try again later.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Atrás",
"close_survey": "Cerrar encuesta",
"company_logo": "Logo de la empresa",
"failed_to_load_booking_widget": "No se pudo cargar el widget de reservas. Tu entorno puede estar bloqueando recursos de origen cruzado.",
"finish": "Finalizar",
"language_switch": "Cambio de idioma",
"next": "Siguiente",
"no_results_found": "No se encontraron resultados",
"open_booking_page_directly_at": "Intenta abrir la página de reservas directamente en",
"open_in_new_tab": "Abrir en nueva pestaña",
"people_responded": "{count, plural, one {1 persona respondió} other {{count} personas respondieron}}",
"please_retry_now_or_try_again_later": "Por favor, inténtalo ahora o prueba más tarde.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Tagasi",
"close_survey": "Sulge küsitlus",
"company_logo": "Ettevõtte logo",
"failed_to_load_booking_widget": "Broneerimisvidina laadimine ebaõnnestus. Sinu keskkond võib blokeerida ristdomeenide ressursse.",
"finish": "Lõpeta",
"language_switch": "Keele vahetamine",
"next": "Edasi",
"no_results_found": "Tulemusi ei leitud",
"open_booking_page_directly_at": "Proovi avada broneerimislehte otse aadressil",
"open_in_new_tab": "Ava uuel vahelehel",
"people_responded": "{count, plural, one {1 inimene vastas} other {{count} inimest vastas}}",
"please_retry_now_or_try_again_later": "Palun proovi uuesti kohe või hiljem.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Retour",
"close_survey": "Fermer le sondage",
"company_logo": "Logo de l'entreprise",
"failed_to_load_booking_widget": "Échec du chargement du widget de réservation. Votre environnement bloque peut-être les ressources d'origine croisée.",
"finish": "Terminer",
"language_switch": "Changement de langue",
"next": "Suivant",
"no_results_found": "Aucun résultat trouvé",
"open_booking_page_directly_at": "Essayez d'ouvrir la page de réservation directement à l'adresse",
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
"people_responded": "{count, plural, one {1 personne a répondu} other {{count} personnes ont répondu}}",
"please_retry_now_or_try_again_later": "Veuillez réessayer maintenant ou réessayer plus tard.",
+2
View File
@@ -6,10 +6,12 @@
"back": "वापस",
"close_survey": "सर्वेक्षण बंद करें",
"company_logo": "कंपनी लोगो",
"failed_to_load_booking_widget": "बुकिंग विजेट लोड करने में विफल। आपका एनवायरनमेंट क्रॉस-ओरिजिन संसाधनों को ब्लॉक कर रहा है।",
"finish": "समाप्त करें",
"language_switch": "भाषा बदलें",
"next": "अगला",
"no_results_found": "कोई परिणाम नहीं मिला",
"open_booking_page_directly_at": "बुकिंग पेज को सीधे यहां खोलने का प्रयास करें",
"open_in_new_tab": "नए टैब में खोलें",
"people_responded": "{count, plural, one {1 व्यक्ति ने जवाब दिया} other {{count} लोगों ने जवाब दिया}}",
"please_retry_now_or_try_again_later": "कृपया अभी पुनः प्रयास करें या बाद में फिर से प्रयास करें।",
+2
View File
@@ -6,10 +6,12 @@
"back": "Vissza",
"close_survey": "Kérdőív lezárása",
"company_logo": "Vállalat logója",
"failed_to_load_booking_widget": "A foglalási modul betöltése sikertelen. Lehet, hogy a környezeted blokkolja a különböző forrásokból származó erőforrásokat.",
"finish": "Befejezés",
"language_switch": "Nyelvválasztó",
"next": "Következő",
"no_results_found": "Nincs találat",
"open_booking_page_directly_at": "Próbáld meg közvetlenül megnyitni a foglalási oldalt itt:",
"open_in_new_tab": "Megnyitás új lapon",
"people_responded": "{count, plural, one {1 személy válaszolt} other {{count} személy válaszolt}}",
"please_retry_now_or_try_again_later": "Próbálkozzon újra most, vagy próbálja meg később újra.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Indietro",
"close_survey": "Chiudi sondaggio",
"company_logo": "Logo aziendale",
"failed_to_load_booking_widget": "Impossibile caricare il widget di prenotazione. Il tuo ambiente potrebbe bloccare le risorse cross-origin.",
"finish": "Fine",
"language_switch": "Cambio lingua",
"next": "Avanti",
"no_results_found": "Nessun risultato trovato",
"open_booking_page_directly_at": "Prova ad aprire la pagina di prenotazione direttamente su",
"open_in_new_tab": "Apri in una nuova scheda",
"people_responded": "{count, plural, one {1 persona ha risposto} other {{count} persone hanno risposto}}",
"please_retry_now_or_try_again_later": "Riprova ora o più tardi.",
+2
View File
@@ -6,10 +6,12 @@
"back": "戻る",
"close_survey": "アンケートを閉じる",
"company_logo": "会社ロゴ",
"failed_to_load_booking_widget": "予約ウィジェットの読み込みに失敗しました。お使いの環境でクロスオリジンリソースがブロックされている可能性があります。",
"finish": "完了",
"language_switch": "言語切替",
"next": "次へ",
"no_results_found": "結果が見つかりません",
"open_booking_page_directly_at": "予約ページを直接開いてみてください:",
"open_in_new_tab": "新しいタブで開く",
"people_responded": "{count, plural, other {{count}人が回答しました}}",
"please_retry_now_or_try_again_later": "今すぐ再試行するか、後でもう一度お試しください。",
+2
View File
@@ -6,10 +6,12 @@
"back": "Terug",
"close_survey": "Enquête sluiten",
"company_logo": "Bedrijfslogo",
"failed_to_load_booking_widget": "Boekingswidget kan niet worden geladen. Je omgeving blokkeert mogelijk cross-origin resources.",
"finish": "Voltooien",
"language_switch": "Taalschakelaar",
"next": "Volgende",
"no_results_found": "Geen resultaten gevonden",
"open_booking_page_directly_at": "Probeer de boekingspagina direct te openen op",
"open_in_new_tab": "Openen in nieuw tabblad",
"people_responded": "{count, plural, one {1 persoon heeft gereageerd} other {{count} mensen hebben gereageerd}}",
"please_retry_now_or_try_again_later": "Probeer het nu opnieuw of probeer het later opnieuw.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Voltar",
"close_survey": "Fechar pesquisa",
"company_logo": "Logo da empresa",
"failed_to_load_booking_widget": "Falha ao carregar o widget de reserva. O teu ambiente pode estar a bloquear recursos de origem cruzada.",
"finish": "Finalizar",
"language_switch": "Alternar idioma",
"next": "Próximo",
"no_results_found": "Nenhum resultado encontrado",
"open_booking_page_directly_at": "Tenta abrir a página de reserva diretamente em",
"open_in_new_tab": "Abrir em nova aba",
"people_responded": "{count, plural, one {1 pessoa respondeu} other {{count} pessoas responderam}}",
"please_retry_now_or_try_again_later": "Por favor, tente novamente agora ou mais tarde.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Înapoi",
"close_survey": "Închide sondajul",
"company_logo": "Sigla companiei",
"failed_to_load_booking_widget": "Nu s-a putut încărca widgetul de rezervare. Este posibil ca mediul tău să blocheze resursele cross-origin.",
"finish": "Finalizează",
"language_switch": "Schimbare limbă",
"next": "Următorul",
"no_results_found": "Nu s-au găsit rezultate",
"open_booking_page_directly_at": "Încearcă să deschizi pagina de rezervare direct la",
"open_in_new_tab": "Deschide într-o filă nouă",
"people_responded": "{count, plural, one {1 persoană a răspuns} other {{count} persoane au răspuns}}",
"please_retry_now_or_try_again_later": "Te rugăm să încerci din nou acum sau mai târziu.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Назад",
"close_survey": "Закрыть опрос",
"company_logo": "Логотип компании",
"failed_to_load_booking_widget": "Не удалось загрузить виджет бронирования. Возможно, ваше окружение блокирует межсайтовые ресурсы.",
"finish": "Завершить",
"language_switch": "Переключение языка",
"next": "Далее",
"no_results_found": "Результатов не найдено",
"open_booking_page_directly_at": "Попробуйте открыть страницу бронирования напрямую по адресу",
"open_in_new_tab": "Открыть в новой вкладке",
"people_responded": "{count, plural, one {1 человек ответил} other {{count} человека ответили}}",
"please_retry_now_or_try_again_later": "Пожалуйста, повторите попытку сейчас или попробуйте позже.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Tillbaka",
"close_survey": "Stäng enkät",
"company_logo": "Företagslogotyp",
"failed_to_load_booking_widget": "Det gick inte att ladda bokningswidgeten. Din miljö kan blockera resurser från andra domäner.",
"finish": "Slutför",
"language_switch": "Språkväxlare",
"next": "Nästa",
"no_results_found": "Inga resultat hittades",
"open_booking_page_directly_at": "Prova att öppna bokningssidan direkt på",
"open_in_new_tab": "Öppna i ny flik",
"people_responded": "{count, plural, one {1 person har svarat} other {{count} personer har svarat}}",
"please_retry_now_or_try_again_later": "Försök igen nu eller försök igen senare.",
+2
View File
@@ -6,10 +6,12 @@
"back": "Orqaga",
"close_survey": "Sorovnomani yopish",
"company_logo": "Kompaniya logotipi",
"failed_to_load_booking_widget": "Bron qilish vidjetini yuklash amalga oshmadi. Muhitingiz turli manbalardan resurslarni bloklayotgan bo'lishi mumkin.",
"finish": "Tugatish",
"language_switch": "Tilni almashtirish",
"next": "Keyingisi",
"no_results_found": "Natijalar topilmadi",
"open_booking_page_directly_at": "Bron qilish sahifasini to'g'ridan-to'g'ri ochishga harakat qiling",
"open_in_new_tab": "Yangi oynada ochish",
"people_responded": "{count, plural, one {1 kishi javob berdi} other {{count} kishi javob berdi}}",
"please_retry_now_or_try_again_later": "Iltimos, hozir qayta urinib koring yoki keyinroq urinib koring.",
+2
View File
@@ -6,10 +6,12 @@
"back": "返回",
"close_survey": "关闭调查",
"company_logo": "公司标志",
"failed_to_load_booking_widget": "预订小部件加载失败。您的环境可能正在阻止跨域资源。",
"finish": "完成",
"language_switch": "语言切换",
"next": "下一步",
"no_results_found": "未找到结果",
"open_booking_page_directly_at": "请尝试直接打开预订页面:",
"open_in_new_tab": "在新标签页中打开",
"people_responded": "{count, plural, one {1 人已回应} other {{count} 人已回应}}",
"please_retry_now_or_try_again_later": "请立即重试或稍后再试。",
@@ -1,5 +1,6 @@
import snippet from "@calcom/embed-snippet";
import { useEffect, useMemo } from "preact/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import { type TSurveyCalElement } from "@formbricks/types/surveys/elements";
import { cn } from "@/lib/utils";
@@ -9,6 +10,9 @@ interface CalEmbedProps {
}
export function CalEmbed({ element, onSuccessfulBooking }: CalEmbedProps) {
const [error, setError] = useState(false);
const { t } = useTranslation();
const cal = useMemo(() => {
const calInline = snippet("https://cal.com/embed.js");
@@ -46,13 +50,76 @@ export function CalEmbed({ element, onSuccessfulBooking }: CalEmbedProps) {
document.querySelectorAll("cal-inline").forEach((el) => {
el.remove();
});
setError(false);
cal("init", { calOrigin: element.calHost ? `https://${element.calHost}` : "https://cal.com" });
cal("inline", {
elementOrSelector: "#cal-embed",
calLink: element.calUserName,
});
// Event-driven error detection via MutationObserver
let observer: MutationObserver | null = null;
let timer: ReturnType<typeof setTimeout> | null = null;
const cleanup = () => {
if (timer) clearTimeout(timer);
if (observer) observer.disconnect();
};
const embedContainer = document.getElementById("cal-embed");
if (embedContainer) {
observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLIFrameElement) {
node.addEventListener("load", () => {
cleanup();
setError(false);
});
node.addEventListener("error", () => {
cleanup();
setError(true);
});
}
}
}
});
observer.observe(embedContainer, { childList: true, subtree: true });
// Fallback timeout in case no iframe appears at all
timer = setTimeout(() => {
const iframe = embedContainer.querySelector("iframe");
if (!iframe) {
cleanup();
setError(true);
}
}, 5000);
}
return cleanup;
}, [cal, element.calHost, element.calUserName]);
if (error) {
return (
<div className="relative mt-4 overflow-auto">
<div className="border-border rounded-input border p-4 text-center">
<p className="text-sm text-red-600">{t("common.failed_to_load_booking_widget")}</p>
<p className="text-muted-foreground mt-2 text-xs">
{t("common.open_booking_page_directly_at")}{" "}
<a
href={`https://${element.calHost || "cal.com"}/${element.calUserName}`}
target="_blank"
rel="noopener noreferrer"
className="underline">
{element.calHost || "cal.com"}/{element.calUserName}
</a>
</p>
</div>
</div>
);
}
return (
<div className="relative mt-4 overflow-auto">
<div id="cal-embed" className={cn("border-border rounded-input border")} />