diff --git a/.env.example b/.env.example
index 3a1f166417..166a783e5b 100644
--- a/.env.example
+++ b/.env.example
@@ -108,4 +108,7 @@ STRIPE_WEBHOOK_SECRET=
# Configure Formbricks usage within Formbricks
NEXT_PUBLIC_FORMBRICKS_API_HOST=
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
-NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
\ No newline at end of file
+NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
+
+# Cron Secret
+CRON_SECRET=
\ No newline at end of file
diff --git a/.github/workflows/cron-weeklySummary.yml b/.github/workflows/cron-weeklySummary.yml
new file mode 100644
index 0000000000..b17c5bc113
--- /dev/null
+++ b/.github/workflows/cron-weeklySummary.yml
@@ -0,0 +1,23 @@
+name: Cron - weeklySummary
+
+on:
+ # "Scheduled workflows run on the latest commit on the default or base branch."
+ # β https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
+ schedule:
+ # Runs βAt 08:00 on Monday.β (see https://crontab.guru)
+ - cron: "0 8 * * 1"
+jobs:
+ cron-weeklySummary:
+ env:
+ APP_URL: ${{ secrets.APP_URL }}
+ CRON_API_KEY: ${{ secrets.CRON_SECRET }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: cURL request
+ if: ${{ env.APP_URL && env.CRON_SECRET }}
+ run: |
+ curl ${{ secrets.APP_URL }}/api/cron/weekly_summary \
+ -X POST \
+ -H 'content-type: application/json' \
+ -H 'authorization: ${{ secrets.CRON_SECRET }}' \
+ --fail
diff --git a/apps/web/app/api/cron/weekly_summary/email.ts b/apps/web/app/api/cron/weekly_summary/email.ts
new file mode 100644
index 0000000000..5e42a6083a
--- /dev/null
+++ b/apps/web/app/api/cron/weekly_summary/email.ts
@@ -0,0 +1,228 @@
+import { sendEmail } from "@/lib/email";
+import { withEmailTemplate } from "@/lib/email-template";
+import { WEBAPP_URL } from "@formbricks/lib/constants";
+import { Insights, NotificationResponse, Survey, SurveyResponse } from "./types";
+
+const getEmailSubject = (productName: string) => {
+ return `${productName} User Insights - Last Week by Formbricks`;
+};
+
+const notificationHeader = (
+ productName: string,
+ startDate: string,
+ endDate: string,
+ startYear: number,
+ endYear: number
+) =>
+ `
+
+
+
Hey π
+
+
+
Weekly Report for ${productName}
+ ${getNotificationHeaderimePeriod(startDate, endDate, startYear, endYear)}
+
+
+
+
+ `;
+
+const getNotificationHeaderimePeriod = (
+ startDate: string,
+ endDate: string,
+ startYear: number,
+ endYear: number
+) => {
+ if (startYear == endYear) {
+ return `${startDate} - ${endDate} ${endYear}
`;
+ } else {
+ return `${startDate} ${startYear} - ${endDate} ${endYear}
`;
+ }
+};
+
+const notificationInsight = (insights: Insights) =>
+ `
+
+
+
+ Surveys
+ ${insights.numLiveSurvey}
+
+
+ Displays
+ ${insights.totalDisplays}
+
+
+ Responses
+ ${insights.totalResponses}
+
+
+ Completed
+ ${insights.totalCompletedResponses}
+
+
+ Completion %
+ ${insights.completionRate.toFixed(2)}%
+
+
+
+
+`;
+
+function convertSurveyStatus(status) {
+ const statusMap = {
+ inProgress: "Live",
+ paused: "Paused",
+ completed: "Completed",
+ };
+
+ return statusMap[status] || status;
+}
+
+const getButtonLabel = (count) => {
+ if (count === 1) {
+ return "View Response";
+ }
+ return `View ${count > 2 ? count - 1 : "1"} more Response${count > 2 ? "s" : ""}`;
+};
+
+const notificationLiveSurveys = (surveys: Survey[], environmentId: string) => {
+ if (!surveys.length) return ` `;
+
+ return surveys
+ .filter((survey) => survey.responses.length > 0)
+ .map((survey) => {
+ const displayStatus = convertSurveyStatus(survey.status);
+ const isLive = displayStatus === "Live";
+
+ return `
+ `;
+ })
+ .join("");
+};
+
+const createSurveyFields = (surveryResponses: SurveyResponse[]) => {
+ let surveyFields = "";
+ const responseCount = surveryResponses.length;
+
+ surveryResponses.forEach((response, index) => {
+ if (!response) {
+ return;
+ }
+
+ for (const [headline, answer] of Object.entries(response)) {
+ surveyFields += `
+
+
${headline}
+
${answer}
+
+ `;
+ }
+
+ // Add only when there are 2 or more responses to display, and it's not the last response
+ if (responseCount >= 2 && index < responseCount - 1) {
+ surveyFields += " ";
+ }
+ });
+
+ return surveyFields;
+};
+
+const notificationFooter = () => {
+ return `
+ All the best,
+ The Formbricks Team
+ This is a Beta feature. If you experience any issues, please let us know by replying to this email π
+ `;
+};
+
+const createReminderNotificationBody = (notificationData: NotificationResponse, webUrl) => {
+ return `
+ Weβd love to send you a Weekly Summary, but currently there are no surveys running for ${notificationData.productName}.
+
+ Donβt let a week pass without learning about your users:
+
+ Setup a new survey
+
+
+ Need help finding the right survey for your product? Pick a 15-minute slot in our CEOs calendar or reply to this email :)
+
+
+ All the best,
+ The Formbricks Team
+
+ This is a Beta feature. If you experience any issues, please let us know by replying to this email π
+ `;
+};
+
+export const sendWeeklySummaryNotificationEmail = async (
+ email: string,
+ notificationData: NotificationResponse
+) => {
+ const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ const startDate = `${notificationData.lastWeekDate.getDate()} ${
+ monthNames[notificationData.lastWeekDate.getMonth()]
+ }`;
+ const endDate = `${notificationData.currentDate.getDate()} ${
+ monthNames[notificationData.currentDate.getMonth()]
+ }`;
+ const startYear = notificationData.lastWeekDate.getFullYear();
+ const endYear = notificationData.currentDate.getFullYear();
+ await sendEmail({
+ to: email,
+ subject: getEmailSubject(notificationData.productName),
+ html: withEmailTemplate(`
+ ${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
+ ${notificationInsight(notificationData.insights)}
+ ${notificationLiveSurveys(notificationData.surveys, notificationData.environmentId)}
+ ${notificationFooter()}
+ `),
+ });
+};
+
+export const sendNoLiveSurveyNotificationEmail = async (
+ email: string,
+ notificationData: NotificationResponse
+) => {
+ const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ const startDate = `${notificationData.lastWeekDate.getDate()} ${
+ monthNames[notificationData.lastWeekDate.getMonth()]
+ }`;
+ const endDate = `${notificationData.currentDate.getDate()} ${
+ monthNames[notificationData.currentDate.getMonth()]
+ }`;
+ const startYear = notificationData.lastWeekDate.getFullYear();
+ const endYear = notificationData.currentDate.getFullYear();
+ await sendEmail({
+ to: email,
+ subject: getEmailSubject(notificationData.productName),
+ html: withEmailTemplate(`
+ ${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
+ ${createReminderNotificationBody(notificationData, WEBAPP_URL)}
+ `),
+ });
+};
diff --git a/apps/web/app/api/cron/weekly_summary/route.ts b/apps/web/app/api/cron/weekly_summary/route.ts
new file mode 100644
index 0000000000..0fce4f448f
--- /dev/null
+++ b/apps/web/app/api/cron/weekly_summary/route.ts
@@ -0,0 +1,187 @@
+import { responses } from "@/lib/api/response";
+import { prisma } from "@formbricks/database";
+import { CRON_SECRET } from "@formbricks/lib/constants";
+import { headers } from "next/headers";
+import { NextResponse } from "next/server";
+import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "./email";
+import { EnvironmentData, NotificationResponse, ProductData, Survey, SurveyResponse } from "./types";
+
+export async function POST(): Promise {
+ // check authentication with x-api-key header and CRON_SECRET env variable
+ if (headers().get("x-api-key") !== CRON_SECRET) {
+ return responses.notAuthenticatedResponse();
+ }
+
+ // list of email sending promises to wait for
+ const emailSendingPromises: Promise[] = [];
+
+ const products = await getProducts();
+
+ // iterate through the products and send weekly summary email to each team member
+ for await (const product of products) {
+ // check if there are team members that have weekly summary notification enabled
+ const teamMembers = product.team.memberships;
+ const teamMembersWithNotificationEnabled = teamMembers.filter((member) => {
+ return (
+ member.user.notificationSettings?.weeklySummary &&
+ member.user.notificationSettings.weeklySummary[product.id]
+ );
+ });
+ // if there are no team members with weekly summary notification enabled, skip to the next product (do not send email)
+ if (teamMembersWithNotificationEnabled.length == 0) {
+ continue;
+ }
+ // calculate insights for the product
+ const notificationResponse = getNotificationResponse(product.environments[0], product.name);
+
+ // if there were no responses in the last 7 days, send a different email
+ if (notificationResponse.insights.totalCompletedResponses == 0) {
+ for (const teamMember of teamMembersWithNotificationEnabled) {
+ emailSendingPromises.push(
+ sendNoLiveSurveyNotificationEmail(teamMember.user.email, notificationResponse)
+ );
+ }
+ continue;
+ }
+
+ // send weekly summary email
+ for (const teamMember of teamMembersWithNotificationEnabled) {
+ emailSendingPromises.push(
+ sendWeeklySummaryNotificationEmail(teamMember.user.email, notificationResponse)
+ );
+ }
+ }
+ // wait for all emails to be sent
+ await Promise.all(emailSendingPromises);
+ return responses.successResponse({}, true);
+}
+
+const getNotificationResponse = (environment: EnvironmentData, productName: string): NotificationResponse => {
+ const insights = {
+ totalCompletedResponses: 0,
+ totalDisplays: 0,
+ totalResponses: 0,
+ completionRate: 0,
+ numLiveSurvey: 0,
+ };
+
+ const surveys: Survey[] = [];
+
+ // iterate through the surveys and calculate the overall insights
+ for (const survey of environment.surveys) {
+ const surveyData: Survey = {
+ id: survey.id,
+ name: survey.name,
+ status: survey.status,
+ responsesCount: survey.responses.length,
+ responses: [],
+ };
+ // iterate through the responses and calculate the survey insights
+ for (const response of survey.responses) {
+ // only take the first 3 responses
+ if (surveyData.responses.length >= 1) {
+ break;
+ }
+ const surveyResponse: SurveyResponse = {};
+ for (const question of survey.questions) {
+ const headline = question.headline;
+ const answer = response.data[question.id]?.toString() || null;
+ if (answer === null || answer === "" || answer?.length === 0) {
+ continue;
+ }
+ surveyResponse[headline] = answer;
+ }
+ surveyData.responses.push(surveyResponse);
+ }
+ surveys.push(surveyData);
+ // calculate the overall insights
+ if (survey.status == "inProgress") {
+ insights.numLiveSurvey += 1;
+ }
+ insights.totalCompletedResponses += survey.responses.filter((r) => r.finished).length;
+ insights.totalDisplays += survey.displays.length;
+ insights.totalResponses += survey.responses.length;
+ insights.completionRate = Math.round((insights.totalCompletedResponses / insights.totalDisplays) * 100);
+ }
+ // build the notification response needed for the emails
+ const lastWeekDate = new Date();
+ lastWeekDate.setDate(lastWeekDate.getDate() - 7);
+ return {
+ environmentId: environment.id,
+ currentDate: new Date(),
+ lastWeekDate,
+ productName: productName,
+ surveys,
+ insights,
+ };
+};
+
+const getProducts = async (): Promise => {
+ // gets all products together with team members, surveys, responses, and displays for the last 7 days
+ const sevenDaysAgo = new Date();
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
+
+ return await prisma.product.findMany({
+ select: {
+ id: true,
+ name: true,
+ environments: {
+ where: {
+ type: "production",
+ },
+ select: {
+ id: true,
+ surveys: {
+ where: {
+ status: {
+ not: "draft",
+ },
+ },
+ select: {
+ id: true,
+ name: true,
+ questions: true,
+ status: true,
+ responses: {
+ where: {
+ createdAt: {
+ gte: sevenDaysAgo,
+ },
+ },
+ select: {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ finished: true,
+ data: true,
+ },
+ orderBy: {
+ createdAt: "desc",
+ },
+ },
+ displays: {
+ select: {
+ status: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ team: {
+ select: {
+ memberships: {
+ select: {
+ user: {
+ select: {
+ email: true,
+ notificationSettings: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+};
diff --git a/apps/web/app/api/cron/weekly_summary/types.ts b/apps/web/app/api/cron/weekly_summary/types.ts
new file mode 100644
index 0000000000..0b3a6b0995
--- /dev/null
+++ b/apps/web/app/api/cron/weekly_summary/types.ts
@@ -0,0 +1,81 @@
+import { TResponseData } from "@formbricks/types/v1/responses";
+import { TSurveyQuestion } from "@formbricks/types/v1/surveys";
+import { TUserNotificationSettings } from "@formbricks/types/v1/users";
+import { DisplayStatus, SurveyStatus } from "@prisma/client";
+
+export interface Insights {
+ totalCompletedResponses: number;
+ totalDisplays: number;
+ totalResponses: number;
+ completionRate: number;
+ numLiveSurvey: number;
+}
+
+export interface SurveyResponse {
+ [headline: string]: string | number | boolean | Date | string[];
+}
+
+export interface Survey {
+ id: string;
+ name: string;
+ responses: SurveyResponse[];
+ responsesCount: number;
+ status: string;
+}
+
+export interface NotificationResponse {
+ environmentId: string;
+ currentDate: Date;
+ lastWeekDate: Date;
+ productName: string;
+ surveys: Survey[];
+ insights: Insights;
+}
+
+// Prisma Types
+
+type ResponseData = {
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ finished: boolean;
+ data: TResponseData;
+};
+
+type DisplayData = {
+ status: DisplayStatus;
+};
+
+type SurveyData = {
+ id: string;
+ name: string;
+ questions: TSurveyQuestion[];
+ status: SurveyStatus;
+ responses: ResponseData[];
+ displays: DisplayData[];
+};
+
+export type EnvironmentData = {
+ id: string;
+ surveys: SurveyData[];
+};
+
+type UserData = {
+ email: string;
+ notificationSettings: TUserNotificationSettings;
+};
+
+type MembershipData = {
+ user: UserData;
+};
+
+type TeamData = {
+ memberships: MembershipData[];
+};
+
+export type ProductData = {
+ id: string;
+ name: string;
+ environments: EnvironmentData[];
+ team: TeamData;
+};
diff --git a/apps/web/app/environments/[environmentId]/settings/SettingsCard.tsx b/apps/web/app/environments/[environmentId]/settings/SettingsCard.tsx
index c7c7c4cf4a..a75a184b20 100644
--- a/apps/web/app/environments/[environmentId]/settings/SettingsCard.tsx
+++ b/apps/web/app/environments/[environmentId]/settings/SettingsCard.tsx
@@ -8,6 +8,7 @@ export default function SettingsCard({
soon = false,
noPadding = false,
dangerZone,
+ beta,
}: {
title: string;
description: string;
@@ -15,18 +16,19 @@ export default function SettingsCard({
soon?: boolean;
noPadding?: boolean;
dangerZone?: boolean;
+ beta?: boolean;
}) {
return (
-
+
{title}
- {soon && }
+
+ {beta && }
+ {soon && }
+
{description}
diff --git a/apps/web/app/environments/[environmentId]/settings/notifications/AlertSwitch.tsx b/apps/web/app/environments/[environmentId]/settings/notifications/AlertSwitch.tsx
deleted file mode 100644
index 9e2ed25f1f..0000000000
--- a/apps/web/app/environments/[environmentId]/settings/notifications/AlertSwitch.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-"use client";
-
-import { Switch } from "@formbricks/ui";
-import { useRouter } from "next/navigation";
-import toast from "react-hot-toast";
-import { updateNotificationSettings } from "./actions";
-
-interface AlertSwitchProps {
- surveyId: string;
- userId: string;
- notificationSettings: any;
-}
-
-export function AlertSwitch({ surveyId, userId, notificationSettings }: AlertSwitchProps) {
- const router = useRouter();
-
- return (
-
{
- // update notificiation settings
- const updatedNotificationSettings = { ...notificationSettings };
- updatedNotificationSettings[surveyId]["responseFinished"] =
- !updatedNotificationSettings[surveyId]["responseFinished"];
- // update db
- await updateNotificationSettings(userId, notificationSettings);
- // show success message if toggled on, different message if toggled off
- if (updatedNotificationSettings[surveyId]["responseFinished"]) {
- toast.success(`Every new response is coming your way.`);
- } else {
- toast.success(`You won't receive notifications anymore.`);
- }
- router.refresh();
- }}
- />
- );
-}
diff --git a/apps/web/app/environments/[environmentId]/settings/notifications/EditAlerts.tsx b/apps/web/app/environments/[environmentId]/settings/notifications/EditAlerts.tsx
index 6bfeae6048..16e2035121 100644
--- a/apps/web/app/environments/[environmentId]/settings/notifications/EditAlerts.tsx
+++ b/apps/web/app/environments/[environmentId]/settings/notifications/EditAlerts.tsx
@@ -1,32 +1,9 @@
-import { AlertSwitch } from "./AlertSwitch";
-import { Switch, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
import { QuestionMarkCircleIcon, UsersIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import type { NotificationSettings } from "@formbricks/types/users";
+import { NotificationSwitch } from "./NotificationSwitch";
import { Membership, User } from "./types";
-const cleanNotificationSettings = (notificationSettings: NotificationSettings, memberships: Membership[]) => {
- const newNotificationSettings = {};
- for (const membership of memberships) {
- for (const product of membership.team.products) {
- for (const environment of product.environments) {
- for (const survey of environment.surveys) {
- // check if the user has notification settings for this survey
- if (notificationSettings[survey.id]) {
- newNotificationSettings[survey.id] = notificationSettings[survey.id];
- } else {
- newNotificationSettings[survey.id] = {
- responseFinished: false,
- weeklySummary: false,
- };
- }
- }
- }
- }
- }
- return newNotificationSettings;
-};
-
interface EditAlertsProps {
memberships: Membership[];
user: User;
@@ -34,8 +11,6 @@ interface EditAlertsProps {
}
export default function EditAlerts({ memberships, user, environmentId }: EditAlertsProps) {
- user.notificationSettings = cleanNotificationSettings(user.notificationSettings, memberships);
-
return (
<>
{memberships.map((membership) => (
@@ -47,9 +22,9 @@ export default function EditAlerts({ memberships, user, environmentId }: EditAle
{membership.team.name}
-
-
Product
+
Survey
+
Product
@@ -60,48 +35,45 @@ export default function EditAlerts({ memberships, user, environmentId }: EditAle
Sends complete responses, no partials.
-
-
-
-
- Weekly Summary
-
- Coming soon π
-
-
-
- {membership.team.products.map((product) => (
-
- {product.environments.map((environment) => (
-
- {environment.surveys.map((survey) => (
-
-
- {product?.name}
+ {membership.team.products.some((product) =>
+ product.environments.some((environment) => environment.surveys.length > 0)
+ ) ? (
+
+ {membership.team.products.map((product) => (
+
+ {product.environments.map((environment) => (
+
+ {environment.surveys.map((survey) => (
+
+
+
+ {product?.name}
+
+
+
+
-
-
-
-
-
-
- ))}
-
- ))}
-
- ))}
-
+ ))}
+
+ ))}
+
+ ))}
+
+ ) : (
+
+ )}
Want to loop in team mates?{" "}
diff --git a/apps/web/app/environments/[environmentId]/settings/notifications/EditWeeklySummary.tsx b/apps/web/app/environments/[environmentId]/settings/notifications/EditWeeklySummary.tsx
new file mode 100644
index 0000000000..eb3be2ab06
--- /dev/null
+++ b/apps/web/app/environments/[environmentId]/settings/notifications/EditWeeklySummary.tsx
@@ -0,0 +1,56 @@
+import { UsersIcon } from "@heroicons/react/24/solid";
+import Link from "next/link";
+import { NotificationSwitch } from "./NotificationSwitch";
+import { Membership, User } from "./types";
+
+interface EditAlertsProps {
+ memberships: Membership[];
+ user: User;
+ environmentId: string;
+}
+
+export default function EditWeeklySummary({ memberships, user, environmentId }: EditAlertsProps) {
+ return (
+ <>
+ {memberships.map((membership) => (
+ <>
+
+
+
+
+
{membership.team.name}
+
+
+
+
Product
+
Weekly Summary
+
+
+ {membership.team.products.map((product) => (
+
+
{product?.name}
+
+
+
+
+ ))}
+
+
+ Want to loop in team mates?{" "}
+
+ Invite them.
+
+
+
+ >
+ ))}
+ >
+ );
+}
diff --git a/apps/web/app/environments/[environmentId]/settings/notifications/NotificationSwitch.tsx b/apps/web/app/environments/[environmentId]/settings/notifications/NotificationSwitch.tsx
new file mode 100644
index 0000000000..b33a9f7618
--- /dev/null
+++ b/apps/web/app/environments/[environmentId]/settings/notifications/NotificationSwitch.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { Switch } from "@formbricks/ui";
+import { useRouter } from "next/navigation";
+import toast from "react-hot-toast";
+import { updateNotificationSettings } from "./actions";
+import { NotificationSettings } from "@formbricks/types/users";
+import { useState } from "react";
+
+interface NotificationSwitchProps {
+ surveyOrProductId: string;
+ userId: string;
+ notificationSettings: NotificationSettings;
+ notificationType: "alert" | "weeklySummary";
+}
+
+export function NotificationSwitch({
+ surveyOrProductId,
+ userId,
+ notificationSettings,
+ notificationType,
+}: NotificationSwitchProps) {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+
+ return (
+
{
+ setIsLoading(true);
+ // update notificiation settings
+ const updatedNotificationSettings = { ...notificationSettings };
+ updatedNotificationSettings[notificationType][surveyOrProductId] =
+ !updatedNotificationSettings[notificationType][surveyOrProductId];
+ await updateNotificationSettings(userId, notificationSettings);
+ setIsLoading(false);
+ toast.success(`Notification settings updated`, { id: "notification-switch" });
+ router.refresh();
+ }}
+ />
+ );
+}
diff --git a/apps/web/app/environments/[environmentId]/settings/notifications/page.tsx b/apps/web/app/environments/[environmentId]/settings/notifications/page.tsx
index 306a6afed3..9cf2499322 100644
--- a/apps/web/app/environments/[environmentId]/settings/notifications/page.tsx
+++ b/apps/web/app/environments/[environmentId]/settings/notifications/page.tsx
@@ -1,9 +1,11 @@
+import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import SettingsCard from "@/app/environments/[environmentId]/settings/SettingsCard";
+import { prisma } from "@formbricks/database";
+import { NotificationSettings } from "@formbricks/types/users";
+import { getServerSession } from "next-auth";
import SettingsTitle from "../SettingsTitle";
import EditAlerts from "./EditAlerts";
-import { prisma } from "@formbricks/database";
-import { getServerSession } from "next-auth";
-import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
+import EditWeeklySummary from "./EditWeeklySummary";
import type { Membership, User } from "./types";
async function getUser(userId: string | undefined): Promise {
@@ -29,6 +31,27 @@ async function getUser(userId: string | undefined): Promise {
return user;
}
+function cleanNotificationSettings(notificationSettings: NotificationSettings, memberships: Membership[]) {
+ const newNotificationSettings = { alert: {}, weeklySummary: {} };
+ for (const membership of memberships) {
+ for (const product of membership.team.products) {
+ // set default values for weekly summary
+ newNotificationSettings.weeklySummary[product.id] =
+ (notificationSettings.weeklySummary && notificationSettings.weeklySummary[product.id]) || false;
+ // set default values for alerts
+ for (const environment of product.environments) {
+ for (const survey of environment.surveys) {
+ newNotificationSettings.alert[survey.id] =
+ notificationSettings[survey.id]?.responseFinished ||
+ (notificationSettings.alert && notificationSettings.alert[survey.id]) ||
+ false; // check for legacy notification settings w/o "alerts" key
+ }
+ }
+ }
+ }
+ return newNotificationSettings;
+}
+
async function getMemberships(userId: string): Promise {
const memberships = await prisma.membership.findMany({
where: {
@@ -72,13 +95,22 @@ export default async function ProfileSettingsPage({ params }) {
throw new Error("Unauthorized");
}
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
+ user.notificationSettings = cleanNotificationSettings(user.notificationSettings, memberships);
return (
-
+
+
+
+
);
}
diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx
index e02be9caab..a19c77a084 100644
--- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx
+++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/responses/page.tsx
@@ -9,6 +9,9 @@ import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getAnalysisData } from "@/app/environments/[environmentId]/surveys/[surveyId]/summary/data";
export default async function ResponsesPage({ params }) {
+ const environmentId = params.environmentId;
+
+ console.log(environmentId);
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Unauthorized");
diff --git a/apps/web/lib/email.ts b/apps/web/lib/email.ts
index b7598c5079..a08164e7d9 100644
--- a/apps/web/lib/email.ts
+++ b/apps/web/lib/email.ts
@@ -16,7 +16,7 @@ interface sendEmailData {
html: string;
}
-const sendEmail = async (emailData: sendEmailData) => {
+export const sendEmail = async (emailData: sendEmailData) => {
let transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index 9d0c39072e..9b2efee6a7 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -16,3 +16,4 @@ export const WEBAPP_URL =
// Other
export const INTERNAL_SECRET = process.env.INTERNAL_SECRET;
+export const CRON_SECRET = process.env.CRON_SECRET;
diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts
index ef83b7e3ab..c40c30a5a9 100644
--- a/packages/types/surveys.ts
+++ b/packages/types/surveys.ts
@@ -33,3 +33,15 @@ export interface AttributeFilter {
condition: string;
value: string;
}
+
+export interface SurveyNotificationData {
+ id: string;
+ numDisplays: number;
+ numDisplaysResponded: number;
+ responseLenght: number;
+ responseCompletedLength: number;
+ latestResponse: any;
+ questions: Question[];
+ status: "draft" | "inProgress" | "archived" | "paused" | "completed";
+ name: String;
+}
diff --git a/packages/types/users.ts b/packages/types/users.ts
index 54a0215ead..18463fe328 100644
--- a/packages/types/users.ts
+++ b/packages/types/users.ts
@@ -1,6 +1,8 @@
export interface NotificationSettings {
- [surveyId: string]: {
- responseFinished: boolean;
- weeklySummary: boolean;
+ alert: {
+ [surveyId: string]: boolean;
+ };
+ weeklySummary: {
+ [productId: string]: boolean;
};
}
diff --git a/packages/types/v1/users.ts b/packages/types/v1/users.ts
index 6dfa894ab5..38c9bc57c7 100644
--- a/packages/types/v1/users.ts
+++ b/packages/types/v1/users.ts
@@ -1,10 +1,8 @@
import { z } from "zod";
-export const ZUserNotificationSettings = z.record(
- z.object({
- responseFinished: z.boolean(),
- weeklySummary: z.boolean(),
- })
-);
+export const ZUserNotificationSettings = z.object({
+ alert: z.record(z.boolean()),
+ weeklySummary: z.record(z.boolean()),
+});
export type TUserNotificationSettings = z.infer;
diff --git a/turbo.json b/turbo.json
index 1f006505fc..be6605b7a0 100644
--- a/turbo.json
+++ b/turbo.json
@@ -5,6 +5,7 @@
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": [
+ "CRON_SECRET",
"GITHUB_ID",
"GITHUB_SECRET",
"GOOGLE_CLIENT_ID",