chore: Adds verified email to response data for email verification surveys (#1943)

This commit is contained in:
Dhruwang Jariwala
2024-01-23 14:56:14 +05:30
committed by GitHub
parent 8ebbad4314
commit 1fd339e5a0
9 changed files with 63 additions and 25 deletions

View File

@@ -173,7 +173,7 @@ const validateHiddenField = (
}
// no key words -- userId & suid & existing question ids
if (
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden"].includes(field) ||
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(field) ||
existingQuestions.findIndex((q) => q.id === field) !== -1
) {
return "Question not allowed";

View File

@@ -41,7 +41,11 @@ export default function UpdateQuestionId({
updateQuestion(questionIdx, { id: prevValue });
toast.error("ID should not be empty.");
return;
} else if (["userId", "source", "suid", "end", "start", "welcomeCard", "hidden"].includes(currentValue)) {
} else if (
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(
currentValue
)
) {
setCurrentValue(prevValue);
updateQuestion(questionIdx, { id: prevValue });
toast.error("Reserved words cannot be used as question ID");

View File

@@ -29,6 +29,7 @@ interface LinkSurveyProps {
singleUseResponse?: TResponse;
webAppUrl: string;
responseCount?: number;
verifiedEmail?: string;
}
export default function LinkSurvey({
@@ -41,6 +42,7 @@ export default function LinkSurvey({
singleUseResponse,
webAppUrl,
responseCount,
verifiedEmail,
}: LinkSurveyProps) {
const responseId = singleUseResponse?.id;
const searchParams = useSearchParams();
@@ -107,6 +109,14 @@ export default function LinkSurvey({
return fieldsSet ? fieldsRecord : null;
}, [searchParams, survey.hiddenFields?.fieldIds]);
const getVerifiedEmail = useMemo<Record<string, string> | null>(() => {
if (survey.verifyEmail && verifiedEmail) {
return { verifiedEmail: verifiedEmail };
} else {
return null;
}
}, [survey.verifyEmail, verifiedEmail]);
useEffect(() => {
responseQueue.updateSurveyState(surveyState);
}, [responseQueue, surveyState]);
@@ -174,6 +184,7 @@ export default function LinkSurvey({
data: {
...responseUpdate.data,
...hiddenFieldsRecord,
...getVerifiedEmail,
},
ttc: responseUpdate.ttc,
finished: responseUpdate.finished,

View File

@@ -26,6 +26,7 @@ interface LinkSurveyPinScreenProps {
IMPRINT_URL?: string;
PRIVACY_URL?: string;
IS_FORMBRICKS_CLOUD: boolean;
verifiedEmail?: string;
}
const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
@@ -41,6 +42,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
IMPRINT_URL,
PRIVACY_URL,
IS_FORMBRICKS_CLOUD,
verifiedEmail,
} = props;
const [localPinEntry, setLocalPinEntry] = useState<string>("");
@@ -120,6 +122,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
singleUseId={singleUseId}
singleUseResponse={singleUseResponse}
webAppUrl={webAppUrl}
verifiedEmail={verifiedEmail}
/>
</MediaBackground>
<LegalFooter

View File

@@ -71,7 +71,7 @@ export default function VerifyEmail({
if (isErrorComponent) {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
<div className="flex h-[100vh] w-[100vw] flex-col items-center justify-center bg-slate-50">
<span className="h-24 w-24 rounded-full bg-slate-300 p-6 text-5xl">🤔</span>
<p className="mt-8 text-4xl font-bold">This looks fishy.</p>
<p className="mt-4 cursor-pointer text-sm text-slate-400" onClick={handleGoBackClick}>

View File

@@ -1,21 +1,26 @@
import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
export const getEmailVerificationStatus = async (
interface emailVerificationDetails {
status: "not-verified" | "verified" | "fishy";
email?: string;
}
export const getEmailVerificationDetails = async (
surveyId: string,
token: string
): Promise<"verified" | "not-verified" | "fishy"> => {
): Promise<emailVerificationDetails> => {
if (!token) {
return "not-verified";
return { status: "not-verified" };
} else {
try {
const validateToken = await verifyTokenForLinkSurvey(token, surveyId);
if (validateToken) {
return "verified";
const verifiedEmail = await verifyTokenForLinkSurvey(token, surveyId);
if (verifiedEmail) {
return { status: "verified", email: verifiedEmail };
} else {
return "fishy";
return { status: "fishy" };
}
} catch (error) {
return "not-verified";
return { status: "not-verified" };
}
}
};

View File

@@ -18,7 +18,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { ZId } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { getEmailVerificationStatus } from "./lib/helpers";
import { getEmailVerificationDetails } from "./lib/helpers";
interface LinkSurveyPageProps {
params: {
@@ -142,7 +142,9 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
}
// verify email: Check if the survey requires email verification
let emailVerificationStatus: string | undefined = undefined;
let emailVerificationStatus: string = "";
let verifiedEmail: string | undefined = undefined;
if (survey.verifyEmail) {
const token =
searchParams && Object.keys(searchParams).length !== 0 && searchParams.hasOwnProperty("verify")
@@ -150,7 +152,9 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
: undefined;
if (token) {
emailVerificationStatus = await getEmailVerificationStatus(survey.id, token);
const emailVerificationDetails = await getEmailVerificationDetails(survey.id, token);
emailVerificationStatus = emailVerificationDetails.status;
verifiedEmail = emailVerificationDetails.email;
}
}
@@ -185,6 +189,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
IMPRINT_URL={IMPRINT_URL}
PRIVACY_URL={PRIVACY_URL}
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
verifiedEmail={verifiedEmail}
/>
);
}
@@ -202,6 +207,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
responseCount={survey.welcomeCard.showResponseCount ? responseCount : undefined}
verifiedEmail={verifiedEmail}
/>
</MediaBackground>
<LegalFooter

View File

@@ -15,16 +15,13 @@ export const createInviteToken = (inviteId: string, email: string, options = {})
return jwt.sign({ inviteId, email }, env.NEXTAUTH_SECRET, options);
};
export function verifyTokenForLinkSurvey(token: string, surveyId: string): Promise<boolean> {
return new Promise((resolve) => {
jwt.verify(token, env.NEXTAUTH_SECRET + surveyId, function (err) {
if (err) {
resolve(false);
} else {
resolve(true);
}
});
});
export function verifyTokenForLinkSurvey(token: string, surveyId: string) {
try {
const payload = jwt.verify(token, process.env.NEXTAUTH_SECRET + surveyId);
return (payload as jwt.JwtPayload).email || null;
} catch (err) {
return null;
}
}
export async function verifyToken(token: string, userEmail: string = ""): Promise<JwtPayload> {

View File

@@ -1,6 +1,6 @@
"use client";
import { TrashIcon } from "@heroicons/react/24/outline";
import { EnvelopeIcon, TrashIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
@@ -315,6 +315,18 @@ export default function SingleResponseCard({
/>
)}
<div className="space-y-6">
{survey.verifyEmail && response.data["verifiedEmail"] && (
<div>
<p className="flex items-center space-x-2 text-sm text-slate-500">
<EnvelopeIcon className="h-4 w-4" />
<span>Verified Email</span>
</p>
<p className="ph-no-capture my-1 font-semibold text-slate-700">
{response.data["verifiedEmail"]}
</p>
</div>
)}
{survey.questions.map((question) => {
const skipped = skippedQuestions.find((skippedQuestionElement) =>
skippedQuestionElement.includes(question.id)