diff --git a/.env.example b/.env.example
index 2f722a67d2..e67e91976f 100644
--- a/.env.example
+++ b/.env.example
@@ -56,6 +56,19 @@ SMTP_PASSWORD=smtpPassword
# Uncomment the variables you would like to use and customize the values.
+##############
+# S3 STORAGE #
+##############
+
+# S3 Storage is required for the file uplaod in serverless environments like Vercel
+S3_ACCESS_KEY=
+S3_SECRET_KEY=
+S3_REGION=
+S3_BUCKET_NAME=
+# Configure a third party S3 compatible storage service endpoint like StorJ leave empty if you use Amazon S3
+# e.g., https://gateway.storjshare.io
+S3_ENDPOINT_URL=
+
#####################
# Disable Features #
#####################
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
index 92366fa462..7537a7480b 100644
--- 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
@@ -122,6 +122,11 @@ These variables can be provided at the runtime i.e. in your docker-compose file.
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user) |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
+| S3_ACCESS_KEY | Access key for S3. | optional (required if S3 is enabled) | |
+| S3_SECRET_KEY | Secret key for S3. | optional (required if S3 is enabled) | |
+| S3_REGION | Region for S3. | optional (required if S3 is enabled) | |
+| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
+| S3_ENDPOINT | Endpoint for S3. | optional (required if S3 is enabled) | |
| PRIVACY_URL | URL for privacy policy. | optional | |
| TERMS_URL | URL for terms of service. | optional | |
| IMPRINT_URL | URL for imprint. | optional | |
@@ -155,7 +160,7 @@ These variables can be provided at the runtime i.e. in your docker-compose file.
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
-| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
+| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
## Build-time Variables
diff --git a/apps/formbricks-com/pages/api/oss-friends/index.ts b/apps/formbricks-com/pages/api/oss-friends/index.ts
index 2805136d10..bccd11f304 100644
--- a/apps/formbricks-com/pages/api/oss-friends/index.ts
+++ b/apps/formbricks-com/pages/api/oss-friends/index.ts
@@ -200,9 +200,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
href: "https://spark-framework.net",
},
{
- "name": "Tiledesk",
- "description": "The innovative open-source framework for developing LLM-enabled chatbots, Tiledesk empowers developers to create advanced, conversational AI agents.",
- "href": "https://tiledesk.com"
+ name: "Tiledesk",
+ description:
+ "The innovative open-source framework for developing LLM-enabled chatbots, Tiledesk empowers developers to create advanced, conversational AI agents.",
+ href: "https://tiledesk.com",
},
{
name: "Tolgee",
diff --git a/apps/formbricks-com/pages/feature-chaser/index.tsx b/apps/formbricks-com/pages/feature-chaser/index.tsx
index 59d0919499..bea06fd642 100644
--- a/apps/formbricks-com/pages/feature-chaser/index.tsx
+++ b/apps/formbricks-com/pages/feature-chaser/index.tsx
@@ -26,7 +26,7 @@ export default function FeatureChaserPage() {
Once you've embedded the Formbricks Widget in your application, you can start following user
- actions. Simply use our No-Code Action wizard to keep track of different actions users perfrom -
+ actions. Simply use our No-Code Action wizard to keep track of different actions users perform -
100% GPDR compliant.
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/layout.tsx
new file mode 100644
index 0000000000..8d20a6d4cc
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/layout.tsx
@@ -0,0 +1,36 @@
+import { Metadata } from "next";
+import { getServerSession } from "next-auth";
+import { notFound } from "next/navigation";
+
+import { authOptions } from "@formbricks/lib/authOptions";
+import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
+import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
+import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
+import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
+
+export const metadata: Metadata = {
+ title: "Billing",
+};
+
+export default async function BillingLayout({ children, params }) {
+ if (!IS_FORMBRICKS_CLOUD) {
+ notFound();
+ }
+
+ const session = await getServerSession(authOptions);
+ const team = await getTeamByEnvironmentId(params.environmentId);
+
+ if (!session) {
+ throw new Error("Unauthorized");
+ }
+ if (!team) {
+ throw new Error("Team not found");
+ }
+
+ const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
+ const { isAdmin, isOwner } = getAccessFlags(currentUserMembership?.role);
+ const isPricingDisabled = !isOwner && !isAdmin;
+
+ return <>{!isPricingDisabled ? <>{children}> : }>;
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx
index cc97c5a210..da026b34ca 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx
@@ -1,37 +1,15 @@
-import { getServerSession } from "next-auth";
-import { notFound } from "next/navigation";
-
-import { authOptions } from "@formbricks/lib/authOptions";
-import {
- IS_FORMBRICKS_CLOUD,
- PRICING_APPSURVEYS_FREE_RESPONSES,
- PRICING_USERTARGETING_FREE_MTU,
-} from "@formbricks/lib/constants";
-import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
-import { getAccessFlags } from "@formbricks/lib/membership/utils";
+import { PRICING_APPSURVEYS_FREE_RESPONSES, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
import {
getMonthlyActiveTeamPeopleCount,
getMonthlyTeamResponseCount,
getTeamByEnvironmentId,
} from "@formbricks/lib/team/service";
-import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import SettingsTitle from "../components/SettingsTitle";
import PricingTable from "./components/PricingTable";
export default async function BillingPage({ params }) {
- if (!IS_FORMBRICKS_CLOUD) {
- notFound();
- }
-
- const session = await getServerSession(authOptions);
-
const team = await getTeamByEnvironmentId(params.environmentId);
-
- if (!session) {
- throw new Error("Unauthorized");
- }
-
if (!team) {
throw new Error("Team not found");
}
@@ -40,26 +18,19 @@ export default async function BillingPage({ params }) {
getMonthlyActiveTeamPeopleCount(team.id),
getMonthlyTeamResponseCount(team.id),
]);
- const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
- const { isAdmin, isOwner } = getAccessFlags(currentUserMembership?.role);
- const isPricingDisabled = !isOwner && !isAdmin;
return (
<>
- {!isPricingDisabled ? (
-
- ) : (
-
- )}
+
>
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited/page.tsx
index cdef01cb6e..d756971357 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited/page.tsx
@@ -1,34 +1,20 @@
-import { getServerSession } from "next-auth";
-import { notFound, redirect } from "next/navigation";
+import { redirect } from "next/navigation";
import { StripePriceLookupKeys } from "@formbricks/ee/billing/lib/constants";
-import { authOptions } from "@formbricks/lib/authOptions";
-import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { upgradePlanAction } from "../actions";
export default async function UnlimitedPage({ params }) {
- if (!IS_FORMBRICKS_CLOUD) {
- notFound();
- }
-
- const session = await getServerSession(authOptions);
-
const team = await getTeamByEnvironmentId(params.environmentId);
-
- if (!session) {
- throw new Error("Unauthorized");
- }
-
if (!team) {
throw new Error("Team not found");
}
const { status, newPlan, url } = await upgradePlanAction(team.id, params.environmentId, [
- StripePriceLookupKeys.inAppSurveyUnlimited,
- StripePriceLookupKeys.linkSurveyUnlimited,
- StripePriceLookupKeys.userTargetingUnlimited,
+ StripePriceLookupKeys.inAppSurveyUnlimitedPlan90,
+ StripePriceLookupKeys.linkSurveyUnlimitedPlan19,
+ StripePriceLookupKeys.userTargetingUnlimitedPlan90,
]);
if (status != 200) {
throw new Error("Something went wrong");
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited99/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited99/page.tsx
new file mode 100644
index 0000000000..1d761aca55
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/unlimited99/page.tsx
@@ -0,0 +1,29 @@
+import { redirect } from "next/navigation";
+
+import { StripePriceLookupKeys } from "@formbricks/ee/billing/lib/constants";
+import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
+
+import { upgradePlanAction } from "../actions";
+
+export default async function UnlimitedPage({ params }) {
+ const team = await getTeamByEnvironmentId(params.environmentId);
+ if (!team) {
+ throw new Error("Team not found");
+ }
+
+ const { status, newPlan, url } = await upgradePlanAction(team.id, params.environmentId, [
+ StripePriceLookupKeys.inAppSurveyUnlimitedPlan33,
+ StripePriceLookupKeys.linkSurveyUnlimitedPlan33,
+ StripePriceLookupKeys.userTargetingUnlimitedPlan33,
+ ]);
+ if (status != 200) {
+ throw new Error("Something went wrong");
+ }
+ if (newPlan && url) {
+ redirect(url);
+ } else if (!newPlan) {
+ redirect(`/billing-confirmation?environmentId=${params.environmentId}`);
+ } else {
+ throw new Error("Something went wrong");
+ }
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx
index 99736233a6..1a99c6a336 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx
@@ -53,7 +53,10 @@ const ResponsePage = ({
const { selectedFilter, dateRange, resetState } = useResponseFilter();
- const filters = useMemo(() => getFormattedFilters(selectedFilter, dateRange), [selectedFilter, dateRange]);
+ const filters = useMemo(
+ () => getFormattedFilters(survey, selectedFilter, dateRange),
+ [survey, selectedFilter, dateRange]
+ );
const searchParams = useSearchParams();
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
index 57d45d4f0b..100dea4514 100755
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
@@ -86,7 +86,10 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps
setSelectedOptions({ questionFilterOptions, questionOptions });
}, [survey, setSelectedOptions, environmentTags, attributes]);
- const filters = useMemo(() => getFormattedFilters(selectedFilter, dateRange), [selectedFilter, dateRange]);
+ const filters = useMemo(
+ () => getFormattedFilters(survey, selectedFilter, dateRange),
+ [survey, selectedFilter, dateRange]
+ );
const datePickerRef = useRef(null);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx
index f6ac53b6af..72e58d245a 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx
@@ -44,7 +44,9 @@ const QuestionFilterComboBox = ({
// multiple when question type is multi selection
const isMultiple =
- type === TSurveyQuestionType.MultipleChoiceMulti || type === TSurveyQuestionType.MultipleChoiceSingle;
+ type === TSurveyQuestionType.MultipleChoiceMulti ||
+ type === TSurveyQuestionType.MultipleChoiceSingle ||
+ type === TSurveyQuestionType.PictureSelection;
// when question type is multi selection so we remove the option from the options which has been already selected
const options = isMultiple
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx
index ee34245f85..92b6e0816c 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx
@@ -5,6 +5,7 @@ import {
CursorArrowRippleIcon,
HashtagIcon,
ListBulletIcon,
+ PhotoIcon,
QuestionMarkCircleIcon,
QueueListIcon,
StarIcon,
@@ -60,6 +61,8 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial;
case TSurveyQuestionType.Consent:
return ;
+ case TSurveyQuestionType.PictureSelection:
+ return ;
}
}
if (type === OptionsType.ATTRIBUTES) {
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx
index 7f66533c6e..11f5374014 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx
@@ -52,7 +52,7 @@ export default function Modal({
return {
transform: `scale(${scaleValue})`,
- "transform-origin": placementClass,
+ transformOrigin: placementClass,
};
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx
index aba7be092f..06c2b98698 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx
@@ -241,8 +241,9 @@ export default function PreviewSurvey({
-