mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-25 20:01:53 -05:00
feat: enable weekly summary & support for callbacks on login (#1885)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com> Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5471324cfe
commit
70fe0fb7a7
@@ -15,6 +15,7 @@ import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/surve
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { deleteSurvey, duplicateSurvey, getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const createShortUrlAction = async (url: string) => {
|
||||
@@ -44,10 +45,25 @@ export async function createTeamAction(teamName: string): Promise<Team> {
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await createProduct(newTeam.id, {
|
||||
const product = await createProduct(newTeam.id, {
|
||||
name: "My Product",
|
||||
});
|
||||
|
||||
const updatedNotificationSettings = {
|
||||
...session.user.notificationSettings,
|
||||
alert: {
|
||||
...session.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...session.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
@@ -244,6 +260,20 @@ export const createProductAction = async (environmentId: string, productName: st
|
||||
const product = await createProduct(team.id, {
|
||||
name: productName,
|
||||
});
|
||||
const updatedNotificationSettings = {
|
||||
...session.user.notificationSettings,
|
||||
alert: {
|
||||
...session.user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...session.user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
|
||||
// get production environment
|
||||
const productionEnvironment = product.environments.find((environment) => environment.type === "production");
|
||||
|
||||
@@ -504,7 +504,7 @@ export default function Navigation({
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
await signOut({ callbackUrl: "/auth/login" });
|
||||
await formbricksLogout();
|
||||
}}>
|
||||
<div className="flex h-full w-full items-center">
|
||||
|
||||
@@ -98,8 +98,11 @@ export const leaveTeamAction = async (teamId: string) => {
|
||||
};
|
||||
|
||||
export const createInviteTokenAction = async (inviteId: string) => {
|
||||
const { email } = await getInvite(inviteId);
|
||||
const inviteToken = createInviteToken(inviteId, email, {
|
||||
const invite = await getInvite(inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
const inviteToken = createInviteToken(inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ export default async function ProfileSettingsPage({ params }) {
|
||||
</SettingsCard>
|
||||
<IntegrationsTip environmentId={params.environmentId} />
|
||||
<SettingsCard
|
||||
beta
|
||||
title="Weekly summary (Products)"
|
||||
description="Stay up-to-date with a Weekly every Monday.">
|
||||
<EditWeeklySummary memberships={memberships} user={user} environmentId={params.environmentId} />
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ function DeleteAccountModal({ setOpen, open, session }: DeleteAccountModalProps)
|
||||
try {
|
||||
setDeleting(true);
|
||||
await deleteUserAction();
|
||||
await signOut();
|
||||
await signOut({ callbackUrl: "/auth/login" });
|
||||
await formbricksLogout();
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong");
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Onboarding({ session, environmentId, user, product }: On
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const updatedProfile = { ...user, onboardingCompleted: true };
|
||||
const updatedProfile = { onboardingCompleted: true };
|
||||
await updateUserAction(updatedProfile);
|
||||
|
||||
if (environmentId) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
const ContentLayout = ({ headline, description, children }) => {
|
||||
interface ContentLayoutProps {
|
||||
headline: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ContentLayout = ({ headline, description, children }: ContentLayoutProps) => {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="m-auto flex flex-col gap-7 text-center text-slate-700">
|
||||
@@ -57,9 +63,17 @@ export const ExpiredContent = () => {
|
||||
return (
|
||||
<ContentLayout
|
||||
headline="Invite expired 😥"
|
||||
description="Invites are valid for 7 days. Please request a new invite.">
|
||||
<div></div>
|
||||
</ContentLayout>
|
||||
description="Invites are valid for 7 days. Please request a new invite."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InvitationNotFound = () => {
|
||||
return (
|
||||
<ContentLayout
|
||||
headline="Invite not found 😥"
|
||||
description="The invitation code cannot be found or has already been used."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { sendInviteAcceptedEmail } from "@formbricks/lib/emails/emails";
|
||||
import { env } from "@formbricks/lib/env.mjs";
|
||||
import { deleteInvite, getInvite } from "@formbricks/lib/invite/service";
|
||||
import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
|
||||
import {
|
||||
ExpiredContent,
|
||||
InvitationNotFound,
|
||||
NotLoggedInContent,
|
||||
RightAccountContent,
|
||||
UsedContent,
|
||||
WrongAccountContent,
|
||||
} from "./components/InviteContentComponents";
|
||||
|
||||
export default async function JoinTeam({ searchParams }) {
|
||||
const currentUser = await getServerSession(authOptions);
|
||||
export default async function InvitePage({ searchParams }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
try {
|
||||
const { inviteId, email } = verifyInviteToken(searchParams.token);
|
||||
|
||||
const invite = await getInvite(inviteId);
|
||||
|
||||
if (!invite) {
|
||||
return <InvitationNotFound />;
|
||||
}
|
||||
|
||||
const isInviteExpired = new Date(invite.expiresAt) < new Date();
|
||||
|
||||
if (!invite || isInviteExpired) {
|
||||
if (isInviteExpired) {
|
||||
return <ExpiredContent />;
|
||||
} else if (invite.accepted) {
|
||||
return <UsedContent />;
|
||||
} else if (!currentUser) {
|
||||
const redirectUrl = env.NEXTAUTH_URL + "/invite?token=" + searchParams.token;
|
||||
} else if (!session) {
|
||||
const redirectUrl = WEBAPP_URL + "/invite?token=" + searchParams.token;
|
||||
return <NotLoggedInContent email={email} token={searchParams.token} redirectUrl={redirectUrl} />;
|
||||
} else if (currentUser.user?.email !== email) {
|
||||
} else if (session.user?.email !== email) {
|
||||
return <WrongAccountContent />;
|
||||
} else {
|
||||
await createMembership(invite.teamId, currentUser.user.id, { accepted: true, role: invite.role });
|
||||
await createMembership(invite.teamId, session.user.id, { accepted: true, role: invite.role });
|
||||
await deleteInvite(inviteId);
|
||||
|
||||
sendInviteAcceptedEmail(invite.creator.name ?? "", currentUser.user?.name ?? "", invite.creator.email);
|
||||
sendInviteAcceptedEmail(invite.creator.name ?? "", session.user?.name ?? "", invite.creator.email);
|
||||
|
||||
return <RightAccountContent />;
|
||||
}
|
||||
} catch (e) {
|
||||
return <ExpiredContent />;
|
||||
console.error(e);
|
||||
return <InvitationNotFound />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,30 +159,27 @@ const createSurveyFields = (surveyResponses: SurveyResponse[]) => {
|
||||
return surveyFields;
|
||||
};
|
||||
|
||||
const notificationFooter = () => {
|
||||
const notificationFooter = (environmentId: string) => {
|
||||
return `
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team 🤍</p>
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;"><p><i>This is a Beta feature. If you experience any issues, please let us know by replying to this email 🙏</i></p></div>
|
||||
`;
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team 🤍</p>
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;">
|
||||
<p><i>To halt Weekly Updates, <a href="${WEBAPP_URL}/environments/${environmentId}/settings/notifications">please turn them off</a> in your settings 🙏</i></p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const createReminderNotificationBody = (notificationData: NotificationResponse, webUrl) => {
|
||||
const createReminderNotificationBody = (notificationData: NotificationResponse) => {
|
||||
return `
|
||||
<p>We’d love to send you a Weekly Summary, but currently there are no surveys running for ${notificationData.productName}.</p>
|
||||
|
||||
<p style="font-weight: bold; padding-top:1em;">Don’t let a week pass without learning about your users:</p>
|
||||
|
||||
<a class="button" href="${webUrl}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA">Setup a new survey</a>
|
||||
|
||||
<a class="button" href="${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA">Setup a new survey</a>
|
||||
|
||||
<br/>
|
||||
<p style="padding-top:1em;">Need help finding the right survey for your product? Pick a 15-minute slot <a href="https://cal.com/johannes/15">in our CEOs calendar</a> or reply to this email :)</p>
|
||||
|
||||
|
||||
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
|
||||
<p style="margin-top:0px;">The Formbricks Team</p>
|
||||
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:99px; margin:1em; padding:0.01em 1.6em; text-align:center;"><p><i>This is a Beta feature. If you experience any issues, please let us know by replying to this email 🙏</i></p></div>
|
||||
${notificationFooter(notificationData.environmentId)}
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -207,7 +204,7 @@ export const sendWeeklySummaryNotificationEmail = async (
|
||||
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
|
||||
${notificationInsight(notificationData.insights)}
|
||||
${notificationLiveSurveys(notificationData.surveys, notificationData.environmentId)}
|
||||
${notificationFooter()}
|
||||
${notificationFooter(notificationData.environmentId)}
|
||||
`),
|
||||
});
|
||||
};
|
||||
@@ -231,7 +228,7 @@ export const sendNoLiveSurveyNotificationEmail = async (
|
||||
subject: getEmailSubject(notificationData.productName),
|
||||
html: withEmailTemplate(`
|
||||
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
|
||||
${createReminderNotificationBody(notificationData, WEBAPP_URL)}
|
||||
${createReminderNotificationBody(notificationData)}
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { verifyInviteToken } from "@formbricks/lib/jwt";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createTeam, getTeam } from "@formbricks/lib/team/service";
|
||||
import { createUser } from "@formbricks/lib/user/service";
|
||||
import { createUser, updateUser } from "@formbricks/lib/user/service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let { inviteToken, ...user } = await request.json();
|
||||
@@ -76,7 +76,22 @@ export async function POST(request: Request) {
|
||||
else {
|
||||
const team = await createTeam({ name: user.name + "'s Team" });
|
||||
await createMembership(team.id, user.id, { role: "owner", accepted: true });
|
||||
await createProduct(team.id, { name: "My Product" });
|
||||
const product = await createProduct(team.id, { name: "My Product" });
|
||||
|
||||
const updatedNotificationSettings = {
|
||||
...user.notificationSettings,
|
||||
alert: {
|
||||
...user.notificationSettings?.alert,
|
||||
},
|
||||
weeklySummary: {
|
||||
...user.notificationSettings?.weeklySummary,
|
||||
[product.id]: true,
|
||||
},
|
||||
};
|
||||
|
||||
await updateUser(user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
}
|
||||
// send verification email amd return user
|
||||
if (!EMAIL_VERIFICATION_DISABLED) {
|
||||
|
||||
@@ -13,3 +13,6 @@ export const shareUrlRoute = (url: string): boolean => {
|
||||
const regex = /\/share\/[A-Za-z0-9]+\/(summary|responses)/;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
export const isWebAppRoute = (url: string): boolean =>
|
||||
url.startsWith("/environments") && url !== "/api/auth/signout";
|
||||
|
||||
@@ -6,14 +6,31 @@ import {
|
||||
} from "@/app/middleware/bucket";
|
||||
import {
|
||||
clientSideApiRoute,
|
||||
isWebAppRoute,
|
||||
loginRoute,
|
||||
shareUrlRoute,
|
||||
signupRoute,
|
||||
} from "@/app/middleware/endpointValidator";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const token = await getToken({ req: request });
|
||||
|
||||
if (isWebAppRoute(request.nextUrl.pathname) && !token) {
|
||||
return NextResponse.redirect(
|
||||
WEBAPP_URL + "/auth/login?callbackUrl=" + WEBAPP_URL + request.nextUrl.pathname
|
||||
);
|
||||
}
|
||||
|
||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||
if (token && callbackUrl) {
|
||||
return NextResponse.redirect(WEBAPP_URL + callbackUrl);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
@@ -54,5 +71,7 @@ export const config = {
|
||||
"/api/v1/js/actions",
|
||||
"/api/v1/client/storage",
|
||||
"/share/(.*)/:path",
|
||||
"/environments/:path*",
|
||||
"/api/auth/signout",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ test.describe("JS Package Test", async () => {
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(page.getByRole("link", { name: "Powered by Formbricks" })).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
test("Admin checks Display", async ({ page }) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const skipOnboarding = async (page: Page): Promise<void> => {
|
||||
await page.waitForURL("/onboarding");
|
||||
await expect(page).toHaveURL("/onboarding");
|
||||
await page.getByRole("button", { name: "I'll do it later" }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByRole("button", { name: "I'll do it later" }).click();
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
await expect(page).toHaveURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
Reference in New Issue
Block a user