mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 02:10:33 -05:00
chore: Adds verified email to response data for email verification surveys (#1943)
This commit is contained in:
committed by
GitHub
parent
8ebbad4314
commit
1fd339e5a0
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user