From f6a2ab2fe42f950387766340a836b4291b40567c Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Tue, 17 Feb 2026 16:43:59 -0500 Subject: [PATCH 1/8] Phone verified added to firestore data model, and cloud function added to set it --- components/db/profile/types.ts | 1 + firestore.rules | 6 ++++- functions/src/index.ts | 2 +- .../src/profile/completePhoneVerification.ts | 26 +++++++++++++++++++ functions/src/profile/index.ts | 1 + functions/src/profile/types.ts | 3 ++- 6 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 functions/src/profile/completePhoneVerification.ts diff --git a/components/db/profile/types.ts b/components/db/profile/types.ts index b99b80d5d..898b84f2e 100644 --- a/components/db/profile/types.ts +++ b/components/db/profile/types.ts @@ -43,4 +43,5 @@ export type Profile = { contactInfo?: ContactInfo location?: string orgCategories?: OrgCategory[] | "" + phoneVerified?: boolean } diff --git a/firestore.rules b/firestore.rules index e33d279e2..56d395b58 100644 --- a/firestore.rules +++ b/firestore.rules @@ -34,6 +34,10 @@ service cloud.firestore { // email digest notification times return !request.resource.data.diff(resource.data).affectedKeys().hasAny(['nextDigestAt']) } + function doesNotChangePhoneVerified() { + // Only the completePhoneVerification cloud function (Admin SDK) sets phoneVerified + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(['phoneVerified']) + } // either the change doesn't include the public field, // or the user is a base user (i.e. not an org) function validPublicChange() { @@ -52,7 +56,7 @@ service cloud.firestore { // Allow users to make updates except to delete their profile or set the role field. // Only admins can delete a user profile or set the user role field. - allow update: if validUser() && doesNotChangeRole() && validPublicChange() && doesNotChangeNextDigestAt() + allow update: if validUser() && doesNotChangeRole() && validPublicChange() && doesNotChangeNextDigestAt() && doesNotChangePhoneVerified() } // Allow querying publications individually or with a collection group. match /{path=**}/publishedTestimony/{id} { diff --git a/functions/src/index.ts b/functions/src/index.ts index d3effd4be..b396108d9 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -31,7 +31,7 @@ export { fetchMemberBatch, startMemberBatches } from "./members" -export { finishSignup } from "./profile" +export { completePhoneVerification, finishSignup } from "./profile" export { checkSearchIndexVersion, searchHealthCheck } from "./search" export { deleteTestimony, diff --git a/functions/src/profile/completePhoneVerification.ts b/functions/src/profile/completePhoneVerification.ts new file mode 100644 index 000000000..e174f39d6 --- /dev/null +++ b/functions/src/profile/completePhoneVerification.ts @@ -0,0 +1,26 @@ +import * as functions from "firebase-functions" +import { db, auth } from "../firebase" +import { checkAuth, fail } from "../common" + +export const completePhoneVerification = functions.https.onCall(async (_, context) => { + const uid = checkAuth(context) + + const user = await auth.getUser(uid) + const hasPhone = user.providerData?.some( + (p) => p.providerId === "phone" + ) + + if (!hasPhone) { + throw fail( + "failed-precondition", + "Phone number is not linked to this account. Complete phone verification first." + ) + } + + await db.doc(`/profiles/${uid}`).set( + { phoneVerified: true }, + { merge: true } + ) + + return { phoneVerified: true } +}) diff --git a/functions/src/profile/index.ts b/functions/src/profile/index.ts index a897f8e16..1b6fde6df 100644 --- a/functions/src/profile/index.ts +++ b/functions/src/profile/index.ts @@ -1 +1,2 @@ +export * from "./completePhoneVerification" export * from "./finishSignup" diff --git a/functions/src/profile/types.ts b/functions/src/profile/types.ts index 733a558df..cd2e2e265 100644 --- a/functions/src/profile/types.ts +++ b/functions/src/profile/types.ts @@ -33,7 +33,8 @@ export const Profile = Record({ profileImage: Optional(String), billsFollowing: Optional(Array(String)), contactInfo: Optional(Dictionary(String)), - location: Optional(String) + location: Optional(String), + phoneVerified: Optional(Boolean) }) export type Profile = Static From 09552445ff668cec50f70222c3b6dbf5b5646595 Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Tue, 17 Feb 2026 17:48:07 -0500 Subject: [PATCH 2/8] Added front end hooks for cloud completePhoneVerification function --- components/auth/hooks.ts | 17 ++++++++++++++--- components/auth/types.tsx | 5 +++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/components/auth/hooks.ts b/components/auth/hooks.ts index bfdb49415..7576a037b 100644 --- a/components/auth/hooks.ts +++ b/components/auth/hooks.ts @@ -12,14 +12,16 @@ import { import { useAsyncCallback } from "react-async-hook" import { setProfile } from "../db" import { auth } from "../firebase" -import { finishSignup, OrgCategory } from "./types" +import { completePhoneVerification, finishSignup, OrgCategory } from "./types" const errorMessages: Record = { "auth/email-already-exists": "You already have an account.", "auth/email-already-in-use": "You already have an account.", "auth/wrong-password": "Your password is wrong.", "auth/invalid-email": "The email you provided is not a valid email.", - "auth/user-not-found": "You don't have an account." + "auth/user-not-found": "You don't have an account.", + "functions/failed-precondition": + "Phone number is not linked to this account. Complete phone verification first." } function getErrorMessage(errorCode?: string) { @@ -39,7 +41,9 @@ function useFirebaseFunction( console.log(err) const message = getErrorMessage( - err instanceof FirebaseError ? err.code : undefined + err instanceof FirebaseError + ? err.code + : (err as { code?: string })?.code ) throw new Error(message) } @@ -104,6 +108,13 @@ export function useSendEmailVerification() { return useFirebaseFunction((user: User) => sendEmailVerification(user)) } +/** Call after the user has linked a phone number via linkWithPhoneNumber + confirm. */ +export function useCompletePhoneVerification() { + return useFirebaseFunction( + async () => (await completePhoneVerification()).data + ) +} + export type SendPasswordResetEmailData = { email: string } export function useSendPasswordResetEmail() { diff --git a/components/auth/types.tsx b/components/auth/types.tsx index c3170e281..ec1b49309 100644 --- a/components/auth/types.tsx +++ b/components/auth/types.tsx @@ -9,3 +9,8 @@ export const finishSignup = httpsCallable< { requestedRole: Role } | Partial, void >(functions, "finishSignup") + +export const completePhoneVerification = httpsCallable< + void, + { phoneVerified: true } +>(functions, "completePhoneVerification") From 6bfe71ebae3e2813af1b4ee5759921fa0386719d Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Fri, 20 Feb 2026 16:17:33 -0500 Subject: [PATCH 3/8] added get verified button on edit profile page --- .../EditProfilePage/EditProfileHeader.tsx | 24 +++++++++++++++++- .../EditProfilePage/EditProfilePage.tsx | 4 +++ public/images/verifiedUser.png | Bin 0 -> 1151 bytes public/locales/en/editProfile.json | 3 +++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 public/images/verifiedUser.png diff --git a/components/EditProfilePage/EditProfileHeader.tsx b/components/EditProfilePage/EditProfileHeader.tsx index b5c02f5b7..8bc239587 100644 --- a/components/EditProfilePage/EditProfileHeader.tsx +++ b/components/EditProfilePage/EditProfileHeader.tsx @@ -7,13 +7,17 @@ import { ProfileEditToggle } from "components/ProfilePage/ProfileButtons" export const EditProfileHeader = ({ formUpdated, onSettingsModalOpen, + onGetVerifiedClick, uid, - role + role, + phoneVerified }: { formUpdated: boolean onSettingsModalOpen: () => void + onGetVerifiedClick?: () => void uid: string role: Role + phoneVerified?: boolean }) => { const { t } = useTranslation("editProfile") @@ -30,6 +34,24 @@ export const EditProfileHeader = ({ onClick={() => onSettingsModalOpen()} /> + {phoneVerified === true ? ( +
+ {t("verifiedUser")} + {t("verifiedUserBadgeAlt")} +
+ ) : onGetVerifiedClick ? ( + + ) : null} ) diff --git a/components/EditProfilePage/EditProfilePage.tsx b/components/EditProfilePage/EditProfilePage.tsx index ad7eb6e0f..1cad6647f 100644 --- a/components/EditProfilePage/EditProfilePage.tsx +++ b/components/EditProfilePage/EditProfilePage.tsx @@ -87,6 +87,8 @@ export function EditProfileForm({ const [formUpdated, setFormUpdated] = useState(false) const [settingsModal, setSettingsModal] = useState<"show" | null>(null) + const [showPhoneVerificationModal, setShowPhoneVerificationModal] = + useState(false) const [notifications, setNotifications] = useState( notificationFrequency || "Weekly" ) @@ -178,8 +180,10 @@ export function EditProfileForm({ setShowPhoneVerificationModal(true)} uid={uid} role={profile.role} + phoneVerified={profile.phoneVerified} /> ()Cc+1RyfafuwZ}1FGE?fN9pZd&ZlfND<(Bg}_Ub`4k|1 z0!Sa*02s@P_FEuW);fkg@S(HjZ^C-ZoKFC%vTnXW0A%g|1ynoptO+L}qkGG-H zRG5u<05E7S08xM)g+F}}I*fBtGv_h@vXp90*;fk&&}w-jz*7$Z9#~5Z=Kv;`06>IS z1p~01lnLPB3Y$W>6pMHuXAyV8_JRSlQ6SJZKLUdu0Q8!kej7OYBI{a#FS1N-DIu@4 zQi9|u<|bEAF^5Z4P7_ca+hVe#56gx|a(zQ{4V((Ly<^)vPq&nD_{=c?S2p#shnxEM z9;@5|R7VyLKvQ}f;^DE%f)2Kl`coarbFej}A8Z-&ylRkidGoK}Rj^~g$&Srac_9i= ze=LZb4vMXw0R$VR4ZD8sn@wJ<#tbPc*m#C~fVckGiGlhKED0xO%fh6@6M#@8-4f!{ zSiz@p_b)wIf9`K=INyp5zqR6x*uK0_%3$&2Kq1v92PLo|yw+D(A;E1*2Kjw6tUA#jkMY)+d@s+n4ywkqhYOL@ecquDB)> z0GE7ST$B9NRe*{}`edjvjsFJ_jLNuGS0F0mrXRaB^X~-+CvhW}IN_T4?LVbHs%RX4 zZU8RWi$G2L$F2Z`_=z7vjpJws;ESX8FW3PnVG!ZuA$I^G=}}t%yfn4p z;K*8kt?kKX@5IUfN&I%hU`Y&9e6eyI#iK zF80?6Z_NON`ndQ3ST0e{%PmHLKve$Ac-_wle;NUNb#eD|zO87&qOH6v%#XksIb^(C z78V13PM}!;F#$F0-u)}N&?t{c#X(*k4o0QR`L37NiI++_F~z2o_wHbM#yf;zctQhw R15f|}002ovPDHLkV1hDC1aANU literal 0 HcmV?d00001 diff --git a/public/locales/en/editProfile.json b/public/locales/en/editProfile.json index 9f8e4402b..49d0b8861 100644 --- a/public/locales/en/editProfile.json +++ b/public/locales/en/editProfile.json @@ -1,5 +1,8 @@ { "header": "Edit Profile", + "getVerified": "Get Verified", + "verifiedUser": "Verified User", + "verifiedUserBadgeAlt": "Verified user badge", "setting": "Settings", "privacySetting": "Privacy Settings", "save": "Save", From fef430aa83bccc78782f26e061f687b03147b69f Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Fri, 20 Feb 2026 17:03:46 -0500 Subject: [PATCH 4/8] added empty Verify your phone number modal --- .../EditProfilePage/EditProfilePage.tsx | 5 ++++ .../PhoneVerificationModal.tsx | 28 +++++++++++++++++++ public/locales/en/editProfile.json | 1 + 3 files changed, 34 insertions(+) create mode 100644 components/EditProfilePage/PhoneVerificationModal.tsx diff --git a/components/EditProfilePage/EditProfilePage.tsx b/components/EditProfilePage/EditProfilePage.tsx index 1cad6647f..2fff9d96a 100644 --- a/components/EditProfilePage/EditProfilePage.tsx +++ b/components/EditProfilePage/EditProfilePage.tsx @@ -17,6 +17,7 @@ import { import { EditProfileHeader } from "./EditProfileHeader" import { FollowingTab } from "./FollowingTab" import { PersonalInfoTab } from "./PersonalInfoTab" +import PhoneVerificationModal from "./PhoneVerificationModal" import ProfileSettingsModal from "./ProfileSettingsModal" import { StyledTabContent, @@ -215,6 +216,10 @@ export function EditProfileForm({ onSettingsModalClose={() => setSettingsModal(null)} show={settingsModal === "show"} /> + setShowPhoneVerificationModal(false)} + /> ) } diff --git a/components/EditProfilePage/PhoneVerificationModal.tsx b/components/EditProfilePage/PhoneVerificationModal.tsx new file mode 100644 index 000000000..a8ed08b48 --- /dev/null +++ b/components/EditProfilePage/PhoneVerificationModal.tsx @@ -0,0 +1,28 @@ +import type { ModalProps } from "react-bootstrap" +import { Modal } from "../bootstrap" +import { useTranslation } from "next-i18next" + +export default function PhoneVerificationModal({ + show, + onHide +}: Pick) { + const { t } = useTranslation("editProfile") + + return ( + + + + {t("phoneVerificationModalTitle")} + + + + {/* Step 1 (phone) and step 2 (code) content will be added in step 3 */} + + + ) +} diff --git a/public/locales/en/editProfile.json b/public/locales/en/editProfile.json index 49d0b8861..84b6209a7 100644 --- a/public/locales/en/editProfile.json +++ b/public/locales/en/editProfile.json @@ -1,6 +1,7 @@ { "header": "Edit Profile", "getVerified": "Get Verified", + "phoneVerificationModalTitle": "Verify your phone number", "verifiedUser": "Verified User", "verifiedUserBadgeAlt": "Verified user badge", "setting": "Settings", From 0e0551a702d760f452a4d589d7b3c51d39f0bdc1 Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Sat, 7 Mar 2026 17:54:05 -0500 Subject: [PATCH 5/8] Have working flow for phone verification from edit profile page --- .../PhoneVerificationModal.tsx | 205 +++++++++++++++++- components/auth/hooks.ts | 12 +- public/locales/en/editProfile.json | 16 ++ 3 files changed, 229 insertions(+), 4 deletions(-) diff --git a/components/EditProfilePage/PhoneVerificationModal.tsx b/components/EditProfilePage/PhoneVerificationModal.tsx index a8ed08b48..37ca568f4 100644 --- a/components/EditProfilePage/PhoneVerificationModal.tsx +++ b/components/EditProfilePage/PhoneVerificationModal.tsx @@ -1,12 +1,154 @@ +import { + type ConfirmationResult, + linkWithPhoneNumber, + RecaptchaVerifier +} from "firebase/auth" +import { useEffect, useRef, useState } from "react" import type { ModalProps } from "react-bootstrap" -import { Modal } from "../bootstrap" +import { Alert, Col, Form, Modal } from "../bootstrap" +import { LoadingButton } from "../buttons" +import Input from "../forms/Input" +import { useAuth } from "../auth" +import { getErrorMessage } from "../auth/hooks" +import { useCompletePhoneVerification } from "../auth/hooks" +import { auth } from "../firebase" import { useTranslation } from "next-i18next" +const US_REGEX = /^(\([2-9][0-9]{2}\)|[2-9][0-9]{2})[- ]?([0-9]{3})[- ]?([0-9]{4})$/; + +const AUTH_ERROR_CODE_TO_KEY: Record = { + "auth/credential-already-in-use": "phoneVerification.errors.credentialAlreadyInUse", + "auth/provider-already-linked": "phoneVerification.errors.providerAlreadyLinked", + "auth/invalid-phone-number": "phoneVerification.errors.invalidPhoneNumber", + "auth/operation-not-allowed": "phoneVerification.errors.operationNotAllowed" +} + export default function PhoneVerificationModal({ show, onHide }: Pick) { const { t } = useTranslation("editProfile") + const { user } = useAuth() + const completePhoneVerification = useCompletePhoneVerification() + + const [step, setStep] = useState<"phone" | "code">("phone") + const [phone, setPhone] = useState("") + const [code, setCode] = useState("") + const [error, setError] = useState(null) + const [sendingCode, setSendingCode] = useState(false) + const [verifying, setVerifying] = useState(false) + const [confirmationResult, setConfirmationResult] = + useState(null) + const recaptchaVerifierRef = useRef(null) + const phoneInputRef = useRef(null) + const codeInputRef = useRef(null) + const RECAPTCHA_CONTAINER_ID = "phone-verification-recaptcha-container" + + const getModalErrorMessage = (code: string | undefined) => { + if (!code) return getErrorMessage(code) + const key = AUTH_ERROR_CODE_TO_KEY[code] + return key ? t(key) : getErrorMessage(code) + } + + useEffect(() => { + if (!show) { + setStep("phone") + setPhone("") + setCode("") + setError(null) + setConfirmationResult(null) + setSendingCode(false) + setVerifying(false) + if (recaptchaVerifierRef.current) { + try { + recaptchaVerifierRef.current.clear() + } catch { + // ignore if already cleared + } + recaptchaVerifierRef.current = null + } + completePhoneVerification.reset() + } + // could not add a reference to completePhoneVerification.reset to dep array without triggering an infinite effect, so: + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [show]) + + const handleSendCode = async () => { + setError(null) + const trimmed = phone.trim() + if (!US_REGEX.test(trimmed)) { + setError(getModalErrorMessage("auth/invalid-phone-number")) + return + } + const phoneDigits = trimmed.replace(/\D/g, ""); + const firebasePhoneFormat = `+1${phoneDigits}`; + + if (!user) { + setError(t("phoneVerification.signedInRequired")) + return + } + + setSendingCode(true) + try { + if (!recaptchaVerifierRef.current) { + recaptchaVerifierRef.current = new RecaptchaVerifier( + RECAPTCHA_CONTAINER_ID, + { size: "invisible" }, + auth + ) + } + const result = await linkWithPhoneNumber( + user, + firebasePhoneFormat, + recaptchaVerifierRef.current + ) + setConfirmationResult(result) + setStep("code") + } catch (err: unknown) { + const code = (err as { code?: string })?.code + setError( + getModalErrorMessage(code) || + (err as Error)?.message || + getErrorMessage() + ) + } finally { + setSendingCode(false) + } + } + + const handleVerify = async () => { + setError(null) + if (!confirmationResult || !code.trim()) { + setError(t("phoneVerification.enterVerificationCode")) + return + } + setVerifying(true) + try { + await confirmationResult.confirm(code.trim()) + if (completePhoneVerification.execute) { + await completePhoneVerification.execute() + } + onHide?.() + } catch (err: unknown) { + const code = (err as { code?: string })?.code + setError( + getModalErrorMessage(code) || + (err as Error)?.message || + getErrorMessage() + ) + } finally { + setVerifying(false) + } + } + + useEffect(() => { + if (!show) return + const el = step === "phone" ? phoneInputRef.current : codeInputRef.current + if (el) { + const id = requestAnimationFrame(() => el.focus()) + return () => cancelAnimationFrame(id) + } + }, [show, step]) return ( - {/* Step 1 (phone) and step 2 (code) content will be added in step 3 */} + + {error ? ( + setError(null)}> + {error} + + ) : null} + + {step === "phone" ? ( +
{ + e.preventDefault() + handleSendCode() + }} + > + setPhone(e.target.value)} + className="mb-3" + /> +
+ + {t("phoneVerification.continue")} + + + ) : ( +
{ + e.preventDefault() + handleVerify() + }} + > + setCode(e.target.value)} + className="mb-3" + /> + + {t("phoneVerification.verify")} + +
+ )} + ) diff --git a/components/auth/hooks.ts b/components/auth/hooks.ts index 7576a037b..110002900 100644 --- a/components/auth/hooks.ts +++ b/components/auth/hooks.ts @@ -21,10 +21,18 @@ const errorMessages: Record = { "auth/invalid-email": "The email you provided is not a valid email.", "auth/user-not-found": "You don't have an account.", "functions/failed-precondition": - "Phone number is not linked to this account. Complete phone verification first." + "Phone number is not linked to this account. Complete phone verification first.", + "auth/credential-already-in-use": + "This phone number is already linked to another account.", + "auth/provider-already-linked": + "This account already has a phone number linked.", + "auth/invalid-phone-number": + "Please enter a valid phone number (e.g. 617 555-1234).", + "auth/operation-not-allowed": + "Phone verification is not enabled. Please try again later or contact us at info@mapletestimony.org." } -function getErrorMessage(errorCode?: string) { +export function getErrorMessage(errorCode?: string) { const niceErrorMessage = errorCode ? errorMessages[errorCode] : undefined return niceErrorMessage || "Something went wrong!" } diff --git a/public/locales/en/editProfile.json b/public/locales/en/editProfile.json index 84b6209a7..7d873d1c4 100644 --- a/public/locales/en/editProfile.json +++ b/public/locales/en/editProfile.json @@ -2,6 +2,22 @@ "header": "Edit Profile", "getVerified": "Get Verified", "phoneVerificationModalTitle": "Verify your phone number", + "phoneVerification": { + "phoneLabel": "Phone number (Ex 617 555-1234)", + "phonePlaceholder": "617 555-1234", + "continue": "Continue", + "codeLabel": "Verification code", + "codePlaceholder": "Enter 6-digit code", + "verify": "Verify", + "errors": { + "credentialAlreadyInUse": "This phone number is already linked to another account.", + "providerAlreadyLinked": "This account already has a phone number linked.", + "invalidPhoneNumber": "Please enter a valid phone number\n(e.g. 617 555-1234).", + "operationNotAllowed": "Phone verification is not enabled. Please try again later or contact us at info@mapletestimony.org." + }, + "signedInRequired": "You must be signed in to verify your phone.", + "enterVerificationCode": "Please enter the verification code." + }, "verifiedUser": "Verified User", "verifiedUserBadgeAlt": "Verified user badge", "setting": "Settings", From 90cac37775ddef6be735ddf8643c5a20b8aa967a Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Mon, 9 Mar 2026 17:39:28 -0400 Subject: [PATCH 6/8] Added handling for account-exists-with-different-credential error --- components/EditProfilePage/PhoneVerificationModal.tsx | 5 ++++- components/auth/hooks.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/components/EditProfilePage/PhoneVerificationModal.tsx b/components/EditProfilePage/PhoneVerificationModal.tsx index 37ca568f4..830b960e6 100644 --- a/components/EditProfilePage/PhoneVerificationModal.tsx +++ b/components/EditProfilePage/PhoneVerificationModal.tsx @@ -17,7 +17,10 @@ import { useTranslation } from "next-i18next" const US_REGEX = /^(\([2-9][0-9]{2}\)|[2-9][0-9]{2})[- ]?([0-9]{3})[- ]?([0-9]{4})$/; const AUTH_ERROR_CODE_TO_KEY: Record = { - "auth/credential-already-in-use": "phoneVerification.errors.credentialAlreadyInUse", + "auth/credential-already-in-use": + "phoneVerification.errors.credentialAlreadyInUse", + "auth/account-exists-with-different-credential": + "phoneVerification.errors.credentialAlreadyInUse", "auth/provider-already-linked": "phoneVerification.errors.providerAlreadyLinked", "auth/invalid-phone-number": "phoneVerification.errors.invalidPhoneNumber", "auth/operation-not-allowed": "phoneVerification.errors.operationNotAllowed" diff --git a/components/auth/hooks.ts b/components/auth/hooks.ts index 110002900..a618d6460 100644 --- a/components/auth/hooks.ts +++ b/components/auth/hooks.ts @@ -24,6 +24,8 @@ const errorMessages: Record = { "Phone number is not linked to this account. Complete phone verification first.", "auth/credential-already-in-use": "This phone number is already linked to another account.", + "auth/account-exists-with-different-credential": + "This phone number is already linked to another account.", "auth/provider-already-linked": "This account already has a phone number linked.", "auth/invalid-phone-number": From 02e80a9a712d589a4966674dbb299604b3c4b006 Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Wed, 18 Mar 2026 11:34:44 -0400 Subject: [PATCH 7/8] fixed formatting --- .../PhoneVerificationModal.tsx | 10 +++--- .../src/profile/completePhoneVerification.ts | 35 +++++++++---------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/components/EditProfilePage/PhoneVerificationModal.tsx b/components/EditProfilePage/PhoneVerificationModal.tsx index 830b960e6..718dbc299 100644 --- a/components/EditProfilePage/PhoneVerificationModal.tsx +++ b/components/EditProfilePage/PhoneVerificationModal.tsx @@ -14,14 +14,16 @@ import { useCompletePhoneVerification } from "../auth/hooks" import { auth } from "../firebase" import { useTranslation } from "next-i18next" -const US_REGEX = /^(\([2-9][0-9]{2}\)|[2-9][0-9]{2})[- ]?([0-9]{3})[- ]?([0-9]{4})$/; +const US_REGEX = + /^(\([2-9][0-9]{2}\)|[2-9][0-9]{2})[- ]?([0-9]{3})[- ]?([0-9]{4})$/ const AUTH_ERROR_CODE_TO_KEY: Record = { "auth/credential-already-in-use": "phoneVerification.errors.credentialAlreadyInUse", "auth/account-exists-with-different-credential": "phoneVerification.errors.credentialAlreadyInUse", - "auth/provider-already-linked": "phoneVerification.errors.providerAlreadyLinked", + "auth/provider-already-linked": + "phoneVerification.errors.providerAlreadyLinked", "auth/invalid-phone-number": "phoneVerification.errors.invalidPhoneNumber", "auth/operation-not-allowed": "phoneVerification.errors.operationNotAllowed" } @@ -83,8 +85,8 @@ export default function PhoneVerificationModal({ setError(getModalErrorMessage("auth/invalid-phone-number")) return } - const phoneDigits = trimmed.replace(/\D/g, ""); - const firebasePhoneFormat = `+1${phoneDigits}`; + const phoneDigits = trimmed.replace(/\D/g, "") + const firebasePhoneFormat = `+1${phoneDigits}` if (!user) { setError(t("phoneVerification.signedInRequired")) diff --git a/functions/src/profile/completePhoneVerification.ts b/functions/src/profile/completePhoneVerification.ts index e174f39d6..b5a28bed2 100644 --- a/functions/src/profile/completePhoneVerification.ts +++ b/functions/src/profile/completePhoneVerification.ts @@ -2,25 +2,24 @@ import * as functions from "firebase-functions" import { db, auth } from "../firebase" import { checkAuth, fail } from "../common" -export const completePhoneVerification = functions.https.onCall(async (_, context) => { - const uid = checkAuth(context) +export const completePhoneVerification = functions.https.onCall( + async (_, context) => { + const uid = checkAuth(context) - const user = await auth.getUser(uid) - const hasPhone = user.providerData?.some( - (p) => p.providerId === "phone" - ) + const user = await auth.getUser(uid) + const hasPhone = user.providerData?.some(p => p.providerId === "phone") - if (!hasPhone) { - throw fail( - "failed-precondition", - "Phone number is not linked to this account. Complete phone verification first." - ) - } + if (!hasPhone) { + throw fail( + "failed-precondition", + "Phone number is not linked to this account. Complete phone verification first." + ) + } - await db.doc(`/profiles/${uid}`).set( - { phoneVerified: true }, - { merge: true } - ) + await db + .doc(`/profiles/${uid}`) + .set({ phoneVerified: true }, { merge: true }) - return { phoneVerified: true } -}) + return { phoneVerified: true } + } +) From 7a799930f7496563736f25c7015276e37d76b44e Mon Sep 17 00:00:00 2001 From: janicelichtman Date: Wed, 18 Mar 2026 14:49:33 -0400 Subject: [PATCH 8/8] Added a feature flag to hide phone verification UI changes behind --- .../EditProfilePage/EditProfileHeader.tsx | 37 ++++++++++--------- components/featureFlags.ts | 13 +++++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/components/EditProfilePage/EditProfileHeader.tsx b/components/EditProfilePage/EditProfileHeader.tsx index 8bc239587..8f0b1a111 100644 --- a/components/EditProfilePage/EditProfileHeader.tsx +++ b/components/EditProfilePage/EditProfileHeader.tsx @@ -3,6 +3,7 @@ import { Role } from "../auth" import { Col, Row } from "../bootstrap" import { GearIcon, OutlineButton } from "../buttons" import { ProfileEditToggle } from "components/ProfilePage/ProfileButtons" +import { useFlags } from "components/featureFlags" export const EditProfileHeader = ({ formUpdated, @@ -20,6 +21,7 @@ export const EditProfileHeader = ({ phoneVerified?: boolean }) => { const { t } = useTranslation("editProfile") + const { phoneVerificationUI } = useFlags() return ( @@ -34,24 +36,25 @@ export const EditProfileHeader = ({ onClick={() => onSettingsModalOpen()} /> - {phoneVerified === true ? ( -
- {t("verifiedUser")} - {t("verifiedUserBadgeAlt")} + {t("verifiedUser")} + {t("verifiedUserBadgeAlt")} +
+ ) : onGetVerifiedClick ? ( + -
- ) : onGetVerifiedClick ? ( - - ) : null} + ) : null)} ) diff --git a/components/featureFlags.ts b/components/featureFlags.ts index 11f3b438f..5e080a850 100644 --- a/components/featureFlags.ts +++ b/components/featureFlags.ts @@ -15,7 +15,9 @@ export const FeatureFlags = z.object({ /** LLM Bill Summary and Tags **/ showLLMFeatures: z.boolean().default(false), /** Hearings and Transcriptions **/ - hearingsAndTranscriptions: z.boolean().default(false) + hearingsAndTranscriptions: z.boolean().default(false), + /** Phone Verification UI changes **/ + phoneVerificationUI: z.boolean().default(false) }) export type FeatureFlags = z.infer @@ -35,7 +37,8 @@ const defaults: Record = { followOrg: true, lobbyingTable: false, showLLMFeatures: true, - hearingsAndTranscriptions: true + hearingsAndTranscriptions: true, + phoneVerificationUI: true }, production: { testimonyDiffing: false, @@ -44,7 +47,8 @@ const defaults: Record = { followOrg: true, lobbyingTable: false, showLLMFeatures: true, - hearingsAndTranscriptions: true + hearingsAndTranscriptions: true, + phoneVerificationUI: false }, test: { testimonyDiffing: false, @@ -53,7 +57,8 @@ const defaults: Record = { followOrg: true, lobbyingTable: false, showLLMFeatures: true, - hearingsAndTranscriptions: true + hearingsAndTranscriptions: true, + phoneVerificationUI: true } }