From 998162bc481c53a682f9654fa108032f102ccd8c Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:26:24 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20Google=20Sheets=20integration=20?= =?UTF-8?q?=E2=80=94=20token=20expiry=20&=20permission=20error=20handling?= =?UTF-8?q?=20(#7282)=20(#7285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integrations/google-sheets/actions.ts | 41 ++++++- .../components/AddIntegrationModal.tsx | 24 +++- .../components/GoogleSheetWrapper.tsx | 19 ++- .../components/ManageIntegration.tsx | 33 +++++- .../app/api/google-sheet/callback/route.ts | 45 ++++--- apps/web/i18n.lock | 5 + apps/web/lib/googleSheet/constants.ts | 6 + apps/web/lib/googleSheet/service.ts | 110 +++++++++++++++--- apps/web/locales/de-DE.json | 7 +- apps/web/locales/en-US.json | 7 +- apps/web/locales/es-ES.json | 7 +- apps/web/locales/fr-FR.json | 7 +- apps/web/locales/hu-HU.json | 7 +- apps/web/locales/ja-JP.json | 7 +- apps/web/locales/nl-NL.json | 7 +- apps/web/locales/pt-BR.json | 7 +- apps/web/locales/pt-PT.json | 7 +- apps/web/locales/ro-RO.json | 7 +- apps/web/locales/ru-RU.json | 7 +- apps/web/locales/sv-SE.json | 7 +- apps/web/locales/zh-Hans-CN.json | 7 +- apps/web/locales/zh-Hant-TW.json | 7 +- 22 files changed, 320 insertions(+), 61 deletions(-) create mode 100644 apps/web/lib/googleSheet/constants.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions.ts b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions.ts index 0644ed8ba235..da70452cd431 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions.ts @@ -1,12 +1,49 @@ "use server"; import { z } from "zod"; -import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet"; -import { getSpreadsheetNameById } from "@/lib/googleSheet/service"; +import { ZId } from "@formbricks/types/common"; +import { + TIntegrationGoogleSheets, + ZIntegrationGoogleSheets, +} from "@formbricks/types/integration/google-sheet"; +import { getSpreadsheetNameById, validateGoogleSheetsConnection } from "@/lib/googleSheet/service"; +import { getIntegrationByType } from "@/lib/integration/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper"; +const ZValidateGoogleSheetsConnectionAction = z.object({ + environmentId: ZId, +}); + +export const validateGoogleSheetsConnectionAction = authenticatedActionClient + .schema(ZValidateGoogleSheetsConnectionAction) + .action(async ({ ctx, parsedInput }) => { + await checkAuthorizationUpdated({ + userId: ctx.user.id, + organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId), + access: [ + { + type: "organization", + roles: ["owner", "manager"], + }, + { + type: "projectTeam", + projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId), + minPermission: "readWrite", + }, + ], + }); + + const integration = await getIntegrationByType(parsedInput.environmentId, "googleSheets"); + if (!integration) { + return { data: false }; + } + + await validateGoogleSheetsConnection(integration as TIntegrationGoogleSheets); + return { data: true }; + }); + const ZGetSpreadsheetNameByIdAction = z.object({ googleSheetIntegration: ZIntegrationGoogleSheets, environmentId: z.string(), diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx index 0ee9b61ac841..87cd6a7fceac 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx @@ -20,6 +20,10 @@ import { isValidGoogleSheetsUrl, } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/util"; import GoogleSheetLogo from "@/images/googleSheetsLogo.png"; +import { + GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION, + GOOGLE_SHEET_INTEGRATION_INVALID_GRANT, +} from "@/lib/googleSheet/constants"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { recallToHeadline } from "@/lib/utils/recall"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; @@ -118,6 +122,17 @@ export const AddIntegrationModal = ({ resetForm(); }, [selectedIntegration, surveys]); + const showErrorMessageToast = (response: Awaited>) => { + const errorMessage = getFormattedErrorMessage(response); + if (errorMessage === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) { + toast.error(t("environments.integrations.google_sheets.token_expired_error")); + } else if (errorMessage === GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION) { + toast.error(t("environments.integrations.google_sheets.spreadsheet_permission_error")); + } else { + toast.error(errorMessage); + } + }; + const linkSheet = async () => { try { if (!isValidGoogleSheetsUrl(spreadsheetUrl)) { @@ -129,6 +144,7 @@ export const AddIntegrationModal = ({ if (selectedElements.length === 0) { throw new Error(t("environments.integrations.select_at_least_one_question_error")); } + setIsLinkingSheet(true); const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl); const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({ googleSheetIntegration, @@ -137,13 +153,11 @@ export const AddIntegrationModal = ({ }); if (!spreadsheetNameResponse?.data) { - const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse); - throw new Error(errorMessage); + showErrorMessageToast(spreadsheetNameResponse); + return; } const spreadsheetName = spreadsheetNameResponse.data; - - setIsLinkingSheet(true); integrationData.spreadsheetId = spreadsheetId; integrationData.spreadsheetName = spreadsheetName; integrationData.surveyId = selectedSurvey.id; @@ -280,7 +294,7 @@ export const AddIntegrationModal = ({
-
+
{surveyElements.map((question) => (
diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper.tsx index cb3a6f65c4f9..6312c17cbe6a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { TEnvironment } from "@formbricks/types/environment"; import { TIntegrationGoogleSheets, @@ -8,9 +8,11 @@ import { } from "@formbricks/types/integration/google-sheet"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; +import { validateGoogleSheetsConnectionAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions"; import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration"; import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/google"; import googleSheetLogo from "@/images/googleSheetsLogo.png"; +import { GOOGLE_SHEET_INTEGRATION_INVALID_GRANT } from "@/lib/googleSheet/constants"; import { ConnectIntegration } from "@/modules/ui/components/connect-integration"; import { AddIntegrationModal } from "./AddIntegrationModal"; @@ -35,10 +37,23 @@ export const GoogleSheetWrapper = ({ googleSheetIntegration ? googleSheetIntegration.config?.key : false ); const [isModalOpen, setIsModalOpen] = useState(false); + const [showReconnectButton, setShowReconnectButton] = useState(false); const [selectedIntegration, setSelectedIntegration] = useState< (TIntegrationGoogleSheetsConfigData & { index: number }) | null >(null); + const validateConnection = useCallback(async () => { + if (!isConnected || !googleSheetIntegration) return; + const response = await validateGoogleSheetsConnectionAction({ environmentId: environment.id }); + if (response?.serverError === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) { + setShowReconnectButton(true); + } + }, [environment.id, isConnected, googleSheetIntegration]); + + useEffect(() => { + validateConnection(); + }, [validateConnection]); + const handleGoogleAuthorization = async () => { authorize(environment.id, webAppUrl).then((url: string) => { if (url) { @@ -64,6 +79,8 @@ export const GoogleSheetWrapper = ({ setOpenAddIntegrationModal={setIsModalOpen} setIsConnected={setIsConnected} setSelectedIntegration={setSelectedIntegration} + showReconnectButton={showReconnectButton} + handleGoogleAuthorization={handleGoogleAuthorization} locale={locale} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration.tsx index 0066b92be822..ce3a8f605bb8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration.tsx @@ -1,6 +1,6 @@ "use client"; -import { Trash2Icon } from "lucide-react"; +import { RefreshCcwIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; @@ -12,15 +12,19 @@ import { TUserLocale } from "@formbricks/types/user"; import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions"; import { timeSince } from "@/lib/time"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { EmptyState } from "@/modules/ui/components/empty-state"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; interface ManageIntegrationProps { googleSheetIntegration: TIntegrationGoogleSheets; setOpenAddIntegrationModal: (v: boolean) => void; setIsConnected: (v: boolean) => void; setSelectedIntegration: (v: (TIntegrationGoogleSheetsConfigData & { index: number }) | null) => void; + showReconnectButton: boolean; + handleGoogleAuthorization: () => void; locale: TUserLocale; } @@ -29,6 +33,8 @@ export const ManageIntegration = ({ setOpenAddIntegrationModal, setIsConnected, setSelectedIntegration, + showReconnectButton, + handleGoogleAuthorization, locale, }: ManageIntegrationProps) => { const { t } = useTranslation(); @@ -68,7 +74,17 @@ export const ManageIntegration = ({ return (
-
+ {showReconnectButton && ( + + + {t("environments.integrations.google_sheets.reconnect_button_description")} + + + {t("environments.integrations.google_sheets.reconnect_button")} + + + )} +
@@ -77,6 +93,19 @@ export const ManageIntegration = ({ })}
+ + + + + + + {t("environments.integrations.google_sheets.reconnect_button_tooltip")} + + +