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/.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/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/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" }, ], }, { 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 }) { 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/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx index 36c6f064e1..f2e7083161 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditWelcomeCard.tsx @@ -157,7 +157,7 @@ export default function EditWelcomeCard({ - {/*
+
-
*/} +
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts index e1a155982a..5a25830bb3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts @@ -18,7 +18,7 @@ const welcomeCardDefault: TSurveyWelcomeCard = { enabled: false, headline: "Welcome!", html: "Thanks for providing your feedback - let's go!", - timeToFinish: false, + timeToFinish: true, }; export const templates: TTemplate[] = [ 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 59980518c4..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,18 +36,26 @@ 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); 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); @@ -73,14 +82,14 @@ const Objective: React.FC = ({ next, skip, formbricksResponseId, return (
-