mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 10:30:00 -06:00
Merge branch 'main' into surveyBg
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -249,6 +249,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ 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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import ConfirmationPage from "./components/ConfirmationPage";
|
||||
|
||||
export default function BillingConfirmation({ searchParams }) {
|
||||
|
||||
@@ -6,8 +6,8 @@ export const fetchTables = async (environmentId: string, baseId: string) => {
|
||||
headers: { environmentId: environmentId },
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
return res.json() as Promise<TIntegrationAirtableTables>;
|
||||
const resJson = await res.json();
|
||||
return resJson.data as Promise<TIntegrationAirtableTables>;
|
||||
};
|
||||
|
||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function EditWelcomeCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="mt-8 flex items-center">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="mr-2">
|
||||
<Switch
|
||||
id="timeToFinish"
|
||||
@@ -176,7 +176,7 @@ export default function EditWelcomeCard({
|
||||
Display an estimate of completion time for survey
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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<Greeting> = ({ 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<HTMLButtonElement | null>(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 (
|
||||
<div className="flex h-full w-full max-w-xl flex-col justify-around gap-8 px-8">
|
||||
@@ -30,7 +52,7 @@ const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
|
||||
<Button size="lg" variant="minimal" onClick={skip}>
|
||||
I'll do it later
|
||||
</Button>
|
||||
<Button size="lg" variant="darkCTA" onClick={next}>
|
||||
<Button size="lg" variant="darkCTA" onClick={next} ref={buttonRef} tabIndex={0}>
|
||||
Begin (1 min)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [isProfileUpdating, setIsProfileUpdating] = useState(false);
|
||||
|
||||
const fieldsetRef = useRef<HTMLFieldSetElement>(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<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
return (
|
||||
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
|
||||
<div className="px-4">
|
||||
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
What do you want to achieve?
|
||||
</label>
|
||||
<label className="block text-sm font-normal leading-6 text-slate-500">
|
||||
We have 85+ templates, help us select the best for your need.
|
||||
</label>
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<fieldset id="choices" aria-label="What do you want to achieve?" ref={fieldsetRef}>
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className=" relative space-y-2 rounded-md">
|
||||
{objectives.map((choice) => (
|
||||
@@ -103,6 +112,11 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleNextClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
|
||||
@@ -88,12 +88,7 @@ export default function Onboarding({ session, environmentId, profile, product }:
|
||||
<Greeting next={next} skip={doLater} name={profile.name ? profile.name : ""} session={session} />
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<Role
|
||||
next={next}
|
||||
skip={skipStep}
|
||||
setFormbricksResponseId={setFormbricksResponseId}
|
||||
profile={profile}
|
||||
/>
|
||||
<Role next={next} skip={skipStep} setFormbricksResponseId={setFormbricksResponseId} />
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<Objective
|
||||
|
||||
@@ -96,6 +96,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId, product })
|
||||
placeholder="e.g. Formbricks"
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
aria-label="Your product name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { updateProfileAction } from "@/app/(app)/onboarding/actions";
|
||||
import { env } from "@/env.mjs";
|
||||
import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
|
||||
import { TProfile } from "@formbricks/types/profile";
|
||||
import { env } from "@/env.mjs";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
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 RoleProps = {
|
||||
next: () => void;
|
||||
skip: () => void;
|
||||
setFormbricksResponseId: (id: string) => void;
|
||||
profile: TProfile;
|
||||
};
|
||||
|
||||
type RoleChoice = {
|
||||
@@ -21,9 +20,18 @@ type RoleChoice = {
|
||||
id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
|
||||
};
|
||||
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profile }) => {
|
||||
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = handleTabNavigation(fieldsetRef, setSelectedChoice);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [fieldsetRef, setSelectedChoice]);
|
||||
|
||||
const roles: Array<RoleChoice> = [
|
||||
{ label: "Project Manager", id: "project_manager" },
|
||||
@@ -39,8 +47,7 @@ const Role: React.FC<RoleProps> = ({ 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);
|
||||
@@ -66,19 +73,20 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
return (
|
||||
<div className="flex w-full max-w-xl flex-col gap-8 px-8">
|
||||
<div className="px-4">
|
||||
<label className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
<label htmlFor="choices" className="mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
What is your role?
|
||||
</label>
|
||||
<label className="block text-sm font-normal leading-6 text-slate-500">
|
||||
Make your Formbricks experience more personalised.
|
||||
</label>
|
||||
<div className="mt-4">
|
||||
<fieldset>
|
||||
<fieldset id="choices" aria-label="What is your role?" ref={fieldsetRef}>
|
||||
<legend className="sr-only">Choices</legend>
|
||||
<div className=" relative space-y-2 rounded-md">
|
||||
{roles.map((choice) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
htmlFor={choice.id}
|
||||
className={cn(
|
||||
selectedChoice === choice.label
|
||||
? "z-10 border-slate-400 bg-slate-100"
|
||||
@@ -90,12 +98,18 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, profil
|
||||
type="radio"
|
||||
id={choice.id}
|
||||
value={choice.label}
|
||||
name="role"
|
||||
checked={choice.label === selectedChoice}
|
||||
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
setSelectedChoice(e.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleNextClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="ml-3 font-medium">
|
||||
{choice.label}
|
||||
|
||||
34
apps/web/app/(app)/onboarding/utils.ts
Normal file
34
apps/web/app/(app)/onboarding/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// util.js
|
||||
export const handleTabNavigation = (fieldsetRef, setSelectedChoice) => (event) => {
|
||||
if (event.key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const radioButtons = fieldsetRef.current?.querySelectorAll('input[type="radio"]');
|
||||
if (!radioButtons || radioButtons.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedRadioButton = fieldsetRef.current?.querySelector(
|
||||
'input[type="radio"]:focus'
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (!focusedRadioButton) {
|
||||
// If no radio button is focused, then it will focus on the first one by default
|
||||
const firstRadioButton = radioButtons[0] as HTMLInputElement;
|
||||
firstRadioButton.focus();
|
||||
setSelectedChoice(firstRadioButton.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedIndex = Array.from(radioButtons).indexOf(focusedRadioButton);
|
||||
const lastIndex = radioButtons.length - 1;
|
||||
|
||||
// Calculating the next index, considering wrapping from the last to the first element
|
||||
const nextIndex = focusedIndex === lastIndex ? 0 : focusedIndex + 1;
|
||||
const nextRadioButton = radioButtons[nextIndex] as HTMLInputElement;
|
||||
nextRadioButton.focus();
|
||||
setSelectedChoice(nextRadioButton.value);
|
||||
};
|
||||
@@ -51,7 +51,7 @@ async function reportTeamUsage(team: TTeam) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
const headersList = headers();
|
||||
const apiKey = headersList.get("x-api-key");
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -27,10 +28,13 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -47,7 +45,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;
|
||||
@@ -84,20 +82,6 @@ export const LOCAL_UPLOAD_URL = {
|
||||
// Pricing
|
||||
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 colours = [
|
||||
"#FFF2D8",
|
||||
@@ -125,3 +109,7 @@ export const colours = [
|
||||
"#F6FDC3",
|
||||
"#CDFAD5",
|
||||
];
|
||||
|
||||
// Enterprise License constant
|
||||
export const ENTERPRISE_LICENSE_KEY = process.env.ENTERPRISE_LICENSE_KEY;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import Progress from "./Progress";
|
||||
import { calculateElementIdx } from "../lib/utils";
|
||||
|
||||
interface ProgressBarProps {
|
||||
survey: TSurveyWithTriggers;
|
||||
@@ -13,6 +14,7 @@ const PROGRESS_INCREMENT = 0.1;
|
||||
export default function ProgressBar({ survey, questionId, brandColor }: ProgressBarProps) {
|
||||
const [progress, setProgress] = useState(0); // [0, 1]
|
||||
const [prevQuestionIdx, setPrevQuestionIdx] = useState(0); // [0, survey.questions.length
|
||||
const [prevQuestionId, setPrevQuestionId] = useState(""); // [0, survey.questions.length
|
||||
|
||||
useEffect(() => {
|
||||
// calculate progress
|
||||
@@ -20,28 +22,10 @@ export default function ProgressBar({ survey, questionId, brandColor }: Progress
|
||||
function calculateProgress(questionId: string, survey: TSurveyWithTriggers, progress: number) {
|
||||
if (survey.questions.length === 0) return 0;
|
||||
if (questionId === "end") return 1;
|
||||
|
||||
let currentQustionIdx = survey.questions.findIndex((e) => e.id === questionId);
|
||||
if (progress > 0 && currentQustionIdx === prevQuestionIdx) return progress;
|
||||
if (progress > 0 && questionId === prevQuestionId) return progress;
|
||||
if (currentQustionIdx === -1) currentQustionIdx = 0;
|
||||
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;
|
||||
const elementIdx = calculateElementIdx(survey, currentQustionIdx);
|
||||
|
||||
const newProgress = elementIdx / survey.questions.length;
|
||||
|
||||
@@ -57,7 +41,7 @@ export default function ProgressBar({ survey, questionId, brandColor }: Progress
|
||||
} else if (newProgress <= progress && progress + PROGRESS_INCREMENT <= 1) {
|
||||
updatedProgress = progress + PROGRESS_INCREMENT;
|
||||
}
|
||||
|
||||
setPrevQuestionId(questionId);
|
||||
setPrevQuestionIdx(currentQustionIdx);
|
||||
return updatedProgress;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export function Survey({
|
||||
timeToFinish={survey.welcomeCard.timeToFinish}
|
||||
brandColor={brandColor}
|
||||
onSubmit={onSubmit}
|
||||
survey={survey}
|
||||
/>
|
||||
);
|
||||
} else if (questionId === "end" && survey.thankYouCard.enabled) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Headline from "./Headline";
|
||||
import HtmlBody from "./HtmlBody";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
import { calculateElementIdx } from "../lib/utils";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
|
||||
interface WelcomeCardProps {
|
||||
headline?: string;
|
||||
@@ -10,8 +12,26 @@ interface WelcomeCardProps {
|
||||
timeToFinish?: boolean;
|
||||
brandColor: string;
|
||||
onSubmit: (data: { [x: string]: any }) => void;
|
||||
survey: TSurveyWithTriggers;
|
||||
}
|
||||
|
||||
const TimerIcon = () => {
|
||||
return (
|
||||
<div className="mr-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-stopwatch"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5V5.6z" />
|
||||
<path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64a.715.715 0 0 1 .012-.013l.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354a.512.512 0 0 1-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5zM8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function WelcomeCard({
|
||||
headline,
|
||||
html,
|
||||
@@ -20,7 +40,36 @@ export default function WelcomeCard({
|
||||
timeToFinish,
|
||||
brandColor,
|
||||
onSubmit,
|
||||
survey,
|
||||
}: WelcomeCardProps) {
|
||||
const calculateTimeToComplete = () => {
|
||||
let idx = calculateElementIdx(survey, 0);
|
||||
if (idx === 0.5) {
|
||||
idx = 1;
|
||||
}
|
||||
const timeInSeconds = (survey.questions.length / idx) * 15; //15 seconds per question.
|
||||
if (timeInSeconds > 360) {
|
||||
// If it's more than 6 minutes
|
||||
return "6+ minutes";
|
||||
}
|
||||
// Calculate minutes, if there are any seconds left, add a minute
|
||||
const minutes = Math.floor(timeInSeconds / 60);
|
||||
const remainingSeconds = timeInSeconds % 60;
|
||||
|
||||
if (remainingSeconds > 0) {
|
||||
// If there are any seconds left, we'll need to round up to the next minute
|
||||
if (minutes === 0) {
|
||||
// If less than 1 minute, return 'less than 1 minute'
|
||||
return "less than 1 minute";
|
||||
} else {
|
||||
// If more than 1 minute, return 'less than X minutes', where X is minutes + 1
|
||||
return `less than ${minutes + 1} minutes`;
|
||||
}
|
||||
}
|
||||
// If there are no remaining seconds, just return the number of minutes
|
||||
return `${minutes} minutes`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fileUrl && (
|
||||
@@ -46,7 +95,12 @@ export default function WelcomeCard({
|
||||
<div className="flex items-center text-xs text-slate-600">Press Enter ↵</div>
|
||||
</div>
|
||||
</div>
|
||||
{timeToFinish && <></>}
|
||||
{timeToFinish && (
|
||||
<div className="item-center mt-4 flex text-slate-500">
|
||||
<TimerIcon />
|
||||
<p className="text-xs">Takes {calculateTimeToComplete()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -15,6 +15,8 @@ export const ColorPicker = ({ color, onChange }: { color: string; onChange: (v:
|
||||
className="ml-2 mr-2 h-10 w-32 flex-1 border-0 bg-transparent text-slate-500 outline-none focus:border-none"
|
||||
color={color}
|
||||
onChange={onChange}
|
||||
id="color"
|
||||
aria-label="Primary color"
|
||||
/>
|
||||
</div>
|
||||
<PopoverPicker color={color} onChange={onChange} />
|
||||
|
||||
139
pnpm-lock.yaml
generated
139
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user