From 0b253e6ba568b4a343468b5ec47f2128dedf46ab Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Thu, 9 Nov 2023 10:53:34 +0100 Subject: [PATCH 01/11] chore: release formbricks-js 1.1.5 (#1601) --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index aa4543b082..d93907a575 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "name": "@formbricks/js", "license": "MIT", - "version": "1.1.4", + "version": "1.1.5", "description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.", "keywords": [ "Formbricks", From d6c9ce7c5b94356e7d43af2612da7d466185bc23 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Thu, 9 Nov 2023 15:29:42 +0530 Subject: [PATCH 02/11] fix: handle invalid surveyId from api endpoint (#1589) --- apps/web/app/api/v1/client/responses/route.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index 391561e193..3fa79addbe 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -9,6 +9,7 @@ import { getTeamDetails } from "@formbricks/lib/teamDetail/service"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses"; import { NextResponse } from "next/server"; import { UAParser } from "ua-parser-js"; +import { TSurvey } from "@formbricks/types/surveys"; export async function OPTIONS(): Promise { return responses.successResponse({}, true); @@ -27,10 +28,13 @@ export async function POST(request: Request): Promise { ); } - let survey; + let survey: TSurvey | null; try { survey = await getSurvey(responseInput.surveyId); + if (!survey) { + return responses.notFoundResponse("Survey", responseInput.surveyId); + } } catch (error) { if (error instanceof InvalidInputError) { return responses.badRequestResponse(error.message); From dd0f0ead398051af34fc1d655bb9077aec209bb2 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:31:49 +0530 Subject: [PATCH 03/11] refactor: Airtable & GS endpoints to use responses return objects (#1592) --- .env.example | 2 +- .../app/docs/integrations/airtable/page.mdx | 4 +-- .../integrations/airtable/lib/airtable.ts | 6 ++-- .../integrations/airtable/page.tsx | 4 +-- .../integrations/google-sheets/lib/google.ts | 2 +- .../app/api/google-sheet/callback/route.ts | 30 +++++-------------- apps/web/app/api/google-sheet/route.ts | 17 ++++++----- .../integrations/airtable/callback/route.ts | 24 +++++++-------- .../app/api/v1/integrations/airtable/route.ts | 20 ++++++------- .../v1/integrations/airtable/tables/route.ts | 17 +++++------ apps/web/env.mjs | 4 +-- packages/lib/airtable/service.ts | 4 +-- packages/lib/constants.ts | 2 +- turbo.json | 2 +- 14 files changed, 62 insertions(+), 76 deletions(-) diff --git a/.env.example b/.env.example index 4b75aeef61..82c2e0a64c 100644 --- a/.env.example +++ b/.env.example @@ -122,7 +122,7 @@ GOOGLE_SHEETS_CLIENT_SECRET= GOOGLE_SHEETS_REDIRECT_URL= # Oauth credentials for Airtable integration -AIR_TABLE_CLIENT_ID= +AIRTABLE_CLIENT_ID= # Enterprise License Key ENTERPRISE_LICENSE_KEY= diff --git a/apps/formbricks-com/app/docs/integrations/airtable/page.mdx b/apps/formbricks-com/app/docs/integrations/airtable/page.mdx index e8bdf49ac8..a1cd24ed05 100644 --- a/apps/formbricks-com/app/docs/integrations/airtable/page.mdx +++ b/apps/formbricks-com/app/docs/integrations/airtable/page.mdx @@ -160,8 +160,8 @@ Enabling the Airtable Integration in a self-hosted environment requires creating ### By now, your environment variables should include the below ones: -- `AIR_TABLE_CLIENT_ID` -- `AIR_TABLE_REDIRECT_URL` +- `AIRTABLE_CLIENT_ID` +- `AIRTABLE_REDIRECT_URL` Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks Cloud](#formbricks-cloud) section to link an Airtable with Formbricks. diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts index bbb61a25d0..cbeff6da6c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable.ts @@ -6,8 +6,8 @@ export const fetchTables = async (environmentId: string, baseId: string) => { headers: { environmentId: environmentId }, cache: "no-store", }); - - return res.json() as Promise; + const resJson = await res.json(); + return resJson.data as Promise; }; export const authorize = async (environmentId: string, apiHost: string): Promise => { @@ -21,6 +21,6 @@ export const authorize = async (environmentId: string, apiHost: string): Promise throw new Error("Could not create response"); } const resJSON = await res.json(); - const authUrl = resJSON.authUrl; + const authUrl = resJSON.data.authUrl; return authUrl; }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx index 2f014c7d8c..405b83b143 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx @@ -1,6 +1,6 @@ import AirtableWrapper from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"; import { getAirtableTables } from "@formbricks/lib/airtable/service"; -import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getIntegrations } from "@formbricks/lib/integration/service"; import { getSurveys } from "@formbricks/lib/survey/service"; @@ -9,7 +9,7 @@ import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; import GoBackButton from "@formbricks/ui/GoBackButton"; export default async function Airtable({ params }) { - const enabled = !!AIR_TABLE_CLIENT_ID; + const enabled = !!AIRTABLE_CLIENT_ID; const [surveys, integrations, environment] = await Promise.all([ getSurveys(params.environmentId), getIntegrations(params.environmentId), diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts index 5238eeca75..dd7d6b03b4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google.ts @@ -9,6 +9,6 @@ export const authorize = async (environmentId: string, apiHost: string): Promise throw new Error("Could not create response"); } const resJSON = await res.json(); - const authUrl = resJSON.authUrl; + const authUrl = resJSON.data.authUrl; return authUrl; }; diff --git a/apps/web/app/api/google-sheet/callback/route.ts b/apps/web/app/api/google-sheet/callback/route.ts index e2c4540adc..0e90fcb276 100644 --- a/apps/web/app/api/google-sheet/callback/route.ts +++ b/apps/web/app/api/google-sheet/callback/route.ts @@ -1,10 +1,11 @@ -import { prisma } from "@formbricks/database"; +import { responses } from "@/app/lib/api/response"; import { GOOGLE_SHEETS_CLIENT_ID, WEBAPP_URL, GOOGLE_SHEETS_CLIENT_SECRET, GOOGLE_SHEETS_REDIRECT_URL, } from "@formbricks/lib/constants"; +import { createOrUpdateIntegration } from "@formbricks/lib/integration/service"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; @@ -15,19 +16,19 @@ export async function GET(req: NextRequest) { const code = queryParams.get("code"); if (!environmentId) { - return NextResponse.json({ error: "Invalid environmentId" }); + return responses.badRequestResponse("Invalid environmentId"); } if (code && typeof code !== "string") { - return NextResponse.json({ message: "`code` must be a string" }, { status: 400 }); + return responses.badRequestResponse("`code` must be a string"); } const client_id = GOOGLE_SHEETS_CLIENT_ID; const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; - if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); - if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); - if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); + if (!client_id) return responses.internalServerErrorResponse("Google client id is missing"); + if (!client_secret) return responses.internalServerErrorResponse("Google client secret is missing"); + if (!redirect_uri) return responses.internalServerErrorResponse("Google redirect url is missing"); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); let key; @@ -61,22 +62,7 @@ export async function GET(req: NextRequest) { }, }; - const result = await prisma.integration.upsert({ - where: { - type_environmentId: { - environmentId, - type: "googleSheets", - }, - }, - update: { - ...googleSheetIntegration, - environment: { connect: { id: environmentId } }, - }, - create: { - ...googleSheetIntegration, - environment: { connect: { id: environmentId } }, - }, - }); + const result = await createOrUpdateIntegration(environmentId, googleSheetIntegration); if (result) { return NextResponse.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/google-sheets`); diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 306deefcf5..2bf28d40e8 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,11 +1,12 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { responses } from "@/app/lib/api/response"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, GOOGLE_SHEETS_REDIRECT_URL, } from "@formbricks/lib/constants"; import { google } from "googleapis"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; @@ -20,24 +21,24 @@ export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); if (!environmentId) { - return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 }); + return responses.badRequestResponse("environmentId is missing"); } if (!session) { - return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); + return responses.notAuthenticatedResponse(); } const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment) { - return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); + return responses.unauthorizedResponse(); } const client_id = GOOGLE_SHEETS_CLIENT_ID; const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; - if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); - if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); - if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); + if (!client_id) return responses.internalServerErrorResponse("Google client id is missing"); + if (!client_secret) return responses.internalServerErrorResponse("Google client secret is missing"); + if (!redirect_uri) return responses.internalServerErrorResponse("Google redirect url is missing"); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const authUrl = oAuth2Client.generateAuthUrl({ @@ -47,5 +48,5 @@ export async function GET(req: NextRequest) { state: environmentId!, }); - return NextResponse.json({ authUrl }, { status: 200 }); + return responses.successResponse({ authUrl }); } diff --git a/apps/web/app/api/v1/integrations/airtable/callback/route.ts b/apps/web/app/api/v1/integrations/airtable/callback/route.ts index da6f287a4d..97c2bba96d 100644 --- a/apps/web/app/api/v1/integrations/airtable/callback/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/callback/route.ts @@ -1,9 +1,10 @@ import { authOptions } from "@formbricks/lib/authOptions"; import { connectAirtable, fetchAirtableAuthToken } from "@formbricks/lib/airtable/service"; -import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getServerSession } from "next-auth"; import { NextRequest, NextResponse } from "next/server"; +import { responses } from "@/app/lib/api/response"; import * as z from "zod"; async function getEmail(token: string) { @@ -26,27 +27,27 @@ export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); if (!environmentId) { - return NextResponse.json({ error: "Invalid environmentId" }); + return responses.badRequestResponse("Invalid environmentId"); } if (!session) { - return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); + return responses.notAuthenticatedResponse(); } if (code && typeof code !== "string") { - return NextResponse.json({ message: "`code` must be a string" }, { status: 400 }); + return responses.badRequestResponse("`code` must be a string"); } const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment) { - return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); + return responses.unauthorizedResponse(); } - const client_id = AIR_TABLE_CLIENT_ID; + const client_id = AIRTABLE_CLIENT_ID; const redirect_uri = WEBAPP_URL + "/api/v1/integrations/airtable/callback"; const code_verifier = Buffer.from(environmentId + session.user.id + environmentId).toString("base64"); - if (!client_id) return NextResponse.json({ Error: "Airtable client id is missing" }, { status: 400 }); - if (!redirect_uri) return NextResponse.json({ Error: "Airtable redirect url is missing" }, { status: 400 }); + if (!client_id) return responses.internalServerErrorResponse("Airtable client id is missing"); + if (!redirect_uri) return responses.internalServerErrorResponse("Airtable redirect url is missing"); const formData = { grant_type: "authorization_code", @@ -59,7 +60,7 @@ export async function GET(req: NextRequest) { try { const key = await fetchAirtableAuthToken(formData); if (!key) { - return NextResponse.json({ Error: "Failed to fetch Airtable auth token" }, { status: 500 }); + return responses.notFoundResponse("airtable auth token", key); } const email = await getEmail(key.access_token); @@ -71,8 +72,7 @@ export async function GET(req: NextRequest) { return NextResponse.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/airtable`); } catch (error) { console.error(error); - NextResponse.json({ Error: error }, { status: 500 }); + responses.internalServerErrorResponse(error); } - - NextResponse.json({ Error: "unknown error occurred" }, { status: 400 }); + responses.badRequestResponse("unknown error occurred"); } diff --git a/apps/web/app/api/v1/integrations/airtable/route.ts b/apps/web/app/api/v1/integrations/airtable/route.ts index 6734e42d20..85775af47f 100644 --- a/apps/web/app/api/v1/integrations/airtable/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/route.ts @@ -1,10 +1,11 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; +import { responses } from "@/app/lib/api/response"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import crypto from "crypto"; -import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; +import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; const scope = `data.records:read data.records:write schema.bases:read schema.bases:write user.email:read`; @@ -13,23 +14,22 @@ export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); if (!environmentId) { - return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 }); + return responses.badRequestResponse("environmentId is missing"); } if (!session) { - return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); + return responses.notAuthenticatedResponse(); } const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment) { - return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); + return responses.unauthorizedResponse(); } - const client_id = AIR_TABLE_CLIENT_ID; + const client_id = AIRTABLE_CLIENT_ID; const redirect_uri = WEBAPP_URL + "/api/v1/integrations/airtable/callback"; - if (!client_id) return NextResponse.json({ Error: "Airtable client id is missing" }, { status: 400 }); - if (!redirect_uri) return NextResponse.json({ Error: "Airtable redirect url is missing" }, { status: 400 }); - + if (!client_id) return responses.internalServerErrorResponse("Airtable client id is missing"); + if (!redirect_uri) return responses.internalServerErrorResponse("Airtable redirect url is missing"); const codeVerifier = Buffer.from(environmentId + session.user.id + environmentId).toString("base64"); const codeChallengeMethod = "S256"; @@ -51,5 +51,5 @@ export async function GET(req: NextRequest) { authUrl.searchParams.append("code_challenge_method", codeChallengeMethod); authUrl.searchParams.append("code_challenge", codeChallenge); - return NextResponse.json({ authUrl: authUrl.toString() }, { status: 200 }); + return responses.successResponse({ authUrl: authUrl.toString() }); } diff --git a/apps/web/app/api/v1/integrations/airtable/tables/route.ts b/apps/web/app/api/v1/integrations/airtable/tables/route.ts index 23f5520d8a..d83afd2103 100644 --- a/apps/web/app/api/v1/integrations/airtable/tables/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/tables/route.ts @@ -4,7 +4,8 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getIntegrationByType } from "@formbricks/lib/integration/service"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; import { getServerSession } from "next-auth"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; +import { responses } from "@/app/lib/api/response"; import * as z from "zod"; export async function GET(req: NextRequest) { @@ -15,30 +16,28 @@ export async function GET(req: NextRequest) { const baseId = z.string().safeParse(queryParams.get("baseId")); if (!baseId.success) { - return NextResponse.json({ Error: "Base Id is Required" }, { status: 400 }); + return responses.missingFieldResponse("Base Id is Required"); } if (!session) { - return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); + return responses.notAuthenticatedResponse(); } if (!environmentId) { - return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 }); + return responses.badRequestResponse("environmentId is missing"); } const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment || !environmentId) { - return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); + return responses.unauthorizedResponse(); } const integration = (await getIntegrationByType(environmentId, "airtable")) as TIntegrationAirtable; - console.log(integration); if (!integration) { - return NextResponse.json({ Error: "integration not found" }, { status: 401 }); + return responses.notFoundResponse("Integration not found", environmentId); } const tables = await getTables(integration.config.key, baseId.data); - - return NextResponse.json(tables, { status: 200 }); + return responses.successResponse(tables); } diff --git a/apps/web/env.mjs b/apps/web/env.mjs index af27358517..e7358258f4 100644 --- a/apps/web/env.mjs +++ b/apps/web/env.mjs @@ -54,7 +54,7 @@ export const env = createEnv({ GOOGLE_SHEETS_CLIENT_ID: z.string().optional(), GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(), GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(), - AIR_TABLE_CLIENT_ID: z.string().optional(), + AIRTABLE_CLIENT_ID: z.string().optional(), AWS_ACCESS_KEY: z.string().optional(), AWS_SECRET_KEY: z.string().optional(), S3_ACCESS_KEY: z.string().optional(), @@ -139,7 +139,7 @@ export const env = createEnv({ AZUREAD_CLIENT_ID: process.env.AZUREAD_CLIENT_ID, AZUREAD_CLIENT_SECRET: process.env.AZUREAD_CLIENT_SECRET, AZUREAD_TENANT_ID: process.env.AZUREAD_TENANT_ID, - AIR_TABLE_CLIENT_ID: process.env.AIR_TABLE_CLIENT_ID, + AIRTABLE_CLIENT_ID: process.env.AIRTABLE_CLIENT_ID, ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY, }, }); diff --git a/packages/lib/airtable/service.ts b/packages/lib/airtable/service.ts index 3672ff272d..85e26615f5 100644 --- a/packages/lib/airtable/service.ts +++ b/packages/lib/airtable/service.ts @@ -13,7 +13,7 @@ import { ZIntegrationAirtableTokenSchema, } from "@formbricks/types/integration/airtable"; import { Prisma } from "@prisma/client"; -import { AIR_TABLE_CLIENT_ID } from "../constants"; +import { AIRTABLE_CLIENT_ID } from "../constants"; import { createOrUpdateIntegration, deleteIntegration, getIntegrationByType } from "../integration/service"; interface ConnectAirtableOptions { @@ -122,7 +122,7 @@ export const getAirtableToken = async (environmentId: string) => { const currentDate = new Date(); if (currentDate >= expiryDate) { - const client_id = AIR_TABLE_CLIENT_ID; + const client_id = AIRTABLE_CLIENT_ID; const newToken = await fetchAirtableAuthToken({ grant_type: "refresh_token", diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 76e8257c1d..dce561ed90 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -47,7 +47,7 @@ export const GOOGLE_SHEETS_CLIENT_ID = process.env.GOOGLE_SHEETS_CLIENT_ID; export const GOOGLE_SHEETS_CLIENT_SECRET = process.env.GOOGLE_SHEETS_CLIENT_SECRET; export const GOOGLE_SHEETS_REDIRECT_URL = process.env.GOOGLE_SHEETS_REDIRECT_URL; -export const AIR_TABLE_CLIENT_ID = process.env.AIR_TABLE_CLIENT_ID; +export const AIRTABLE_CLIENT_ID = process.env.AIRTABLE_CLIENT_ID; export const SMTP_HOST = process.env.SMTP_HOST; export const SMTP_PORT = process.env.SMTP_PORT; diff --git a/turbo.json b/turbo.json index d4927a3879..3e69f5a876 100644 --- a/turbo.json +++ b/turbo.json @@ -112,7 +112,7 @@ "TELEMETRY_DISABLED", "VERCEL_URL", "WEBAPP_URL", - "AIR_TABLE_CLIENT_ID", + "AIRTABLE_CLIENT_ID", "AWS_ACCESS_KEY", "AWS_SECRET_KEY", "S3_ACCESS_KEY", From a34606ab03c63c1bf5efe8a4b550d71e6c6692a6 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Thu, 9 Nov 2023 15:36:57 +0530 Subject: [PATCH 04/11] chore: remove unused method and restructure env var fetching for team roles (#1595) Co-authored-by: Matti Nannt --- packages/ee/lib/service.ts | 5 +---- packages/lib/constants.ts | 16 +--------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/ee/lib/service.ts b/packages/ee/lib/service.ts index 9df36423d0..0381c73f95 100644 --- a/packages/ee/lib/service.ts +++ b/packages/ee/lib/service.ts @@ -1,10 +1,7 @@ import "server-only"; -import { env } from "../../../apps/web/env.mjs"; import { unstable_cache } from "next/cache"; - -// Enterprise License constant -export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY; +import { ENTERPRISE_LICENSE_KEY } from "@formbricks/lib/constants"; export const getIsEnterpriseEdition = () => unstable_cache( diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index dce561ed90..4ab78d2d02 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -1,7 +1,5 @@ import "server-only"; import path from "path"; -import { env } from "@/env.mjs"; -import { unstable_cache } from "next/cache"; export const IS_FORMBRICKS_CLOUD = process.env.IS_FORMBRICKS_CLOUD === "1"; export const REVALIDATION_INTERVAL = 0; //TODO: find a good way to cache and revalidate data when it changes @@ -85,16 +83,4 @@ export const LOCAL_UPLOAD_URL = { export const PRICING_USERTARGETING_FREE_MTU = 2500; export const PRICING_APPSURVEYS_FREE_RESPONSES = 250; // Enterprise License constant -export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY; - -export const getIsEnterpriseEdition = () => - unstable_cache( - async () => { - if (ENTERPRISE_LICENSE_KEY) { - return ENTERPRISE_LICENSE_KEY?.length > 0; - } - return false; - }, - ["isEE"], - { revalidate: 60 * 60 * 24 } - )(); +export const ENTERPRISE_LICENSE_KEY = process.env.ENTERPRISE_LICENSE_KEY; From 7acd2ccabbb44d0e4d00636927966bb46383be32 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Thu, 9 Nov 2023 15:42:23 +0530 Subject: [PATCH 05/11] fix: github action that reports stripe usage (#1599) --- .github/workflows/cron-reportUsageToStripe.yml | 8 ++++---- apps/web/app/api/cron/report-usage/route.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cron-reportUsageToStripe.yml b/.github/workflows/cron-reportUsageToStripe.yml index 3a6d20bd96..e7252b02e2 100644 --- a/.github/workflows/cron-reportUsageToStripe.yml +++ b/.github/workflows/cron-reportUsageToStripe.yml @@ -10,13 +10,13 @@ jobs: cron-reportUsageToStripe: env: APP_URL: ${{ secrets.APP_URL }} - API_KEY: ${{ secrets.API_KEY }} + CRON_SECRET: ${{ secrets.CRON_SECRET }} runs-on: ubuntu-latest steps: - name: cURL request - if: ${{ env.APP_URL && env.API_KEY }} + if: ${{ env.APP_URL && env.CRON_SECRET }} run: | curl ${{ env.APP_URL }}/api/cron/report-usage \ - -X GET \ - -H 'x-api-key: ${{ env.API_KEY }}' \ + -X POST \ + -H 'x-api-key: ${{ env.CRON_SECRET }}' \ --fail diff --git a/apps/web/app/api/cron/report-usage/route.ts b/apps/web/app/api/cron/report-usage/route.ts index 57fcdd30aa..30555fc789 100644 --- a/apps/web/app/api/cron/report-usage/route.ts +++ b/apps/web/app/api/cron/report-usage/route.ts @@ -51,7 +51,7 @@ async function reportTeamUsage(team: TTeam) { } } -export async function GET(): Promise { +export async function POST(): Promise { const headersList = headers(); const apiKey = headersList.get("x-api-key"); From a2df7abf854dc02c7ee2ece1b2a8be3313f20148 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Thu, 9 Nov 2023 15:43:25 +0530 Subject: [PATCH 06/11] fix: opt billing page forcefully out of static generation (#1600) --- apps/web/app/(app)/billing-confirmation/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/app/(app)/billing-confirmation/page.tsx b/apps/web/app/(app)/billing-confirmation/page.tsx index 802afb931f..4c36b6f1c1 100644 --- a/apps/web/app/(app)/billing-confirmation/page.tsx +++ b/apps/web/app/(app)/billing-confirmation/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import ConfirmationPage from "./components/ConfirmationPage"; export default function BillingConfirmation({ searchParams }) { From 84ce0c267cbb533fd46ba0b2f7f6b48d77bcee7e Mon Sep 17 00:00:00 2001 From: dominikmukrecki <64379788+dominikmukrecki@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:23:33 +0100 Subject: [PATCH 07/11] docs: Add Google OAuth Integration Guide (#1411) Co-authored-by: Dhruwang --- .../external-auth-providers/page.mdx | 66 +++++++++++++++++++ .../components/docs/Navigation.tsx | 1 + 2 files changed, 67 insertions(+) create mode 100644 apps/formbricks-com/app/docs/self-hosting/external-auth-providers/page.mdx diff --git a/apps/formbricks-com/app/docs/self-hosting/external-auth-providers/page.mdx b/apps/formbricks-com/app/docs/self-hosting/external-auth-providers/page.mdx new file mode 100644 index 0000000000..c2fee35fad --- /dev/null +++ b/apps/formbricks-com/app/docs/self-hosting/external-auth-providers/page.mdx @@ -0,0 +1,66 @@ +export const metadata = { + title: "External auth providers", + description: + "Set up and integrate multiple external authentication providers with Formbricks. Our step-by-step guide covers Google OAuth and more, ensuring a seamless login experience for your users.", +}; + +## Google OAuth Authentication + +Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance. + +### Requirements + +- A Google Cloud Platform (GCP) account. +- A Formbricks instance running and accessible. + +### Steps + +1. **Create a GCP Project**: + + - Navigate to the [GCP Console](https://console.cloud.google.com/). + - From the projects list, select a project or create a new one. + +2. **Setting up OAuth 2.0**: + + - If the **APIs & services** page isn't already open, open the console left side menu and select **APIs & services**. + - On the left, click **Credentials**. + - Click **Create Credentials**, then select **OAuth client ID**. + +3. **Configure OAuth Consent Screen**: + + - If this is your first time creating a client ID, configure your consent screen by clicking **Consent Screen**. + - Fill in the necessary details and under **Authorized domains**, add the domain where your Formbricks instance is hosted. + +4. **Create OAuth 2.0 Client IDs**: + - Select the application type **Web application** for your project and enter any additional information required. + - Ensure to specify authorized JavaScript origins and authorized redirect URIs. + +``` +Authorized JavaScript origins: {WEBAPP_URL} +Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google +``` + +5. **Update Environment Variables in Docker**: + - To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container. + - In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform: + - Alternatively, you can add the environment variables directly to the running container using the following commands (replace `container_id` with your actual Docker container ID): + +``` +docker exec -it container_id /bin/bash +export GOOGLE_AUTH_ENABLED=1 +export GOOGLE_CLIENT_ID=your-client-id-here +export GOOGLE_CLIENT_SECRET=your-client-secret-here +exit +``` + +``` +GOOGLE_AUTH_ENABLED=1 +GOOGLE_CLIENT_ID=your-client-id-here +GOOGLE_CLIENT_SECRET=your-client-secret-here +``` + +6. **Restart Your Formbricks Instance**: + - **Note:** Restarting your Docker containers may cause a brief period of downtime. Plan accordingly. + - Once the environment variables have been updated, it's crucial to restart your Docker containers to apply the changes. This ensures that your Formbricks instance can utilize the new Google OAuth configuration for user authentication. Here's how you can do it: + - Navigate to your Docker setup directory where your `docker-compose.yml` file is located. + - Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration: diff --git a/apps/formbricks-com/components/docs/Navigation.tsx b/apps/formbricks-com/components/docs/Navigation.tsx index 01a2a7c66a..09558feeb3 100644 --- a/apps/formbricks-com/components/docs/Navigation.tsx +++ b/apps/formbricks-com/components/docs/Navigation.tsx @@ -249,6 +249,7 @@ export const navigation: Array = [ { title: "Production", href: "/docs/self-hosting/production" }, { title: "Docker", href: "/docs/self-hosting/docker" }, { title: "Migration Guide", href: "/docs/self-hosting/migration-guide" }, + { title: "External auth providers", href: "/docs/self-hosting/external-auth-providers" }, ], }, { From c2675a9d49bc89a5d145bae556082c2e99d09050 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 10 Nov 2023 12:01:43 +0100 Subject: [PATCH 08/11] fix: infinite loop in onboarding (#1604) --- .../web/app/(app)/onboarding/components/Objective.tsx | 6 ++---- .../app/(app)/onboarding/components/Onboarding.tsx | 7 +------ apps/web/app/(app)/onboarding/components/Role.tsx | 11 ++++------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/apps/web/app/(app)/onboarding/components/Objective.tsx b/apps/web/app/(app)/onboarding/components/Objective.tsx index 59980518c4..108ed1fed6 100644 --- a/apps/web/app/(app)/onboarding/components/Objective.tsx +++ b/apps/web/app/(app)/onboarding/components/Objective.tsx @@ -41,12 +41,10 @@ const Objective: React.FC = ({ next, skip, formbricksResponseId, if (selectedObjective) { try { setIsProfileUpdating(true); - const updatedProfile = { - ...profile, + await updateProfileAction({ objective: selectedObjective.id, name: profile.name ?? undefined, - }; - await updateProfileAction(updatedProfile); + }); setIsProfileUpdating(false); } catch (e) { setIsProfileUpdating(false); diff --git a/apps/web/app/(app)/onboarding/components/Onboarding.tsx b/apps/web/app/(app)/onboarding/components/Onboarding.tsx index 8ae5a7fb60..0dec262f26 100644 --- a/apps/web/app/(app)/onboarding/components/Onboarding.tsx +++ b/apps/web/app/(app)/onboarding/components/Onboarding.tsx @@ -88,12 +88,7 @@ export default function Onboarding({ session, environmentId, profile, product }: )} {currentStep === 2 && ( - + )} {currentStep === 3 && ( void; skip: () => void; setFormbricksResponseId: (id: string) => void; - profile: TProfile; }; type RoleChoice = { @@ -21,7 +19,7 @@ type RoleChoice = { id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other"; }; -const Role: React.FC = ({ next, skip, setFormbricksResponseId, profile }) => { +const Role: React.FC = ({ next, skip, setFormbricksResponseId }) => { const [selectedChoice, setSelectedChoice] = useState(null); const [isUpdating, setIsUpdating] = useState(false); @@ -39,8 +37,7 @@ const Role: React.FC = ({ next, skip, setFormbricksResponseId, profil if (selectedRole) { try { setIsUpdating(true); - const updatedProfile = { ...profile, role: selectedRole.id }; - await updateProfileAction(updatedProfile); + await updateProfileAction({ role: selectedRole.id }); setIsUpdating(false); } catch (e) { setIsUpdating(false); From aa6d6df178100cef1f929a70e208da0b04fefeb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:53:26 +0100 Subject: [PATCH 09/11] chore(deps): bump @sentry/nextjs from 7.76.0 to 7.77.0 (#1603) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/web/package.json | 2 +- pnpm-lock.yaml | 139 +++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 70 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index ee6f5253c5..5fc34ce7f0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,7 +27,7 @@ "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dropdown-menu": "^2.0.6", "@react-email/components": "^0.0.9", - "@sentry/nextjs": "^7.76.0", + "@sentry/nextjs": "^7.77.0", "@t3-oss/env-nextjs": "^0.7.1", "@vercel/og": "^0.5.20", "bcryptjs": "^2.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e31c267a5c..eaa0463eec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - importers: .: @@ -330,8 +326,8 @@ importers: specifier: ^0.0.9 version: 0.0.9(react@18.2.0) '@sentry/nextjs': - specifier: ^7.76.0 - version: 7.76.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0)(webpack@5.89.0) + specifier: ^7.77.0 + version: 7.77.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0)(webpack@5.89.0) '@t3-oss/env-nextjs': specifier: ^0.7.1 version: 0.7.1(zod@3.22.4) @@ -526,7 +522,7 @@ importers: version: 9.0.0(eslint@8.52.0) eslint-config-turbo: specifier: latest - version: 1.8.8(eslint@8.52.0) + version: 1.10.16(eslint@8.52.0) eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.52.0) @@ -7202,24 +7198,24 @@ packages: selderee: 0.11.0 dev: false - /@sentry-internal/tracing@7.76.0: - resolution: {integrity: sha512-QQVIv+LS2sbGf/e5P2dRisHzXpy02dAcLqENLPG4sZ9otRaFNjdFYEqnlJ4qko+ORpJGQEQp/BX7Q/qzZQHlAg==} + /@sentry-internal/tracing@7.77.0: + resolution: {integrity: sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw==} engines: {node: '>=8'} dependencies: - '@sentry/core': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry/core': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 dev: false - /@sentry/browser@7.76.0: - resolution: {integrity: sha512-83xA+cWrBhhkNuMllW5ucFsEO2NlUh2iBYtmg07lp3fyVW+6+b1yMKRnc4RFArJ+Wcq6UO+qk2ZEvrSAts1wEw==} + /@sentry/browser@7.77.0: + resolution: {integrity: sha512-nJ2KDZD90H8jcPx9BysQLiQW+w7k7kISCWeRjrEMJzjtge32dmHA8G4stlUTRIQugy5F+73cOayWShceFP7QJQ==} engines: {node: '>=8'} dependencies: - '@sentry-internal/tracing': 7.76.0 - '@sentry/core': 7.76.0 - '@sentry/replay': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry-internal/tracing': 7.77.0 + '@sentry/core': 7.77.0 + '@sentry/replay': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 dev: false /@sentry/cli@1.75.2(encoding@0.1.13): @@ -7239,26 +7235,26 @@ packages: - supports-color dev: false - /@sentry/core@7.76.0: - resolution: {integrity: sha512-M+ptkCTeCNf6fn7p2MmEb1Wd9/JXUWxIT/0QEc+t11DNR4FYy1ZP2O9Zb3Zp2XacO7ORrlL3Yc+VIfl5JTgjfw==} + /@sentry/core@7.77.0: + resolution: {integrity: sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==} engines: {node: '>=8'} dependencies: - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 dev: false - /@sentry/integrations@7.76.0: - resolution: {integrity: sha512-4ea0PNZrGN9wKuE/8bBCRrxxw4Cq5T710y8rhdKHAlSUpbLqr/atRF53h8qH3Fi+ec0m38PB+MivKem9zUwlwA==} + /@sentry/integrations@7.77.0: + resolution: {integrity: sha512-P055qXgBHeZNKnnVEs5eZYLdy6P49Zr77A1aWJuNih/EenzMy922GOeGy2mF6XYrn1YJSjEwsNMNsQkcvMTK8Q==} engines: {node: '>=8'} dependencies: - '@sentry/core': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry/core': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 localforage: 1.10.0 dev: false - /@sentry/nextjs@7.76.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0)(webpack@5.89.0): - resolution: {integrity: sha512-3/iTnBJ7qOrhoEUQw85CmZ+S2wTZapRui5yfWO6/We11T8q6tvrUPIYmnE0BY/2BIelz4dfPwXRHXRJlgEarhg==} + /@sentry/nextjs@7.77.0(encoding@0.1.13)(next@13.5.6)(react@18.2.0)(webpack@5.89.0): + resolution: {integrity: sha512-8tYPBt5luFjrng1sAMJqNjM9sq80q0jbt6yariADU9hEr7Zk8YqFaOI2/Q6yn9dZ6XyytIRtLEo54kk2AO94xw==} engines: {node: '>=8'} peerDependencies: next: ^10.0.8 || ^11.0 || ^12.0 || ^13.0 || ^14.0 @@ -7269,13 +7265,13 @@ packages: optional: true dependencies: '@rollup/plugin-commonjs': 24.0.0(rollup@2.78.0) - '@sentry/core': 7.76.0 - '@sentry/integrations': 7.76.0 - '@sentry/node': 7.76.0 - '@sentry/react': 7.76.0(react@18.2.0) - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 - '@sentry/vercel-edge': 7.76.0 + '@sentry/core': 7.77.0 + '@sentry/integrations': 7.77.0 + '@sentry/node': 7.77.0 + '@sentry/react': 7.77.0(react@18.2.0) + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 + '@sentry/vercel-edge': 7.77.0 '@sentry/webpack-plugin': 1.20.0(encoding@0.1.13) chalk: 3.0.0 next: 13.5.6(react-dom@18.2.0)(react@18.2.0) @@ -7289,61 +7285,61 @@ packages: - supports-color dev: false - /@sentry/node@7.76.0: - resolution: {integrity: sha512-C+YZ5S5W9oTphdWTBgV+3nDdcV1ldnupIHylHzf2Co+xNtJ76V06N5NjdJ/l9+qvQjMn0DdSp7Uu7KCEeNBT/g==} + /@sentry/node@7.77.0: + resolution: {integrity: sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g==} engines: {node: '>=8'} dependencies: - '@sentry-internal/tracing': 7.76.0 - '@sentry/core': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry-internal/tracing': 7.77.0 + '@sentry/core': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 https-proxy-agent: 5.0.1 transitivePeerDependencies: - supports-color dev: false - /@sentry/react@7.76.0(react@18.2.0): - resolution: {integrity: sha512-FtwB1TjCaHLbyAnEEu3gBdcnh/hhpC+j0dII5bOqp4YvmkGkXfgQcjZskZFX7GydMcRAjWX35s0VRjuBBZu/fA==} + /@sentry/react@7.77.0(react@18.2.0): + resolution: {integrity: sha512-Q+htKzib5em0MdaQZMmPomaswaU3xhcVqmLi2CxqQypSjbYgBPPd+DuhrXKoWYLDDkkbY2uyfe4Lp3yLRWeXYw==} engines: {node: '>=8'} peerDependencies: react: 15.x || 16.x || 17.x || 18.x dependencies: - '@sentry/browser': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry/browser': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: false - /@sentry/replay@7.76.0: - resolution: {integrity: sha512-OACT7MfMHC/YGKnKST8SF1d6znr3Yu8fpUpfVVh2t9TNeh3+cQJVTOliHDqLy+k9Ljd5FtitgSn4IHtseCHDLQ==} + /@sentry/replay@7.77.0: + resolution: {integrity: sha512-M9Ik2J5ekl+C1Och3wzLRZVaRGK33BlnBwfwf3qKjgLDwfKW+1YkwDfTHbc2b74RowkJbOVNcp4m8ptlehlSaQ==} engines: {node: '>=12'} dependencies: - '@sentry-internal/tracing': 7.76.0 - '@sentry/core': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry-internal/tracing': 7.77.0 + '@sentry/core': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 dev: false - /@sentry/types@7.76.0: - resolution: {integrity: sha512-vj6z+EAbVrKAXmJPxSv/clpwS9QjPqzkraMFk2hIdE/kii8s8kwnkBwTSpIrNc8GnzV3qYC4r3qD+BXDxAGPaw==} + /@sentry/types@7.77.0: + resolution: {integrity: sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==} engines: {node: '>=8'} dev: false - /@sentry/utils@7.76.0: - resolution: {integrity: sha512-40jFD+yfQaKpFYINghdhovzec4IEpB7aAuyH/GtE7E0gLpcqnC72r55krEIVILfqIR2Mlr5OKUzyeoCyWAU/yw==} + /@sentry/utils@7.77.0: + resolution: {integrity: sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==} engines: {node: '>=8'} dependencies: - '@sentry/types': 7.76.0 + '@sentry/types': 7.77.0 dev: false - /@sentry/vercel-edge@7.76.0: - resolution: {integrity: sha512-CU/besmv2SWNfVh4v7yVs1VknxU4aG7+kIW001wTYnaNXF8IjV8Bgyn0lDRxFuBXRcrTn8KJO/rUN7aJEmeg4Q==} + /@sentry/vercel-edge@7.77.0: + resolution: {integrity: sha512-ffddPCgxVeAccPYuH5sooZeHBqDuJ9OIhIRYKoDi4TvmwAzWo58zzZWhRpkHqHgIQdQvhLVZ5F+FSQVWnYSOkw==} engines: {node: '>=8'} dependencies: - '@sentry/core': 7.76.0 - '@sentry/types': 7.76.0 - '@sentry/utils': 7.76.0 + '@sentry/core': 7.77.0 + '@sentry/types': 7.77.0 + '@sentry/utils': 7.77.0 dev: false /@sentry/webpack-plugin@1.20.0(encoding@0.1.13): @@ -12840,13 +12836,13 @@ packages: resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==} dev: true - /eslint-config-turbo@1.8.8(eslint@8.52.0): - resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} + /eslint-config-turbo@1.10.16(eslint@8.52.0): + resolution: {integrity: sha512-O3NQI72bQHV7FvSC6lWj66EGx8drJJjuT1kuInn6nbMLOHdMBhSUX/8uhTAlHRQdlxZk2j9HtgFCIzSc93w42g==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.52.0 - eslint-plugin-turbo: 1.8.8(eslint@8.52.0) + eslint-plugin-turbo: 1.10.16(eslint@8.52.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -13048,11 +13044,12 @@ packages: - typescript dev: true - /eslint-plugin-turbo@1.8.8(eslint@8.52.0): - resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} + /eslint-plugin-turbo@1.10.16(eslint@8.52.0): + resolution: {integrity: sha512-ZjrR88MTN64PNGufSEcM0tf+V1xFYVbeiMeuIqr0aiABGomxFLo4DBkQ7WI4WzkZtWQSIA2sP+yxqSboEfL9MQ==} peerDependencies: eslint: '>6.6.0' dependencies: + dotenv: 16.0.3 eslint: 8.52.0 dev: true @@ -23729,3 +23726,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From 11ede2e5179262c737bac40e19835bb543f4e8ef Mon Sep 17 00:00:00 2001 From: Harish Gautam <33349461+harsh9975@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:49:06 +0530 Subject: [PATCH 10/11] fix: Onboarding page added `aria-lable` and keyboard navigation (#1562) Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> --- .../(app)/onboarding/components/Greeting.tsx | 24 ++++++++++++- .../(app)/onboarding/components/Objective.tsx | 22 ++++++++++-- .../(app)/onboarding/components/Product.tsx | 1 + .../app/(app)/onboarding/components/Role.tsx | 23 +++++++++++-- apps/web/app/(app)/onboarding/utils.ts | 34 +++++++++++++++++++ packages/ui/ColorPicker/index.tsx | 2 ++ 6 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 apps/web/app/(app)/onboarding/utils.ts diff --git a/apps/web/app/(app)/onboarding/components/Greeting.tsx b/apps/web/app/(app)/onboarding/components/Greeting.tsx index 197bde421f..af5c4582fe 100644 --- a/apps/web/app/(app)/onboarding/components/Greeting.tsx +++ b/apps/web/app/(app)/onboarding/components/Greeting.tsx @@ -3,6 +3,7 @@ import { Button } from "@formbricks/ui/Button"; import type { Session } from "next-auth"; import Link from "next/link"; +import { useEffect, useRef } from "react"; type Greeting = { next: () => void; @@ -13,6 +14,27 @@ type Greeting = { const Greeting: React.FC = ({ next, skip, name, session }) => { const legacyUser = !session ? false : new Date(session?.user?.createdAt) < new Date("2023-05-03T00:00:00"); // if user is created before onboarding deployment + const buttonRef = useRef(null); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault(); + next(); + } + }; + const button = buttonRef.current; + if (button) { + button.focus(); + button.addEventListener("keydown", handleKeyDown); + } + + return () => { + if (button) { + button.removeEventListener("keydown", handleKeyDown); + } + }; + }, []); return (
@@ -30,7 +52,7 @@ const Greeting: React.FC = ({ next, skip, name, session }) => { -
diff --git a/apps/web/app/(app)/onboarding/components/Objective.tsx b/apps/web/app/(app)/onboarding/components/Objective.tsx index 108ed1fed6..269dc067d3 100644 --- a/apps/web/app/(app)/onboarding/components/Objective.tsx +++ b/apps/web/app/(app)/onboarding/components/Objective.tsx @@ -7,8 +7,9 @@ import { cn } from "@formbricks/lib/cn"; import { TProfileObjective } from "@formbricks/types/profile"; import { TProfile } from "@formbricks/types/profile"; import { Button } from "@formbricks/ui/Button"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; +import { handleTabNavigation } from "../utils"; type ObjectiveProps = { next: () => void; @@ -35,6 +36,16 @@ const Objective: React.FC = ({ next, skip, formbricksResponseId, const [selectedChoice, setSelectedChoice] = useState(null); const [isProfileUpdating, setIsProfileUpdating] = useState(false); + const fieldsetRef = useRef(null); + + useEffect(() => { + const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice); + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [fieldsetRef, setSelectedChoice]); + const handleNextClick = async () => { if (selectedChoice) { const selectedObjective = objectives.find((objective) => objective.label === selectedChoice); @@ -71,14 +82,14 @@ const Objective: React.FC = ({ next, skip, formbricksResponseId, return (
-
- {timeToFinish && <>} + {timeToFinish && ( +
+ +

Takes {calculateTimeToComplete()}

+
+ )}
); } diff --git a/packages/surveys/src/lib/logicEvaluator.ts b/packages/surveys/src/lib/logicEvaluator.ts index 85c4c90af8..f6cd083924 100644 --- a/packages/surveys/src/lib/logicEvaluator.ts +++ b/packages/surveys/src/lib/logicEvaluator.ts @@ -47,7 +47,6 @@ export function evaluateCondition(logic: TSurveyLogic, responseValue: any): bool (Array.isArray(responseValue) && responseValue.length === 0) || responseValue === "" || responseValue === null || - responseValue === undefined || responseValue === "dismissed" ); default: diff --git a/packages/surveys/src/lib/utils.ts b/packages/surveys/src/lib/utils.ts index 9f606ad95d..d513134b4f 100644 --- a/packages/surveys/src/lib/utils.ts +++ b/packages/surveys/src/lib/utils.ts @@ -1,3 +1,5 @@ +import { TSurveyWithTriggers } from "@formbricks/types/js"; + export const cn = (...classes: string[]) => { return classes.filter(Boolean).join(" "); }; @@ -45,3 +47,25 @@ export const shuffleQuestions = (array: any[], shuffleOption: string) => { return arrayCopy; }; + +export const calculateElementIdx = (survey: TSurveyWithTriggers, currentQustionIdx: number): number => { + const currentQuestion = survey.questions[currentQustionIdx]; + const surveyLength = survey.questions.length; + const middleIdx = Math.floor(surveyLength / 2); + const possibleNextQuestions = currentQuestion?.logic?.map((l) => l.destination) || []; + + const getLastQuestionIndex = () => { + const lastQuestion = survey.questions + .filter((q) => possibleNextQuestions.includes(q.id)) + .sort((a, b) => survey.questions.indexOf(a) - survey.questions.indexOf(b)) + .pop(); + return survey.questions.findIndex((e) => e.id === lastQuestion?.id); + }; + + let elementIdx = currentQustionIdx || 0.5; + const lastprevQuestionIdx = getLastQuestionIndex(); + + if (lastprevQuestionIdx > 0) elementIdx = Math.min(middleIdx, lastprevQuestionIdx - 1); + if (possibleNextQuestions.includes("end")) elementIdx = middleIdx; + return elementIdx; +}; diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts index 1b5420dd51..2447d146b4 100644 --- a/packages/types/surveys.ts +++ b/packages/types/surveys.ts @@ -25,7 +25,7 @@ export const ZSurveyWelcomeCard = z.object({ html: z.string().optional(), fileUrl: z.string().optional(), buttonLabel: z.string().optional(), - timeToFinish: z.boolean().default(false), + timeToFinish: z.boolean().default(true), }); export const ZSurveyHiddenFields = z.object({