From 099c1ad067adba8c25d127b7dfcfdd82ec2fdf1e Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 15:31:22 -0400 Subject: [PATCH 01/46] Allow starting over from verification page --- app/web/features/auth/locales/en.json | 1 + .../signup/ResendVerificationEmailForm.tsx | 22 +++++++++++++++++-- app/web/features/auth/useAuthStore.ts | 5 +++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index 8c73cb7092..f26c2c1192 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -176,6 +176,7 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click <2>here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", + "sign_up_restart_signup": "Mistype your email address? Click <2>here to start over.", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", "unhandled_sign_up_state": "Error: unhandled signup flow state.", diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index c99b347408..bebe713017 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,6 +12,12 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); + console.log("INRYO", authState, authActions); + const handleRestartSignup = async () => { + let state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); + state.needBasic = true; + authActions.updateSignupState(state); + }; const [resent, setResent] = useState(false); const mutation = useMutation({ @@ -21,14 +27,13 @@ export default function ResendVerificationEmailForm() { setResent(true); }, }); - return ( <> {mutation.error && {mutation.error.message || ""}} {t("auth:sign_up_completed_prompt")} - + {!resent ? ( {t("auth:sign_up_resend_verification_done")} )} + + + ), + }} + /> + ); } diff --git a/app/web/features/auth/useAuthStore.ts b/app/web/features/auth/useAuthStore.ts index c9d296443c..126bf44046 100644 --- a/app/web/features/auth/useAuthStore.ts +++ b/app/web/features/auth/useAuthStore.ts @@ -143,6 +143,11 @@ export default function useAuthStore() { return; } }, + async restartSignup() { + return client.signupFlow({ + restartSignup: true, + }); + }, async firstLogin(res: AuthRes.AsObject) { setError(null); setUserId(res.userId); From dd7551ccae99a04b0b3155ec658c085194684488 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 15:36:30 -0400 Subject: [PATCH 02/46] Allow starting over from verification page --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index bebe713017..d6d14adc42 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -11,8 +11,6 @@ import { service } from "service"; export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); - - console.log("INRYO", authState, authActions); const handleRestartSignup = async () => { let state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); state.needBasic = true; From 38b5bac9c1fbff021f74807340f6c0e4eff3c003 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 15:40:55 -0400 Subject: [PATCH 03/46] reverting unnecessary changes to useAuthStore --- app/web/features/auth/useAuthStore.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/web/features/auth/useAuthStore.ts b/app/web/features/auth/useAuthStore.ts index 126bf44046..c9d296443c 100644 --- a/app/web/features/auth/useAuthStore.ts +++ b/app/web/features/auth/useAuthStore.ts @@ -143,11 +143,6 @@ export default function useAuthStore() { return; } }, - async restartSignup() { - return client.signupFlow({ - restartSignup: true, - }); - }, async firstLogin(res: AuthRes.AsObject) { setError(null); setUserId(res.userId); From 88ffc2683c74cb7a9b72ea1ab372bbbb035139fd Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:50:22 +0000 Subject: [PATCH 04/46] Format frontend --- .../signup/ResendVerificationEmailForm.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index d6d14adc42..36852aebcf 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,9 +12,9 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); const handleRestartSignup = async () => { - let state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); - state.needBasic = true; - authActions.updateSignupState(state); + const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); + state.needBasic = true; + authActions.updateSignupState(state); }; const [resent, setResent] = useState(false); @@ -51,18 +51,13 @@ export default function ResendVerificationEmailForm() { <>{t("auth:sign_up_resend_verification_done")} )} - - - ), - }} - /> + + , + }} + /> ); From 98f4ddb8ef6e9c69040e32f87f85092a7e329bfa Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 17:06:06 -0400 Subject: [PATCH 05/46] specifically clearing flowToken on restart --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 36852aebcf..fbfa23233b 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,7 +12,8 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); const handleRestartSignup = async () => { - const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); + let state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); + state.flowToken = undefined; state.needBasic = true; authActions.updateSignupState(state); }; From 910ee53db1d204a1c8289241ae0d0904abf25496 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:10:16 +0000 Subject: [PATCH 06/46] Format frontend --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index fbfa23233b..372205e676 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,7 +12,7 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); const handleRestartSignup = async () => { - let state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); + const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); state.flowToken = undefined; state.needBasic = true; authActions.updateSignupState(state); From 296cec6b5c4459e0c98c4986d817b18d4017200c Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 18:37:28 -0400 Subject: [PATCH 07/46] use function signupFlowRestartSignup --- .../features/auth/signup/ResendVerificationEmailForm.tsx | 3 +-- app/web/service/auth.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 372205e676..2ca1122f34 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,8 +12,7 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); const handleRestartSignup = async () => { - const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); - state.flowToken = undefined; + let state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); state.needBasic = true; authActions.updateSignupState(state); }; diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 7fcd4f1f50..cd98d87df0 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -144,6 +144,14 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { return res.toObject(); } +export async function signupFlowRestartSignup(flowToken: string) { + const req = new SignupFlowReq(); + req.setFlowToken(flowToken); + const res = await client.auth.signupFlow(req); + return res.toObject(); +} + + export async function validateUsername(username: string) { const req = new UsernameValidReq(); req.setUsername(username); From aacce9093dee7a6329e1f889f9fd9d591baf1710 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:39:37 +0000 Subject: [PATCH 08/46] Format frontend --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 2 +- app/web/service/auth.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 2ca1122f34..53a851cd5c 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -12,7 +12,7 @@ export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); const handleRestartSignup = async () => { - let state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); + const state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); state.needBasic = true; authActions.updateSignupState(state); }; diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index cd98d87df0..cdc0f025bd 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -151,7 +151,6 @@ export async function signupFlowRestartSignup(flowToken: string) { return res.toObject(); } - export async function validateUsername(username: string) { const req = new UsernameValidReq(); req.setUsername(username); From 3d2a518030bfc2e786fdd7a1b05ee904404d162b Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 20:21:11 -0400 Subject: [PATCH 09/46] add more error handling --- .../signup/ResendVerificationEmailForm.tsx | 34 ++++++++++++------- app/web/service/auth.ts | 4 ++- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 53a851cd5c..7c2cc8bef8 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -11,23 +11,36 @@ import { service } from "service"; export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); - const handleRestartSignup = async () => { - const state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); - state.needBasic = true; - authActions.updateSignupState(state); - }; const [resent, setResent] = useState(false); - - const mutation = useMutation({ + const mutationResend = useMutation({ mutationFn: async () => { const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); authActions.updateSignupState(state); setResent(true); }, }); + const mutationRestart = useMutation({ + mutationFn: async () => { + let state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); + state.needBasic = true; + authActions.updateSignupState(state); + }, + }); + + const handleResendVerification = async (e) =>{ + e.preventDefault(); + mutationResend.mutateAsync(); + }; + + const handleRestartSignup = async (e) =>{ + e.preventDefault(); + mutationRestart.mutateAsync(); + }; + return ( <> - {mutation.error && {mutation.error.message || ""}} + {mutationResend.error && {mutationResend.error.message || ""}} + {mutationRestart.error && {mutationRestart.error.message || ""}} {t("auth:sign_up_completed_prompt")} @@ -39,10 +52,7 @@ export default function ResendVerificationEmailForm() { 2: ( { - e.preventDefault(); - mutation.mutateAsync(); - }} + onClick={handleResendVerification} /> ), }} diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index cdc0f025bd..67b00c1dea 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -137,14 +137,16 @@ export async function signupFlowCommunityGuidelines(flowToken: string, accept: b } export async function signupFlowResendVerificationEmail(flowToken: string) { + console.log("TEST1"); const req = new SignupFlowReq(); req.setFlowToken(flowToken); - req.setResendVerificationEmail(true); + // req.setResendVerificationEmail(true); const res = await client.auth.signupFlow(req); return res.toObject(); } export async function signupFlowRestartSignup(flowToken: string) { + console.log("TEST2"); const req = new SignupFlowReq(); req.setFlowToken(flowToken); const res = await client.auth.signupFlow(req); From 29a63a38f99b828df1bd5ebd73e4d67121201507 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 20:22:53 -0400 Subject: [PATCH 10/46] add more error handling --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 3 --- app/web/service/auth.ts | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 7c2cc8bef8..4cb1c468ab 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -26,17 +26,14 @@ export default function ResendVerificationEmailForm() { authActions.updateSignupState(state); }, }); - const handleResendVerification = async (e) =>{ e.preventDefault(); mutationResend.mutateAsync(); }; - const handleRestartSignup = async (e) =>{ e.preventDefault(); mutationRestart.mutateAsync(); }; - return ( <> {mutationResend.error && {mutationResend.error.message || ""}} diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 67b00c1dea..cdc0f025bd 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -137,16 +137,14 @@ export async function signupFlowCommunityGuidelines(flowToken: string, accept: b } export async function signupFlowResendVerificationEmail(flowToken: string) { - console.log("TEST1"); const req = new SignupFlowReq(); req.setFlowToken(flowToken); - // req.setResendVerificationEmail(true); + req.setResendVerificationEmail(true); const res = await client.auth.signupFlow(req); return res.toObject(); } export async function signupFlowRestartSignup(flowToken: string) { - console.log("TEST2"); const req = new SignupFlowReq(); req.setFlowToken(flowToken); const res = await client.auth.signupFlow(req); From 4953dd6651eba42ff0a5d01585b0374429d86409 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:25:05 +0000 Subject: [PATCH 11/46] Format frontend --- .../auth/signup/ResendVerificationEmailForm.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 4cb1c468ab..9b91eea75a 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -21,16 +21,16 @@ export default function ResendVerificationEmailForm() { }); const mutationRestart = useMutation({ mutationFn: async () => { - let state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); + const state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); state.needBasic = true; authActions.updateSignupState(state); }, }); - const handleResendVerification = async (e) =>{ + const handleResendVerification = async (e) => { e.preventDefault(); mutationResend.mutateAsync(); }; - const handleRestartSignup = async (e) =>{ + const handleRestartSignup = async (e) => { e.preventDefault(); mutationRestart.mutateAsync(); }; @@ -46,12 +46,7 @@ export default function ResendVerificationEmailForm() { - ), + 2: , }} /> ) : ( From f7e846e929cd2752abb19051f77258dbb2f4b543 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 20:59:15 -0400 Subject: [PATCH 12/46] add more error handling --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 9b91eea75a..b4a95d9ab5 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -26,11 +26,11 @@ export default function ResendVerificationEmailForm() { authActions.updateSignupState(state); }, }); - const handleResendVerification = async (e) => { + const handleResendVerification = (e) =>{ e.preventDefault(); mutationResend.mutateAsync(); }; - const handleRestartSignup = async (e) => { + const handleRestartSignup = (e) =>{ e.preventDefault(); mutationRestart.mutateAsync(); }; From bf27d5c08662b9d2be07a2cff71066951eac03a2 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:03:00 +0000 Subject: [PATCH 13/46] Format frontend --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index b4a95d9ab5..33970d5017 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -26,11 +26,11 @@ export default function ResendVerificationEmailForm() { authActions.updateSignupState(state); }, }); - const handleResendVerification = (e) =>{ + const handleResendVerification = (e) => { e.preventDefault(); mutationResend.mutateAsync(); }; - const handleRestartSignup = (e) =>{ + const handleRestartSignup = (e) => { e.preventDefault(); mutationRestart.mutateAsync(); }; From 9fac54bdb9e9eecee0ae2007a8aa863dc4071568 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 3 Aug 2026 21:20:41 -0400 Subject: [PATCH 14/46] add type for e --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 33970d5017..5aa03cf12f 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -26,11 +26,11 @@ export default function ResendVerificationEmailForm() { authActions.updateSignupState(state); }, }); - const handleResendVerification = (e) => { + const handleResendVerification = (e: React.MouseEvent) => { e.preventDefault(); mutationResend.mutateAsync(); }; - const handleRestartSignup = (e) => { + const handleRestartSignup = (e: React.MouseEvent) => { e.preventDefault(); mutationRestart.mutateAsync(); }; From 9cf499d019a628a4f6dc8f7b5b1fbc5acec88bc2 Mon Sep 17 00:00:00 2001 From: nc225 Date: Wed, 19 Aug 2026 14:14:36 -0400 Subject: [PATCH 15/46] use new link reference system --- app/web/features/auth/locales/en.json | 2 +- .../auth/signup/ResendVerificationEmailForm.tsx | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index 4b8d0f4543..f080a5e96c 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -176,7 +176,7 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", - "sign_up_restart_signup": "Mistype your email address? Click <2>here to start over.", + "sign_up_restart_signup": "Mistype your email address? Click here to start over.", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", "unhandled_sign_up_state": "Error: unhandled signup flow state.", diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index bffe40caed..13ff60b503 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -17,7 +17,8 @@ export default function ResendVerificationEmailForm() { const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); authActions.updateSignupState(state); setResent(true); - }, + },app/web/features/auth/signup/ResendVerificationEmailForm.tsx + }); const mutationRestart = useMutation({ mutationFn: async () => { @@ -58,9 +59,11 @@ export default function ResendVerificationEmailForm() { , - }} + components={{ + restartSignup: ( + , + ), + }} /> From 0bf8c4900bd056516fbb920da9844ce724fd137a Mon Sep 17 00:00:00 2001 From: nc225 Date: Wed, 19 Aug 2026 14:15:22 -0400 Subject: [PATCH 16/46] use new link reference system --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 13ff60b503..0cfed0cc69 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -17,8 +17,7 @@ export default function ResendVerificationEmailForm() { const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); authActions.updateSignupState(state); setResent(true); - },app/web/features/auth/signup/ResendVerificationEmailForm.tsx - + }, }); const mutationRestart = useMutation({ mutationFn: async () => { From d91faf9949e02ae7f0826b38edadbd9a0e800a52 Mon Sep 17 00:00:00 2001 From: nc225 Date: Wed, 19 Aug 2026 14:16:34 -0400 Subject: [PATCH 17/46] use new link reference system --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 0cfed0cc69..2cd74391e9 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -47,7 +47,7 @@ export default function ResendVerificationEmailForm() { i18nKey="auth:sign_up_resend_verification_email_help" components={{ resendLink: ( - , + ), }} /> @@ -60,7 +60,7 @@ export default function ResendVerificationEmailForm() { i18nKey="auth:sign_up_restart_signup" components={{ restartSignup: ( - , + ), }} /> From 15a62afbc5fc390ef5d101d20f2e70295bdcea44 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:20:02 +0000 Subject: [PATCH 18/46] Format frontend --- .../auth/signup/ResendVerificationEmailForm.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 2cd74391e9..f0931dbf2c 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -46,9 +46,7 @@ export default function ResendVerificationEmailForm() { - ), + resendLink: , }} /> ) : ( @@ -58,11 +56,9 @@ export default function ResendVerificationEmailForm() { - ), - }} + components={{ + restartSignup: , + }} /> From c0935c8d6209e4538a83cb98d7d4ff7cf56b14b8 Mon Sep 17 00:00:00 2001 From: nc225 Date: Fri, 21 Aug 2026 12:16:17 -0400 Subject: [PATCH 19/46] allow signup restart by deleting flowtoken --- app/web/features/auth/locales/en.json | 4 ++-- app/web/features/auth/signup/BasicForm.tsx | 2 ++ .../auth/signup/ResendVerificationEmailForm.tsx | 14 +++++++++----- app/web/features/auth/useAuthStore.ts | 4 ++++ app/web/service/auth.ts | 7 ------- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index f080a5e96c..5a99ad125c 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -176,8 +176,8 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", - "sign_up_restart_signup": "Mistype your email address? Click here to start over.", - "sign_up_completed_prompt": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", + "sign_up_restart_signup": "Mistyped your email address? Click here to start over.", + "sign_up_completed_prompt": "We have sent an email with a verification link to your email address: {{providedEmailAddress}}. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", "unhandled_sign_up_state": "Error: unhandled signup flow state.", "login_prompt": "Click here to log in", diff --git a/app/web/features/auth/signup/BasicForm.tsx b/app/web/features/auth/signup/BasicForm.tsx index 7f443f3fda..33e603d987 100644 --- a/app/web/features/auth/signup/BasicForm.tsx +++ b/app/web/features/auth/signup/BasicForm.tsx @@ -45,6 +45,8 @@ export default function BasicForm({ submitText, successCallback, inviteCode }: B mutationFn: async (data) => { const sanitizedEmail = lowercaseAndTrimField(data.email); const sanitizedName = data.name.trim(); + // locally store email for recall on verification page + localStorage.setItem("signupEmail", sanitizedEmail); const state = await service.auth.startSignup(sanitizedName, sanitizedEmail, inviteCode); doAntibot("signup"); return authActions.updateSignupState(state); diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index f0931dbf2c..b74d8b4f5c 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -21,9 +21,7 @@ export default function ResendVerificationEmailForm() { }); const mutationRestart = useMutation({ mutationFn: async () => { - const state = await service.auth.signupFlowRestartSignup(authState.flowState!.flowToken); - state.needBasic = true; - authActions.updateSignupState(state); + await authActions.restartSignup(); }, }); const handleResendVerification = (e: React.MouseEvent) => { @@ -39,8 +37,14 @@ export default function ResendVerificationEmailForm() { {mutationResend.error && {mutationResend.error.message || ""}} {mutationRestart.error && {mutationRestart.error.message || ""}} - {t("auth:sign_up_completed_prompt")} - + { + } + {!resent ? ( Date: Fri, 21 Aug 2026 16:18:40 +0000 Subject: [PATCH 20/46] Format frontend --- app/web/features/auth/signup/ResendVerificationEmailForm.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index b74d8b4f5c..61d546e5e5 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -37,14 +37,15 @@ export default function ResendVerificationEmailForm() { {mutationResend.error && {mutationResend.error.message || ""}} {mutationRestart.error && {mutationRestart.error.message || ""}} - { } - + {!resent ? ( Date: Fri, 21 Aug 2026 13:22:46 -0400 Subject: [PATCH 21/46] updating signuptest to show test email --- app/web/features/auth/signup/Signup.test.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/web/features/auth/signup/Signup.test.tsx b/app/web/features/auth/signup/Signup.test.tsx index 4b44345371..2fe6982419 100644 --- a/app/web/features/auth/signup/Signup.test.tsx +++ b/app/web/features/auth/signup/Signup.test.tsx @@ -446,11 +446,27 @@ describe("Signup", () => { needVerifyEmail: true, flowToken: "token", }; + + const providedEmailAddress = "test@example.com"; + window.localStorage.setItem("auth.flowState", JSON.stringify(state)); + window.localStorage.setItem("signupEmail", providedEmailAddress); + + // Removing tags so the translated message can be compared with the element's textContent. + const message = t("auth:sign_up_completed_prompt", { + providedEmailAddress: "test@example.com", + }).replace(/<\/?strong>/g, ""); + render(, { wrapper }); - expect(screen.getByText(t("auth:sign_up_completed_prompt"))).toBeVisible(); + expect( + screen.getByText((_, element) => { + return ( + element?.textContent === + `${message}` + ); + }) + ).toBeVisible(); }); - it("displays the redirect message when nothing is pending and has authRes", async () => { const state: SignupFlowRes.AsObject = { needBasic: false, From dc1b505def5cd908e9377e2dfee7d070d20edeb8 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:25:05 +0000 Subject: [PATCH 22/46] Format frontend --- app/web/features/auth/signup/Signup.test.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/web/features/auth/signup/Signup.test.tsx b/app/web/features/auth/signup/Signup.test.tsx index 2fe6982419..b0cf833d93 100644 --- a/app/web/features/auth/signup/Signup.test.tsx +++ b/app/web/features/auth/signup/Signup.test.tsx @@ -446,7 +446,7 @@ describe("Signup", () => { needVerifyEmail: true, flowToken: "token", }; - + const providedEmailAddress = "test@example.com"; window.localStorage.setItem("auth.flowState", JSON.stringify(state)); @@ -456,15 +456,12 @@ describe("Signup", () => { const message = t("auth:sign_up_completed_prompt", { providedEmailAddress: "test@example.com", }).replace(/<\/?strong>/g, ""); - + render(, { wrapper }); expect( screen.getByText((_, element) => { - return ( - element?.textContent === - `${message}` - ); - }) + return element?.textContent === `${message}`; + }), ).toBeVisible(); }); it("displays the redirect message when nothing is pending and has authRes", async () => { From 20a7e159d690d8aefa69e457806b8dc3c9489219 Mon Sep 17 00:00:00 2001 From: nc225 Date: Fri, 21 Aug 2026 13:38:32 -0400 Subject: [PATCH 23/46] simplifying resendVerification functions --- .../signup/ResendVerificationEmailForm.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 61d546e5e5..5a06e4147f 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -24,14 +24,6 @@ export default function ResendVerificationEmailForm() { await authActions.restartSignup(); }, }); - const handleResendVerification = (e: React.MouseEvent) => { - e.preventDefault(); - mutationResend.mutateAsync(); - }; - const handleRestartSignup = (e: React.MouseEvent) => { - e.preventDefault(); - mutationRestart.mutateAsync(); - }; return ( <> {mutationResend.error && {mutationResend.error.message || ""}} @@ -51,7 +43,10 @@ export default function ResendVerificationEmailForm() { , + resendLink: { + e.preventDefault(); + mutationResend.mutateAsync(); + }}/>, }} /> ) : ( @@ -62,7 +57,10 @@ export default function ResendVerificationEmailForm() { , + restartSignup: { + e.preventDefault(); + mutationRestart.mutateAsync(); + }}/>, }} /> From ab3a37e0ff451392dbe822c1b2760ddec390754d Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:40:29 +0000 Subject: [PATCH 24/46] Format frontend --- .../signup/ResendVerificationEmailForm.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 5a06e4147f..117eacdd92 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -43,10 +43,15 @@ export default function ResendVerificationEmailForm() { { + resendLink: ( + { e.preventDefault(); mutationResend.mutateAsync(); - }}/>, + }} + /> + ), }} /> ) : ( @@ -57,10 +62,15 @@ export default function ResendVerificationEmailForm() { { - e.preventDefault(); - mutationRestart.mutateAsync(); - }}/>, + restartSignup: ( + { + e.preventDefault(); + mutationRestart.mutateAsync(); + }} + /> + ), }} /> From e45fd1b50aed49139e61d0333a217fff244908bc Mon Sep 17 00:00:00 2001 From: nc225 Date: Fri, 21 Aug 2026 13:41:37 -0400 Subject: [PATCH 25/46] using testEmail variable --- app/web/features/auth/signup/Signup.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/web/features/auth/signup/Signup.test.tsx b/app/web/features/auth/signup/Signup.test.tsx index b0cf833d93..c36cfd3bc6 100644 --- a/app/web/features/auth/signup/Signup.test.tsx +++ b/app/web/features/auth/signup/Signup.test.tsx @@ -447,14 +447,14 @@ describe("Signup", () => { flowToken: "token", }; - const providedEmailAddress = "test@example.com"; + const testEmail = "test@example.com"; window.localStorage.setItem("auth.flowState", JSON.stringify(state)); - window.localStorage.setItem("signupEmail", providedEmailAddress); + window.localStorage.setItem("signupEmail", testEmail); // Removing tags so the translated message can be compared with the element's textContent. const message = t("auth:sign_up_completed_prompt", { - providedEmailAddress: "test@example.com", + providedEmailAddress: testEmail, }).replace(/<\/?strong>/g, ""); render(, { wrapper }); From e4b3c7b4539b400ed7043da6a07c684cc6d441e9 Mon Sep 17 00:00:00 2001 From: nc225 Date: Sat, 22 Aug 2026 00:55:46 -0400 Subject: [PATCH 26/46] revise restartSignup process to use authStore --- app/web/features/auth/locales/en.json | 2 +- app/web/features/auth/signup/BasicForm.tsx | 3 +-- .../signup/ResendVerificationEmailForm.tsx | 24 ++++++------------- app/web/features/auth/signup/Signup.test.tsx | 19 ++++++++------- app/web/features/auth/useAuthStore.ts | 11 +++++++-- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index 5a99ad125c..0f02ea8abb 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -176,7 +176,7 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", - "sign_up_restart_signup": "Mistyped your email address? Click here to start over.", + "sign_up_restart_signup": "Mistyped your email address? Click here to start over.", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address: {{providedEmailAddress}}. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", "unhandled_sign_up_state": "Error: unhandled signup flow state.", diff --git a/app/web/features/auth/signup/BasicForm.tsx b/app/web/features/auth/signup/BasicForm.tsx index 33e603d987..e6f0a77cce 100644 --- a/app/web/features/auth/signup/BasicForm.tsx +++ b/app/web/features/auth/signup/BasicForm.tsx @@ -45,8 +45,7 @@ export default function BasicForm({ submitText, successCallback, inviteCode }: B mutationFn: async (data) => { const sanitizedEmail = lowercaseAndTrimField(data.email); const sanitizedName = data.name.trim(); - // locally store email for recall on verification page - localStorage.setItem("signupEmail", sanitizedEmail); + authActions.setSignupEmail(sanitizedEmail); const state = await service.auth.startSignup(sanitizedName, sanitizedEmail, inviteCode); doAntibot("signup"); return authActions.updateSignupState(state); diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 117eacdd92..2f7ccc40b2 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -33,7 +33,7 @@ export default function ResendVerificationEmailForm() { } @@ -43,15 +43,10 @@ export default function ResendVerificationEmailForm() { { + resendLink: { e.preventDefault(); mutationResend.mutateAsync(); - }} - /> - ), + }}/>, }} /> ) : ( @@ -62,15 +57,10 @@ export default function ResendVerificationEmailForm() { { - e.preventDefault(); - mutationRestart.mutateAsync(); - }} - /> - ), + restartSignup: { + e.preventDefault(); + mutationRestart.mutateAsync(); + }}/>, }} /> diff --git a/app/web/features/auth/signup/Signup.test.tsx b/app/web/features/auth/signup/Signup.test.tsx index c36cfd3bc6..7708edacee 100644 --- a/app/web/features/auth/signup/Signup.test.tsx +++ b/app/web/features/auth/signup/Signup.test.tsx @@ -449,21 +449,22 @@ describe("Signup", () => { const testEmail = "test@example.com"; - window.localStorage.setItem("auth.flowState", JSON.stringify(state)); - window.localStorage.setItem("signupEmail", testEmail); - - // Removing tags so the translated message can be compared with the element's textContent. - const message = t("auth:sign_up_completed_prompt", { - providedEmailAddress: testEmail, - }).replace(/<\/?strong>/g, ""); + localStorage.setItem("auth.flowState", JSON.stringify(state)); + localStorage.setItem("auth.signupEmail", JSON.stringify(testEmail)); render(, { wrapper }); + expect( - screen.getByText((_, element) => { - return element?.textContent === `${message}`; + await screen.findByText((_, element) => { + return ( + element?.textContent === + `We have sent an email with a verification link to your email address: ${testEmail}. Please click the link to activate your account.` + ); }), ).toBeVisible(); }); + + it("displays the redirect message when nothing is pending and has authRes", async () => { const state: SignupFlowRes.AsObject = { needBasic: false, diff --git a/app/web/features/auth/useAuthStore.ts b/app/web/features/auth/useAuthStore.ts index 9e2b587b59..3413b2cc77 100644 --- a/app/web/features/auth/useAuthStore.ts +++ b/app/web/features/auth/useAuthStore.ts @@ -47,6 +47,7 @@ export default function useAuthStore() { const [userId, setUserId] = usePersistedState("auth.userId", null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [signupEmail, setSignupEmail] = usePersistedState("auth.signupEmail", null); const [flowState, setFlowState] = usePersistedState("auth.flowState", null); //this is used to set the current user in the user cache @@ -139,13 +140,18 @@ export default function useAuthStore() { setFlowState(state); if (state.authRes) { setFlowState(null); + setSignupEmail(null); authActions.firstLogin(state.authRes!); return; } }, + setSignupEmail(email: string) { + setSignupEmail(email); + }, async restartSignup() { setFlowState(null); setError(null); + setSignupEmail(null); }, async firstLogin(res: AuthRes.AsObject) { setError(null); @@ -194,9 +200,9 @@ export default function useAuthStore() { setLoading(false); }, }), - //note: there should be no dependenices on the state or t, or + //note: there should be no dependencies on the state or t, or //some useEffects will break. Eg. the token login in Login.tsx - [setAuthenticated, setJailed, setUserId, setFlowState, queryClient], + [setAuthenticated, setJailed, setUserId, setFlowState, setSignupEmail, queryClient], ); return { @@ -206,6 +212,7 @@ export default function useAuthStore() { error, jailed, loading, + signupEmail, userId, flowState, }, From cbf2d14926e7e9ed30849f543b561f40462fe520 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 04:57:57 +0000 Subject: [PATCH 27/46] Format frontend --- .../signup/ResendVerificationEmailForm.tsx | 22 ++++++++++++++----- app/web/features/auth/signup/Signup.test.tsx | 1 - 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 2f7ccc40b2..3f3fd87c81 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -43,10 +43,15 @@ export default function ResendVerificationEmailForm() { { + resendLink: ( + { e.preventDefault(); mutationResend.mutateAsync(); - }}/>, + }} + /> + ), }} /> ) : ( @@ -57,10 +62,15 @@ export default function ResendVerificationEmailForm() { { - e.preventDefault(); - mutationRestart.mutateAsync(); - }}/>, + restartSignup: ( + { + e.preventDefault(); + mutationRestart.mutateAsync(); + }} + /> + ), }} /> diff --git a/app/web/features/auth/signup/Signup.test.tsx b/app/web/features/auth/signup/Signup.test.tsx index 7708edacee..d8d59b8ae0 100644 --- a/app/web/features/auth/signup/Signup.test.tsx +++ b/app/web/features/auth/signup/Signup.test.tsx @@ -464,7 +464,6 @@ describe("Signup", () => { ).toBeVisible(); }); - it("displays the redirect message when nothing is pending and has authRes", async () => { const state: SignupFlowRes.AsObject = { needBasic: false, From a9d1321e32c8c97e5ca7bdbbd67fe3ac1ed66920 Mon Sep 17 00:00:00 2001 From: nc225 Date: Sat, 22 Aug 2026 12:09:17 -0400 Subject: [PATCH 28/46] clean up and use clearer names --- app/web/features/auth/signup/BasicForm.tsx | 2 +- .../auth/signup/ResendVerificationEmailForm.tsx | 14 ++++++-------- app/web/features/auth/useAuthStore.ts | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/web/features/auth/signup/BasicForm.tsx b/app/web/features/auth/signup/BasicForm.tsx index e6f0a77cce..00e56f46f2 100644 --- a/app/web/features/auth/signup/BasicForm.tsx +++ b/app/web/features/auth/signup/BasicForm.tsx @@ -45,7 +45,7 @@ export default function BasicForm({ submitText, successCallback, inviteCode }: B mutationFn: async (data) => { const sanitizedEmail = lowercaseAndTrimField(data.email); const sanitizedName = data.name.trim(); - authActions.setSignupEmail(sanitizedEmail); + authActions.assignSignupEmail(sanitizedEmail); const state = await service.auth.startSignup(sanitizedName, sanitizedEmail, inviteCode); doAntibot("signup"); return authActions.updateSignupState(state); diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 3f3fd87c81..12708b96b5 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -29,14 +29,12 @@ export default function ResendVerificationEmailForm() { {mutationResend.error && {mutationResend.error.message || ""}} {mutationRestart.error && {mutationRestart.error.message || ""}} - { - - } + {!resent ? ( diff --git a/app/web/features/auth/useAuthStore.ts b/app/web/features/auth/useAuthStore.ts index 3413b2cc77..b801f54451 100644 --- a/app/web/features/auth/useAuthStore.ts +++ b/app/web/features/auth/useAuthStore.ts @@ -145,10 +145,10 @@ export default function useAuthStore() { return; } }, - setSignupEmail(email: string) { + assignSignupEmail(email: string) { setSignupEmail(email); }, - async restartSignup() { + restartSignup() { setFlowState(null); setError(null); setSignupEmail(null); From f16f5d537f56c0fd8a1af90c44f0ba37a701f8ea Mon Sep 17 00:00:00 2001 From: nc225 Date: Sun, 23 Aug 2026 23:00:08 -0400 Subject: [PATCH 29/46] changing signup_flows: adding signup_cancelled and adjusting username uniqueness --- app/backend/src/couchers/models/rest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/backend/src/couchers/models/rest.py b/app/backend/src/couchers/models/rest.py index e8009c5350..185dbd8eac 100644 --- a/app/backend/src/couchers/models/rest.py +++ b/app/backend/src/couchers/models/rest.py @@ -186,7 +186,8 @@ class SignupFlow(Base, kw_only=True): email_sent: Mapped[bool] = mapped_column(Boolean, default=False) email_token: Mapped[str | None] = mapped_column(String, unique=True, default=None) email_token_expiry: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) - + signup_cancelled: Mapped[bool] = mapped_column(Boolean, default=False) + ## Basic name: Mapped[str] = mapped_column(String) # TODO: unique across both tables @@ -195,7 +196,7 @@ class SignupFlow(Base, kw_only=True): ## Account # TODO: unique across both tables - username: Mapped[str | None] = mapped_column(String, unique=True, default=None) + username: Mapped[str | None] = mapped_column(String, default=None) hashed_password: Mapped[bytes | None] = mapped_column(Binary, default=None) birthdate: Mapped[date | None] = mapped_column(Date, default=None) # in the timezone of birthplace gender: Mapped[str | None] = mapped_column(String, default=None) @@ -258,6 +259,17 @@ def is_completed(self) -> Any: & self.filled_motivations ) + __table_args__ = ( + Index( + "uq_signup_flows_username", + username, + unique=True, + postgresql_where=( + (username != None) & (signup_cancelled == False) + ), + ), + ) + class AccountDeletionToken(Base, kw_only=True): __tablename__ = "account_deletion_tokens" From aeabf2b084341e7e1dbdaee0644afc178daabb16 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:01:55 +0000 Subject: [PATCH 30/46] Generate migrations --- ..._allow_starting_over_from_verification_.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py diff --git a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py new file mode 100644 index 0000000000..3a0323bec7 --- /dev/null +++ b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py @@ -0,0 +1,40 @@ +"""Allow starting over from verification page + +Revision ID: 0187 +Revises: 0186 +Create Date: 2026-08-24 03:01:54.582432 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0187" +down_revision = "0186" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("signup_flows", sa.Column("signup_cancelled", sa.Boolean(), nullable=False)) + op.drop_constraint(op.f("uq_signup_flows_username"), "signup_flows", type_="unique") + op.create_index( + "uq_signup_flows_username", + "signup_flows", + ["username"], + unique=True, + postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), + ) + + +def downgrade() -> None: + op.drop_index( + "uq_signup_flows_username", + table_name="signup_flows", + postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), + ) + op.create_unique_constraint( + op.f("uq_signup_flows_username"), "signup_flows", ["username"], postgresql_nulls_not_distinct=False + ) + op.drop_column("signup_flows", "signup_cancelled") From f720035bf3ea9a624539aabc70b7998a4877ae1e Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:02:43 +0000 Subject: [PATCH 31/46] Format backend --- app/backend/src/couchers/models/rest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/backend/src/couchers/models/rest.py b/app/backend/src/couchers/models/rest.py index 185dbd8eac..06f840124e 100644 --- a/app/backend/src/couchers/models/rest.py +++ b/app/backend/src/couchers/models/rest.py @@ -187,7 +187,7 @@ class SignupFlow(Base, kw_only=True): email_token: Mapped[str | None] = mapped_column(String, unique=True, default=None) email_token_expiry: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) signup_cancelled: Mapped[bool] = mapped_column(Boolean, default=False) - + ## Basic name: Mapped[str] = mapped_column(String) # TODO: unique across both tables @@ -264,9 +264,7 @@ def is_completed(self) -> Any: "uq_signup_flows_username", username, unique=True, - postgresql_where=( - (username != None) & (signup_cancelled == False) - ), + postgresql_where=((username != None) & (signup_cancelled == False)), ), ) From f88240c7083666bf1a9530f9c11d2c80ab3f266c Mon Sep 17 00:00:00 2001 From: nc225 Date: Sun, 23 Aug 2026 23:09:04 -0400 Subject: [PATCH 32/46] adding signup_cancelled --- .../versions/0187_add_signup_cancelled.py | 29 +++++++++++++++++++ app/backend/src/couchers/servicers/auth.py | 9 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py diff --git a/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py b/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py new file mode 100644 index 0000000000..72bd2a097e --- /dev/null +++ b/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py @@ -0,0 +1,29 @@ +"""add signup cancelled + +Revision ID: 0187 +Revises: 0186 +Create Date: 2026-08-24 00:55:18.840814 + +""" +from alembic import op +import sqlalchemy as sa +import geoalchemy2 + + +# revision identifiers, used by Alembic. +revision = '0187' +down_revision = '0186' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('signup_flows', sa.Column('signup_cancelled', sa.Boolean(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('signup_flows', 'signup_cancelled') + # ### end Alembic commands ### diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py index dc488c0207..898d6f4bba 100644 --- a/app/backend/src/couchers/servicers/auth.py +++ b/app/backend/src/couchers/servicers/auth.py @@ -179,7 +179,7 @@ def _username_available(session: Session, username: str) -> bool: user_exists = session.execute(select(User).where(User.username == username)).scalar_one_or_none() is not None # check for started signup with that username signup_exists = ( - session.execute(select(SignupFlow).where(SignupFlow.username == username)).scalar_one_or_none() is not None + session.execute(select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled.is_not(True))).scalar_one_or_none() is not None ) # return False if user exists, True otherwise return not user_exists and not signup_exists @@ -278,6 +278,13 @@ def SignupFlow( ).scalar_one_or_none() if not flow: context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") + if request.cancel_signup: + flow.signup_cancelled = True + flow.email_token = None + flow.email_token_expiry = None + session.flush() + + return auth_pb2.SignupFlowRes() if request.HasField("basic"): context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_basic_filled") From f1c3642dd9de9b9b5aea9241784c675cfd81dd88 Mon Sep 17 00:00:00 2001 From: nc225 Date: Sun, 23 Aug 2026 23:10:02 -0400 Subject: [PATCH 33/46] adding signup_cancelled --- app/proto/auth.proto | 1 + app/web/.env.development | 17 +++++++++-------- .../auth/signup/ResendVerificationEmailForm.tsx | 3 ++- app/web/service/auth.ts | 10 ++++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/proto/auth.proto b/app/proto/auth.proto index f253401c4f..c36fc831d6 100644 --- a/app/proto/auth.proto +++ b/app/proto/auth.proto @@ -123,6 +123,7 @@ message SignupFlowReq { google.protobuf.BoolValue accept_community_guidelines = 6; bool resend_verification_email = 7; + bool cancel_signup = 9; } message SignupFlowRes { diff --git a/app/web/.env.development b/app/web/.env.development index c9a6905dc5..218e7b050e 100644 --- a/app/web/.env.development +++ b/app/web/.env.development @@ -1,12 +1,13 @@ -NEXT_PUBLIC_COUCHERS_ENV=preview -NEXT_PUBLIC_BASE_URL="https://next.couchershq.org" -NEXT_PUBLIC_API_BASE_URL="https://next.couchershq.org/api" -NEXT_PUBLIC_MEDIA_BASE_URL="https://dev-user-media.couchershq.org" -NEXT_PUBLIC_CONSOLE_BASE_URL="https://next-console.couchershq.org" +NEXT_PUBLIC_COUCHERS_ENV=dev +NEXT_PUBLIC_BASE_URL="/" +NEXT_PUBLIC_API_BASE_URL="http://localhost:8888" +NEXT_PUBLIC_MEDIA_BASE_URL="http://localhost:5001" +NEXT_PUBLIC_CONSOLE_BASE_URL="http://localhost:10027" +NEXT_PUBLIC_IS_POST_BETA_ENABLED=true NEXT_PUBLIC_NOMINATIM_URL="https://nominatim.openstreetmap.org/" +NEXT_PUBLIC_IS_VERIFICATION_ENABLED=true +NEXT_PUBLIC_IS_COMMUNITIES_PART2_ENABLED=true NEXT_PUBLIC_STRIPE_KEY="pk_test_51KEzByIfR5z29g5khFE5samD8XKOGLcCrM1lhCkfOomGPUFAEYOw8uAqI2Nkv33wYdPM2FgTQNTC07IiNfHY1kLJ00Jqm8Ppai" -NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/next.json" +NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/localdev.json" NEXT_PUBLIC_GROWTHBOOK_API_HOST="https://gbapi.couchershq.org" NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY="sdk-f8lwseEODN02p" -# When enabled, flags resolve from app/web/feature-flags.dev.json instead of GrowthBook (dev/testing) -NEXT_PUBLIC_FEATURE_FLAGS_OVERRIDE="1" diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 12708b96b5..1bf8d77efa 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -21,7 +21,8 @@ export default function ResendVerificationEmailForm() { }); const mutationRestart = useMutation({ mutationFn: async () => { - await authActions.restartSignup(); + const state = await service.auth.signupFlowCancelSignup(authState.flowState!.flowToken); + authActions.restartSignup(); }, }); return ( diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 7fcd4f1f50..0983caa451 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -144,6 +144,16 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { return res.toObject(); } + +export async function signupFlowCancelSignup(flowToken: string) { + const req = new SignupFlowReq(); + req.setFlowToken(flowToken); + req.setEmailToken(null); //invalidate any existing email tokens + req.setCancelSignup(true); + const res = await client.auth.signupFlow(req); + return res.toObject(); +} + export async function validateUsername(username: string) { const req = new UsernameValidReq(); req.setUsername(username); From c254b079b1d9562fe1c4f182c165962174be9a99 Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:10:49 +0000 Subject: [PATCH 34/46] Format backend --- .../versions/0187_add_signup_cancelled.py | 13 ++++++------- app/backend/src/couchers/servicers/auth.py | 9 ++++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py b/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py index 72bd2a097e..23b4610195 100644 --- a/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py +++ b/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py @@ -5,25 +5,24 @@ Create Date: 2026-08-24 00:55:18.840814 """ -from alembic import op -import sqlalchemy as sa -import geoalchemy2 +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision = '0187' -down_revision = '0186' +revision = "0187" +down_revision = "0186" branch_labels = None depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.add_column('signup_flows', sa.Column('signup_cancelled', sa.Boolean(), nullable=False)) + op.add_column("signup_flows", sa.Column("signup_cancelled", sa.Boolean(), nullable=False)) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('signup_flows', 'signup_cancelled') + op.drop_column("signup_flows", "signup_cancelled") # ### end Alembic commands ### diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py index 898d6f4bba..ed9952ab38 100644 --- a/app/backend/src/couchers/servicers/auth.py +++ b/app/backend/src/couchers/servicers/auth.py @@ -179,7 +179,10 @@ def _username_available(session: Session, username: str) -> bool: user_exists = session.execute(select(User).where(User.username == username)).scalar_one_or_none() is not None # check for started signup with that username signup_exists = ( - session.execute(select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled.is_not(True))).scalar_one_or_none() is not None + session.execute( + select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled.is_not(True)) + ).scalar_one_or_none() + is not None ) # return False if user exists, True otherwise return not user_exists and not signup_exists @@ -280,10 +283,10 @@ def SignupFlow( context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") if request.cancel_signup: flow.signup_cancelled = True - flow.email_token = None + flow.email_token = None flow.email_token_expiry = None session.flush() - + return auth_pb2.SignupFlowRes() if request.HasField("basic"): context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_basic_filled") From 00e80bd68ef6523a4bfbd66d95d3b81842b23e8f Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:12:30 +0000 Subject: [PATCH 35/46] Format frontend --- app/web/service/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 0983caa451..e118c7e533 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -144,7 +144,6 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { return res.toObject(); } - export async function signupFlowCancelSignup(flowToken: string) { const req = new SignupFlowReq(); req.setFlowToken(flowToken); From e3a0bf0a5575729f30813cdecb9044488ff41437 Mon Sep 17 00:00:00 2001 From: nc225 Date: Sun, 23 Aug 2026 23:57:40 -0400 Subject: [PATCH 36/46] update migration files --- .../versions/0187_add_signup_cancelled.py | 28 ----------------- ..._allow_starting_over_from_verification_.py | 31 ++++++++++++++++--- 2 files changed, 26 insertions(+), 33 deletions(-) delete mode 100644 app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py diff --git a/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py b/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py deleted file mode 100644 index 23b4610195..0000000000 --- a/app/backend/src/couchers/migrations/versions/0187_add_signup_cancelled.py +++ /dev/null @@ -1,28 +0,0 @@ -"""add signup cancelled - -Revision ID: 0187 -Revises: 0186 -Create Date: 2026-08-24 00:55:18.840814 - -""" - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = "0187" -down_revision = "0186" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column("signup_flows", sa.Column("signup_cancelled", sa.Boolean(), nullable=False)) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column("signup_flows", "signup_cancelled") - # ### end Alembic commands ### diff --git a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py index 3a0323bec7..72e83d679d 100644 --- a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py +++ b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py @@ -15,18 +15,39 @@ branch_labels = None depends_on = None - def upgrade() -> None: - op.add_column("signup_flows", sa.Column("signup_cancelled", sa.Boolean(), nullable=False)) - op.drop_constraint(op.f("uq_signup_flows_username"), "signup_flows", type_="unique") + op.add_column( + "signup_flows", + sa.Column( + "signup_cancelled", + sa.Boolean(), + nullable=False, + server_default=sa.false(), + ), + ) + + op.drop_constraint( + op.f("uq_signup_flows_username"), + "signup_flows", + type_="unique", + ) + op.create_index( "uq_signup_flows_username", "signup_flows", ["username"], unique=True, - postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), + postgresql_where=sa.text( + "username IS NOT NULL AND signup_cancelled = false" + ), ) + op.alter_column( + "signup_flows", + "signup_cancelled", + server_default=None, + ) + def downgrade() -> None: op.drop_index( @@ -37,4 +58,4 @@ def downgrade() -> None: op.create_unique_constraint( op.f("uq_signup_flows_username"), "signup_flows", ["username"], postgresql_nulls_not_distinct=False ) - op.drop_column("signup_flows", "signup_cancelled") + op.drop_column("signup_flows", "signup_cancelled") \ No newline at end of file From 5cf0baba32cf6ac7f7a2d3f2e3ea746bbc383acb Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 03:58:21 +0000 Subject: [PATCH 37/46] Format backend --- .../0187_allow_starting_over_from_verification_.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py index 72e83d679d..722062d110 100644 --- a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py +++ b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py @@ -15,6 +15,7 @@ branch_labels = None depends_on = None + def upgrade() -> None: op.add_column( "signup_flows", @@ -37,9 +38,7 @@ def upgrade() -> None: "signup_flows", ["username"], unique=True, - postgresql_where=sa.text( - "username IS NOT NULL AND signup_cancelled = false" - ), + postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), ) op.alter_column( @@ -47,7 +46,7 @@ def upgrade() -> None: "signup_cancelled", server_default=None, ) - + def downgrade() -> None: op.drop_index( @@ -58,4 +57,4 @@ def downgrade() -> None: op.create_unique_constraint( op.f("uq_signup_flows_username"), "signup_flows", ["username"], postgresql_nulls_not_distinct=False ) - op.drop_column("signup_flows", "signup_cancelled") \ No newline at end of file + op.drop_column("signup_flows", "signup_cancelled") From 3950d0d0c8debfc35edf951eb337ac83811f2371 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 24 Aug 2026 00:46:59 -0400 Subject: [PATCH 38/46] update upgrade and downgrade to add signup_cancelled --- ...87_allow_starting_over_from_verification_.py | 5 +++++ app/web/.env.development | 17 ++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py index 722062d110..c5e9882685 100644 --- a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py +++ b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py @@ -57,4 +57,9 @@ def downgrade() -> None: op.create_unique_constraint( op.f("uq_signup_flows_username"), "signup_flows", ["username"], postgresql_nulls_not_distinct=False ) +<<<<<<< HEAD op.drop_column("signup_flows", "signup_cancelled") +======= + op.execute("DELETE FROM signup_flows WHERE signup_cancelled") + op.drop_column("signup_flows", "signup_cancelled") +>>>>>>> fa81c79b8 (update upgrade and downgrade to add signup_cancelled) diff --git a/app/web/.env.development b/app/web/.env.development index 218e7b050e..c9a6905dc5 100644 --- a/app/web/.env.development +++ b/app/web/.env.development @@ -1,13 +1,12 @@ -NEXT_PUBLIC_COUCHERS_ENV=dev -NEXT_PUBLIC_BASE_URL="/" -NEXT_PUBLIC_API_BASE_URL="http://localhost:8888" -NEXT_PUBLIC_MEDIA_BASE_URL="http://localhost:5001" -NEXT_PUBLIC_CONSOLE_BASE_URL="http://localhost:10027" -NEXT_PUBLIC_IS_POST_BETA_ENABLED=true +NEXT_PUBLIC_COUCHERS_ENV=preview +NEXT_PUBLIC_BASE_URL="https://next.couchershq.org" +NEXT_PUBLIC_API_BASE_URL="https://next.couchershq.org/api" +NEXT_PUBLIC_MEDIA_BASE_URL="https://dev-user-media.couchershq.org" +NEXT_PUBLIC_CONSOLE_BASE_URL="https://next-console.couchershq.org" NEXT_PUBLIC_NOMINATIM_URL="https://nominatim.openstreetmap.org/" -NEXT_PUBLIC_IS_VERIFICATION_ENABLED=true -NEXT_PUBLIC_IS_COMMUNITIES_PART2_ENABLED=true NEXT_PUBLIC_STRIPE_KEY="pk_test_51KEzByIfR5z29g5khFE5samD8XKOGLcCrM1lhCkfOomGPUFAEYOw8uAqI2Nkv33wYdPM2FgTQNTC07IiNfHY1kLJ00Jqm8Ppai" -NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/localdev.json" +NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/next.json" NEXT_PUBLIC_GROWTHBOOK_API_HOST="https://gbapi.couchershq.org" NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY="sdk-f8lwseEODN02p" +# When enabled, flags resolve from app/web/feature-flags.dev.json instead of GrowthBook (dev/testing) +NEXT_PUBLIC_FEATURE_FLAGS_OVERRIDE="1" From 68ae9d1ac93bcc607077f4c46f4ddf0dae3a8f49 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 24 Aug 2026 00:47:49 -0400 Subject: [PATCH 39/46] add signup_cancelled to auth flow for correct error handling --- app/backend/src/couchers/servicers/auth.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py index ed9952ab38..fa933d21fa 100644 --- a/app/backend/src/couchers/servicers/auth.py +++ b/app/backend/src/couchers/servicers/auth.py @@ -180,7 +180,7 @@ def _username_available(session: Session, username: str) -> bool: # check for started signup with that username signup_exists = ( session.execute( - select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled.is_not(True)) + select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled == False) ).scalar_one_or_none() is not None ) @@ -203,6 +203,7 @@ def SignupFlow( flow = session.execute( select(SignupFlow) .where(SignupFlow.email_verified == False) + .where(SignupFlow.signup_cancelled == False) .where(SignupFlow.email_token == request.email_token) .where(SignupFlow.token_is_valid) ).scalar_one_or_none() @@ -217,7 +218,7 @@ def SignupFlow( else: # just try to find the flow by flow token, no verification is done flow = session.execute( - select(SignupFlow).where(SignupFlow.flow_token == request.email_token) + select(SignupFlow).where(SignupFlow.flow_token == request.email_token).where(SignupFlow.signup_cancelled == False) ).scalar_one_or_none() if not flow: context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") @@ -237,7 +238,7 @@ def SignupFlow( ) context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_email_taken") existing_flow = session.execute( - select(SignupFlow).where(SignupFlow.email == request.basic.email) + select(SignupFlow).where(SignupFlow.email == request.basic.email).where(SignupFlow.signup_cancelled == False) ).scalar_one_or_none() if existing_flow: send_signup_email(context, session, existing_flow) @@ -277,7 +278,7 @@ def SignupFlow( else: # not fresh signup flow = session.execute( - select(SignupFlow).where(SignupFlow.flow_token == request.flow_token) + select(SignupFlow).where(SignupFlow.flow_token == request.flow_token).where(SignupFlow.signup_cancelled == False) ).scalar_one_or_none() if not flow: context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") @@ -286,7 +287,6 @@ def SignupFlow( flow.email_token = None flow.email_token_expiry = None session.flush() - return auth_pb2.SignupFlowRes() if request.HasField("basic"): context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_basic_filled") @@ -520,7 +520,7 @@ def Authenticate(self, request: auth_pb2.AuthReq, context: CouchersContext, sess else: # user not found # check if this is an email and they tried to sign up but didn't complete signup_flow = session.execute( - select(SignupFlow).where(username_or_email(request.user, table=SignupFlow)) + select(SignupFlow).where(username_or_email(request.user, table=SignupFlow)).where(SignupFlow.signup_cancelled == False) ).scalar_one_or_none() if signup_flow: send_signup_email(context, session, signup_flow) From f747dcf6a6970a636babd519a20aed55b38bda56 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 24 Aug 2026 00:48:49 -0400 Subject: [PATCH 40/46] fall back to old email message if email is not stored --- .../signup/ResendVerificationEmailForm.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index 1bf8d77efa..bb39f49153 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -21,7 +21,7 @@ export default function ResendVerificationEmailForm() { }); const mutationRestart = useMutation({ mutationFn: async () => { - const state = await service.auth.signupFlowCancelSignup(authState.flowState!.flowToken); + await service.auth.signupFlowCancelSignup(authState.flowState!.flowToken); authActions.restartSignup(); }, }); @@ -30,12 +30,16 @@ export default function ResendVerificationEmailForm() { {mutationResend.error && {mutationResend.error.message || ""}} {mutationRestart.error && {mutationRestart.error.message || ""}} - + {authState.signupEmail ? ( + + ) : ( + + )} {!resent ? ( From 5d471d078b24033fe5d22ee39dfde799300340aa Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 24 Aug 2026 00:49:17 -0400 Subject: [PATCH 41/46] fall back to old email message if email is not stored --- app/web/features/auth/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index 0f02ea8abb..ceb7a4816b 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -178,6 +178,7 @@ "sign_up_resend_verification_done": "Done! We've sent you another email.", "sign_up_restart_signup": "Mistyped your email address? Click here to start over.", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address: {{providedEmailAddress}}. Please click the link to activate your account.", + "sign_up_completed_prompt_noemail": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", "unhandled_sign_up_state": "Error: unhandled signup flow state.", "login_prompt": "Click here to log in", From c605f7f501bd99284c8d61aac4d3f7efb1fa2ce0 Mon Sep 17 00:00:00 2001 From: nc225 Date: Mon, 24 Aug 2026 00:50:18 -0400 Subject: [PATCH 42/46] remove email_token invalidation here since this happens in auth.py instead --- app/web/service/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index e118c7e533..1f6a441d32 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -147,7 +147,6 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { export async function signupFlowCancelSignup(flowToken: string) { const req = new SignupFlowReq(); req.setFlowToken(flowToken); - req.setEmailToken(null); //invalidate any existing email tokens req.setCancelSignup(true); const res = await client.auth.signupFlow(req); return res.toObject(); From f5fc91722e7e00a716cb7e9fbf9bb09d49614b73 Mon Sep 17 00:00:00 2001 From: nc225 Date: Tue, 25 Aug 2026 01:09:41 -0400 Subject: [PATCH 43/46] rework to prevent blocking usernames --- ..._allow_starting_over_from_verification_.py | 65 ------------- app/backend/src/couchers/models/rest.py | 12 +-- app/backend/src/couchers/servicers/auth.py | 93 ++++++++++++++++--- app/proto/auth.proto | 10 +- app/web/.env.development | 17 ++-- app/web/features/auth/locales/en.json | 9 +- .../auth/signup/ChangeSignupEmail.tsx | 93 +++++++++++++++++++ .../signup/ResendVerificationEmailForm.tsx | 62 +++++-------- app/web/features/auth/useAuthStore.ts | 5 - app/web/service/auth.ts | 19 +++- 10 files changed, 237 insertions(+), 148 deletions(-) delete mode 100644 app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py create mode 100644 app/web/features/auth/signup/ChangeSignupEmail.tsx diff --git a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py b/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py deleted file mode 100644 index c5e9882685..0000000000 --- a/app/backend/src/couchers/migrations/versions/0187_allow_starting_over_from_verification_.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Allow starting over from verification page - -Revision ID: 0187 -Revises: 0186 -Create Date: 2026-08-24 03:01:54.582432 - -""" - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision = "0187" -down_revision = "0186" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.add_column( - "signup_flows", - sa.Column( - "signup_cancelled", - sa.Boolean(), - nullable=False, - server_default=sa.false(), - ), - ) - - op.drop_constraint( - op.f("uq_signup_flows_username"), - "signup_flows", - type_="unique", - ) - - op.create_index( - "uq_signup_flows_username", - "signup_flows", - ["username"], - unique=True, - postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), - ) - - op.alter_column( - "signup_flows", - "signup_cancelled", - server_default=None, - ) - - -def downgrade() -> None: - op.drop_index( - "uq_signup_flows_username", - table_name="signup_flows", - postgresql_where=sa.text("username IS NOT NULL AND signup_cancelled = false"), - ) - op.create_unique_constraint( - op.f("uq_signup_flows_username"), "signup_flows", ["username"], postgresql_nulls_not_distinct=False - ) -<<<<<<< HEAD - op.drop_column("signup_flows", "signup_cancelled") -======= - op.execute("DELETE FROM signup_flows WHERE signup_cancelled") - op.drop_column("signup_flows", "signup_cancelled") ->>>>>>> fa81c79b8 (update upgrade and downgrade to add signup_cancelled) diff --git a/app/backend/src/couchers/models/rest.py b/app/backend/src/couchers/models/rest.py index 06f840124e..e8009c5350 100644 --- a/app/backend/src/couchers/models/rest.py +++ b/app/backend/src/couchers/models/rest.py @@ -186,7 +186,6 @@ class SignupFlow(Base, kw_only=True): email_sent: Mapped[bool] = mapped_column(Boolean, default=False) email_token: Mapped[str | None] = mapped_column(String, unique=True, default=None) email_token_expiry: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) - signup_cancelled: Mapped[bool] = mapped_column(Boolean, default=False) ## Basic name: Mapped[str] = mapped_column(String) @@ -196,7 +195,7 @@ class SignupFlow(Base, kw_only=True): ## Account # TODO: unique across both tables - username: Mapped[str | None] = mapped_column(String, default=None) + username: Mapped[str | None] = mapped_column(String, unique=True, default=None) hashed_password: Mapped[bytes | None] = mapped_column(Binary, default=None) birthdate: Mapped[date | None] = mapped_column(Date, default=None) # in the timezone of birthplace gender: Mapped[str | None] = mapped_column(String, default=None) @@ -259,15 +258,6 @@ def is_completed(self) -> Any: & self.filled_motivations ) - __table_args__ = ( - Index( - "uq_signup_flows_username", - username, - unique=True, - postgresql_where=((username != None) & (signup_cancelled == False)), - ), - ) - class AccountDeletionToken(Base, kw_only=True): __tablename__ = "account_deletion_tokens" diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py index fa933d21fa..178d72d22d 100644 --- a/app/backend/src/couchers/servicers/auth.py +++ b/app/backend/src/couchers/servicers/auth.py @@ -180,7 +180,7 @@ def _username_available(session: Session, username: str) -> bool: # check for started signup with that username signup_exists = ( session.execute( - select(SignupFlow).where(SignupFlow.username == username).where(SignupFlow.signup_cancelled == False) + select(SignupFlow).where(SignupFlow.username == username) ).scalar_one_or_none() is not None ) @@ -203,7 +203,6 @@ def SignupFlow( flow = session.execute( select(SignupFlow) .where(SignupFlow.email_verified == False) - .where(SignupFlow.signup_cancelled == False) .where(SignupFlow.email_token == request.email_token) .where(SignupFlow.token_is_valid) ).scalar_one_or_none() @@ -218,7 +217,7 @@ def SignupFlow( else: # just try to find the flow by flow token, no verification is done flow = session.execute( - select(SignupFlow).where(SignupFlow.flow_token == request.email_token).where(SignupFlow.signup_cancelled == False) + select(SignupFlow).where(SignupFlow.flow_token == request.email_token) ).scalar_one_or_none() if not flow: context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") @@ -238,7 +237,7 @@ def SignupFlow( ) context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_email_taken") existing_flow = session.execute( - select(SignupFlow).where(SignupFlow.email == request.basic.email).where(SignupFlow.signup_cancelled == False) + select(SignupFlow).where(SignupFlow.email == request.basic.email) ).scalar_one_or_none() if existing_flow: send_signup_email(context, session, existing_flow) @@ -278,16 +277,10 @@ def SignupFlow( else: # not fresh signup flow = session.execute( - select(SignupFlow).where(SignupFlow.flow_token == request.flow_token).where(SignupFlow.signup_cancelled == False) + select(SignupFlow).where(SignupFlow.flow_token == request.flow_token) ).scalar_one_or_none() if not flow: context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "invalid_token") - if request.cancel_signup: - flow.signup_cancelled = True - flow.email_token = None - flow.email_token_expiry = None - session.flush() - return auth_pb2.SignupFlowRes() if request.HasField("basic"): context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "signup_flow_basic_filled") @@ -363,7 +356,6 @@ def SignupFlow( signup_guidelines_accepted_counter.inc() flow.accepted_community_guidelines = GUIDELINES_VERSION session.flush() - # send verification email if needed if not flow.email_sent or request.resend_verification_email: send_signup_email(context, session, flow) @@ -520,7 +512,7 @@ def Authenticate(self, request: auth_pb2.AuthReq, context: CouchersContext, sess else: # user not found # check if this is an email and they tried to sign up but didn't complete signup_flow = session.execute( - select(SignupFlow).where(username_or_email(request.user, table=SignupFlow)).where(SignupFlow.signup_cancelled == False) + select(SignupFlow).where(username_or_email(request.user, table=SignupFlow)) ).scalar_one_or_none() if signup_flow: send_signup_email(context, session, signup_flow) @@ -823,3 +815,78 @@ def GetInviteCodeInfo( avatar_url=avatar_upload.thumbnail_url if avatar_upload else None, url=urls.invite_code_link(code=request.code), ) + def SignupFlowChangeEmail( + self, + request: auth_pb2.ChangeSignupEmailReq, + context: CouchersContext, + session: Session, + ) -> auth_pb2.SignupFlowRes: + flow = session.execute( + select(SignupFlow).where( + SignupFlow.flow_token == request.flow_token + ) + ).scalar_one_or_none() + + if not flow: + context.abort_with_error_code( + grpc.StatusCode.NOT_FOUND, + "invalid_token", + ) + + new_email = request.new_email.strip().lower() + + if not is_valid_email(new_email): + context.abort_with_error_code( + grpc.StatusCode.INVALID_ARGUMENT, + "invalid_email", + ) + + existing_user = session.execute( + select(User).where(User.email == new_email) + ).scalar_one_or_none() + + if existing_user: + if not existing_user.is_visible: + context.abort_with_error_code( + grpc.StatusCode.FAILED_PRECONDITION, + "signup_email_cannot_be_used", + ) + + context.abort_with_error_code( + grpc.StatusCode.FAILED_PRECONDITION, + "signup_flow_email_taken", + ) + existing_signup = session.execute( + select(SignupFlow).where( + SignupFlow.email == new_email, + SignupFlow.id != flow.id, + ) + ).scalar_one_or_none() + + if existing_signup: + context.abort_with_error_code( + grpc.StatusCode.FAILED_PRECONDITION, + "signup_flow_email_taken", + ) + + flow.email = new_email + + # Invalidate the old verification token. + flow.email_token = None + flow.email_token_expiry = None + flow.email_sent = False + + send_signup_email(context, session, flow) + + session.flush() + + return auth_pb2.SignupFlowRes( + flow_token=flow.flow_token, + need_account=not flow.account_is_filled, + need_feedback=False, + need_verify_email=True, + need_accept_community_guidelines=( + flow.accepted_community_guidelines < GUIDELINES_VERSION + ), + need_motivations=not flow.filled_motivations, + ) diff --git a/app/proto/auth.proto b/app/proto/auth.proto index c36fc831d6..a84b763e59 100644 --- a/app/proto/auth.proto +++ b/app/proto/auth.proto @@ -49,6 +49,10 @@ service Auth { // * Once the flow completes, the user is logged in and the signup flow is destroyed } + rpc SignupFlowChangeEmail(ChangeSignupEmailReq) returns (SignupFlowRes) { + // Change the email address of an incomplete signup flow. + } + rpc UsernameValid(UsernameValidReq) returns (UsernameValidRes) { // Check whether the username is valid and available } @@ -123,7 +127,6 @@ message SignupFlowReq { google.protobuf.BoolValue accept_community_guidelines = 6; bool resend_verification_email = 7; - bool cancel_signup = 9; } message SignupFlowRes { @@ -285,3 +288,8 @@ message GetInviteCodeInfoRes { string avatar_url = 3; string url = 4; } + +message ChangeSignupEmailReq { + string flow_token = 1; + string new_email = 2; +} diff --git a/app/web/.env.development b/app/web/.env.development index c9a6905dc5..218e7b050e 100644 --- a/app/web/.env.development +++ b/app/web/.env.development @@ -1,12 +1,13 @@ -NEXT_PUBLIC_COUCHERS_ENV=preview -NEXT_PUBLIC_BASE_URL="https://next.couchershq.org" -NEXT_PUBLIC_API_BASE_URL="https://next.couchershq.org/api" -NEXT_PUBLIC_MEDIA_BASE_URL="https://dev-user-media.couchershq.org" -NEXT_PUBLIC_CONSOLE_BASE_URL="https://next-console.couchershq.org" +NEXT_PUBLIC_COUCHERS_ENV=dev +NEXT_PUBLIC_BASE_URL="/" +NEXT_PUBLIC_API_BASE_URL="http://localhost:8888" +NEXT_PUBLIC_MEDIA_BASE_URL="http://localhost:5001" +NEXT_PUBLIC_CONSOLE_BASE_URL="http://localhost:10027" +NEXT_PUBLIC_IS_POST_BETA_ENABLED=true NEXT_PUBLIC_NOMINATIM_URL="https://nominatim.openstreetmap.org/" +NEXT_PUBLIC_IS_VERIFICATION_ENABLED=true +NEXT_PUBLIC_IS_COMMUNITIES_PART2_ENABLED=true NEXT_PUBLIC_STRIPE_KEY="pk_test_51KEzByIfR5z29g5khFE5samD8XKOGLcCrM1lhCkfOomGPUFAEYOw8uAqI2Nkv33wYdPM2FgTQNTC07IiNfHY1kLJ00Jqm8Ppai" -NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/next.json" +NEXT_PUBLIC_GLOBAL_MESSAGE_URL="https://gm.couchershq.org/localdev.json" NEXT_PUBLIC_GROWTHBOOK_API_HOST="https://gbapi.couchershq.org" NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY="sdk-f8lwseEODN02p" -# When enabled, flags resolve from app/web/feature-flags.dev.json instead of GrowthBook (dev/testing) -NEXT_PUBLIC_FEATURE_FLAGS_OVERRIDE="1" diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index ceb7a4816b..45fa83486c 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -8,6 +8,13 @@ "hide_current_password": "Hide current password", "show_current_password": "Show current password" }, + "change_signup_email_form": { + "title": "Change signup email", + "current_email_message": "Your email address is currently {{email}}.", + "success_message": "Your email change has been received. Check your new email to confirm your email.", + "new_email": "Changed email address", + "signup_change_email": "Change your email address" + }, "do_not_email": { "title": "Do not email me", "status": { @@ -176,7 +183,7 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", - "sign_up_restart_signup": "Mistyped your email address? Click here to start over.", + "sign_up_change_email": "Mistyped your email address? Enter your correct email address here.", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address: {{providedEmailAddress}}. Please click the link to activate your account.", "sign_up_completed_prompt_noemail": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", diff --git a/app/web/features/auth/signup/ChangeSignupEmail.tsx b/app/web/features/auth/signup/ChangeSignupEmail.tsx new file mode 100644 index 0000000000..202a49a286 --- /dev/null +++ b/app/web/features/auth/signup/ChangeSignupEmail.tsx @@ -0,0 +1,93 @@ +import { IconButton, InputAdornment, styled, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { useMutation } from "@tanstack/react-query"; +import Alert from "components/Alert"; +import Button from "components/Button"; +import TextField from "components/TextField"; +import { Empty } from "google-protobuf/google/protobuf/empty_pb"; +import { RpcError } from "grpc-web"; +import { useAuthContext } from "features/auth/AuthProvider"; +import { Trans, useTranslation } from "i18n"; +import { AUTH, GLOBAL } from "i18n/namespaces"; +import { useForm } from "react-hook-form"; +import { service } from "service"; +import { useState } from "react"; +import { lowercaseAndTrimField } from "utils/validation"; + +const StyledForm = styled("form")(({ theme }) => ({ + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + display: "flex", + flexDirection: "column", + gap: theme.spacing(1), + alignItems: "flex-start", + width: "100%", + [theme.breakpoints.up("md")]: { + width: "15.5rem", + }, +})); + +interface ChangeSignupEmailFormData { + newSignupEmail: string; +} + +interface ChangeSignupEmailProps { + email: string; + className?: string; +} + +export default function ChangeSignupEmail({ className }: ChangeSignupEmailProps) { + const { t } = useTranslation([AUTH, GLOBAL]); + const { authActions, authState } = useAuthContext(); + const theme = useTheme(); + const isMdOrWider = useMediaQuery(theme.breakpoints.up("md")); + + const [changedEmail, setChangedEmail] = useState(false); + + const { handleSubmit, register, reset: resetForm } = useForm(); + const onSubmit = handleSubmit(({ newSignupEmail }) => { + const sanitizedEmail = lowercaseAndTrimField(newSignupEmail); + setChangedEmail(true); + changeSignupEmail({newSignupEmail}); + }); + + const { + error: changeSignupEmailError, + isPending: isChangeSignupEmailLoading, + isSuccess: isChangeSignupEmailSuccess, + mutate: changeSignupEmail, + } = useMutation({ + mutationFn: async ({newSignupEmail}) => { + await service.auth.signupFlowChangeEmail(authState.flowState!.flowToken, lowercaseAndTrimField(newSignupEmail)); + // authActions.restartSignup(); + }, onSuccess: (_, {newSignupEmail}) => { + authActions.assignSignupEmail(newSignupEmail); + resetForm(); + }, + }); + + + + return ( +
+ { + !changedEmail ? ( t("Mistype your email address? Change it here:") ) : ("") + } + <> + {changeSignupEmailError && {changeSignupEmailError.message}} + {isChangeSignupEmailSuccess && {t("auth:change_signup_email_form.success_message")}} + { !changedEmail ? ( + + + ) : (<>)} + +
+ ); +} diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index bb39f49153..da72541be6 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -5,43 +5,41 @@ import StyledLink from "components/StyledLink"; import { useAuthContext } from "features/auth/AuthProvider"; import { Trans, useTranslation } from "i18n"; import { AUTH, GLOBAL } from "i18n/namespaces"; + +import ChangeSignupEmail from "features/auth/signup/ChangeSignupEmail"; import { useState } from "react"; import { service } from "service"; export default function ResendVerificationEmailForm() { const { t } = useTranslation([AUTH, GLOBAL]); const { authActions, authState } = useAuthContext(); + const [resent, setResent] = useState(false); - const mutationResend = useMutation({ + + const mutation = useMutation({ mutationFn: async () => { const state = await service.auth.signupFlowResendVerificationEmail(authState.flowState!.flowToken); authActions.updateSignupState(state); setResent(true); }, }); - const mutationRestart = useMutation({ - mutationFn: async () => { - await service.auth.signupFlowCancelSignup(authState.flowState!.flowToken); - authActions.restartSignup(); - }, - }); + return ( <> - {mutationResend.error && {mutationResend.error.message || ""}} - {mutationRestart.error && {mutationRestart.error.message || ""}} - - {authState.signupEmail ? ( - - ) : ( - - )} - + {mutation.error && {mutation.error.message || ""}} + + {authState.signupEmail ? ( + + ) : ( + + )} + {!resent ? ( { e.preventDefault(); - mutationResend.mutateAsync(); + mutation.mutateAsync(); }} /> ), @@ -61,22 +59,8 @@ export default function ResendVerificationEmailForm() { <>{t("auth:sign_up_resend_verification_done")} )} - - { - e.preventDefault(); - mutationRestart.mutateAsync(); - }} - /> - ), - }} - /> - + + ); -} +} \ No newline at end of file diff --git a/app/web/features/auth/useAuthStore.ts b/app/web/features/auth/useAuthStore.ts index b801f54451..d66d94d134 100644 --- a/app/web/features/auth/useAuthStore.ts +++ b/app/web/features/auth/useAuthStore.ts @@ -148,11 +148,6 @@ export default function useAuthStore() { assignSignupEmail(email: string) { setSignupEmail(email); }, - restartSignup() { - setFlowState(null); - setError(null); - setSignupEmail(null); - }, async firstLogin(res: AuthRes.AsObject) { setError(null); setUserId(res.userId); diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 1f6a441d32..019b6f3b74 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -12,6 +12,7 @@ import { SignupMotivations, UnsubscribeReq, UsernameValidReq, + ChangeSignupEmailReq } from "proto/auth_pb"; import { Temporal } from "temporal-polyfill"; @@ -144,12 +145,20 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { return res.toObject(); } -export async function signupFlowCancelSignup(flowToken: string) { - const req = new SignupFlowReq(); +// export async function signupFlowCancelSignup(flowToken: string) { +// const req = new SignupFlowReq(); +// req.setFlowToken(flowToken); +// req.setCancelSignup(true); +// const res = await client.auth.signupFlow(req); +// return res.toObject(); +// } + + +export function signupFlowChangeEmail(flowToken: string, newEmail: string) { + const req = new ChangeSignupEmailReq(); + req.setNewEmail(newEmail); req.setFlowToken(flowToken); - req.setCancelSignup(true); - const res = await client.auth.signupFlow(req); - return res.toObject(); + return client.auth.signupFlowChangeEmail(req); } export async function validateUsername(username: string) { From 528576c75dc010ef397ba207782110f17219e0cd Mon Sep 17 00:00:00 2001 From: nc225 Date: Tue, 25 Aug 2026 01:17:43 -0400 Subject: [PATCH 44/46] rework to prevent blocking usernames --- .../auth/signup/ChangeSignupEmail.tsx | 63 ++++++++++--------- .../signup/ResendVerificationEmailForm.tsx | 10 ++- app/web/service/auth.ts | 3 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/app/web/features/auth/signup/ChangeSignupEmail.tsx b/app/web/features/auth/signup/ChangeSignupEmail.tsx index 202a49a286..2eaf5afd7f 100644 --- a/app/web/features/auth/signup/ChangeSignupEmail.tsx +++ b/app/web/features/auth/signup/ChangeSignupEmail.tsx @@ -1,16 +1,16 @@ -import { IconButton, InputAdornment, styled, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { styled, Typography, useMediaQuery, useTheme } from "@mui/material"; import { useMutation } from "@tanstack/react-query"; import Alert from "components/Alert"; import Button from "components/Button"; import TextField from "components/TextField"; +import { useAuthContext } from "features/auth/AuthProvider"; import { Empty } from "google-protobuf/google/protobuf/empty_pb"; import { RpcError } from "grpc-web"; -import { useAuthContext } from "features/auth/AuthProvider"; -import { Trans, useTranslation } from "i18n"; +import { useTranslation } from "i18n"; import { AUTH, GLOBAL } from "i18n/namespaces"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { service } from "service"; -import { useState } from "react"; import { lowercaseAndTrimField } from "utils/validation"; const StyledForm = styled("form")(({ theme }) => ({ @@ -40,14 +40,14 @@ export default function ChangeSignupEmail({ className }: ChangeSignupEmailProps) const { authActions, authState } = useAuthContext(); const theme = useTheme(); const isMdOrWider = useMediaQuery(theme.breakpoints.up("md")); - + const [changedEmail, setChangedEmail] = useState(false); const { handleSubmit, register, reset: resetForm } = useForm(); const onSubmit = handleSubmit(({ newSignupEmail }) => { const sanitizedEmail = lowercaseAndTrimField(newSignupEmail); setChangedEmail(true); - changeSignupEmail({newSignupEmail}); + changeSignupEmail({ newSignupEmail: sanitizedEmail }); }); const { @@ -56,37 +56,42 @@ export default function ChangeSignupEmail({ className }: ChangeSignupEmailProps) isSuccess: isChangeSignupEmailSuccess, mutate: changeSignupEmail, } = useMutation({ - mutationFn: async ({newSignupEmail}) => { + mutationFn: async ({ newSignupEmail }) => { await service.auth.signupFlowChangeEmail(authState.flowState!.flowToken, lowercaseAndTrimField(newSignupEmail)); - // authActions.restartSignup(); - }, onSuccess: (_, {newSignupEmail}) => { - authActions.assignSignupEmail(newSignupEmail); + }, + onSuccess: (_, { newSignupEmail }) => { + const sanitizedEmail = lowercaseAndTrimField(newSignupEmail); + authActions.assignSignupEmail(sanitizedEmail); resetForm(); }, }); - - return (
- { - !changedEmail ? ( t("Mistype your email address? Change it here:") ) : ("") - } - <> + + {!changedEmail ? t("Mistype your email address? Change it here:") : ""} + + <> {changeSignupEmailError && {changeSignupEmailError.message}} - {isChangeSignupEmailSuccess && {t("auth:change_signup_email_form.success_message")}} - { !changedEmail ? ( - - - ) : (<>)} + {isChangeSignupEmailSuccess && ( + {t("auth:change_signup_email_form.success_message")} + )} + {!changedEmail ? ( + + + + + ) : ( + <> + )}
); diff --git a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx index da72541be6..69f8bd71e3 100644 --- a/app/web/features/auth/signup/ResendVerificationEmailForm.tsx +++ b/app/web/features/auth/signup/ResendVerificationEmailForm.tsx @@ -3,10 +3,9 @@ import { useMutation } from "@tanstack/react-query"; import Alert from "components/Alert"; import StyledLink from "components/StyledLink"; import { useAuthContext } from "features/auth/AuthProvider"; +import ChangeSignupEmail from "features/auth/signup/ChangeSignupEmail"; import { Trans, useTranslation } from "i18n"; import { AUTH, GLOBAL } from "i18n/namespaces"; - -import ChangeSignupEmail from "features/auth/signup/ChangeSignupEmail"; import { useState } from "react"; import { service } from "service"; @@ -28,7 +27,7 @@ export default function ResendVerificationEmailForm() { <> {mutation.error && {mutation.error.message || ""}} - + {authState.signupEmail ? ( {t("auth:sign_up_resend_verification_done")} )} - - + ); -} \ No newline at end of file +} diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index 019b6f3b74..edbdbb55da 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -2,6 +2,7 @@ import { BoolValue } from "google-protobuf/google/protobuf/wrappers_pb"; import { HostingStatus } from "proto/api_pb"; import { AntiBotReq, + ChangeSignupEmailReq, ConfirmDeleteAccountReq, ContributorForm as ContributorFormPb, GetInviteCodeInfoReq, @@ -12,7 +13,6 @@ import { SignupMotivations, UnsubscribeReq, UsernameValidReq, - ChangeSignupEmailReq } from "proto/auth_pb"; import { Temporal } from "temporal-polyfill"; @@ -153,7 +153,6 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { // return res.toObject(); // } - export function signupFlowChangeEmail(flowToken: string, newEmail: string) { const req = new ChangeSignupEmailReq(); req.setNewEmail(newEmail); From cbd9a5c0149aff9227ef7617003ecc7424ded955 Mon Sep 17 00:00:00 2001 From: nc225 Date: Tue, 25 Aug 2026 01:26:44 -0400 Subject: [PATCH 45/46] draft new approach to email change during signup --- app/web/features/auth/locales/en.json | 2 +- app/web/service/auth.ts | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/web/features/auth/locales/en.json b/app/web/features/auth/locales/en.json index 45fa83486c..5957b93b6b 100644 --- a/app/web/features/auth/locales/en.json +++ b/app/web/features/auth/locales/en.json @@ -183,7 +183,7 @@ "sign_up_need_verification_title": "One last thing: confirm your email.", "sign_up_resend_verification_email_help": "Didn't receive the email? Click here to resend the verification link.", "sign_up_resend_verification_done": "Done! We've sent you another email.", - "sign_up_change_email": "Mistyped your email address? Enter your correct email address here.", + "sign_up_change_email": "Mistyped your email address? Enter your correct email address here:", "sign_up_completed_prompt": "We have sent an email with a verification link to your email address: {{providedEmailAddress}}. Please click the link to activate your account.", "sign_up_completed_prompt_noemail": "We have sent an email with a verification link to your email address. Please click the link to activate your account.", "sign_up_confirmed_prompt": "You're all done! If you are not redirected, try logging in.", diff --git a/app/web/service/auth.ts b/app/web/service/auth.ts index edbdbb55da..fc6ce9d423 100644 --- a/app/web/service/auth.ts +++ b/app/web/service/auth.ts @@ -145,14 +145,6 @@ export async function signupFlowResendVerificationEmail(flowToken: string) { return res.toObject(); } -// export async function signupFlowCancelSignup(flowToken: string) { -// const req = new SignupFlowReq(); -// req.setFlowToken(flowToken); -// req.setCancelSignup(true); -// const res = await client.auth.signupFlow(req); -// return res.toObject(); -// } - export function signupFlowChangeEmail(flowToken: string, newEmail: string) { const req = new ChangeSignupEmailReq(); req.setNewEmail(newEmail); From f7b98a163158147bc82110ac468c3cd6437a6a3f Mon Sep 17 00:00:00 2001 From: "couchersbot[bot]" <179870305+couchersbot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:27:42 +0000 Subject: [PATCH 46/46] Format backend --- app/backend/src/couchers/servicers/auth.py | 24 ++++++++-------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/app/backend/src/couchers/servicers/auth.py b/app/backend/src/couchers/servicers/auth.py index 178d72d22d..bdc9ca5564 100644 --- a/app/backend/src/couchers/servicers/auth.py +++ b/app/backend/src/couchers/servicers/auth.py @@ -179,10 +179,7 @@ def _username_available(session: Session, username: str) -> bool: user_exists = session.execute(select(User).where(User.username == username)).scalar_one_or_none() is not None # check for started signup with that username signup_exists = ( - session.execute( - select(SignupFlow).where(SignupFlow.username == username) - ).scalar_one_or_none() - is not None + session.execute(select(SignupFlow).where(SignupFlow.username == username)).scalar_one_or_none() is not None ) # return False if user exists, True otherwise return not user_exists and not signup_exists @@ -815,6 +812,7 @@ def GetInviteCodeInfo( avatar_url=avatar_upload.thumbnail_url if avatar_upload else None, url=urls.invite_code_link(code=request.code), ) + def SignupFlowChangeEmail( self, request: auth_pb2.ChangeSignupEmailReq, @@ -822,9 +820,7 @@ def SignupFlowChangeEmail( session: Session, ) -> auth_pb2.SignupFlowRes: flow = session.execute( - select(SignupFlow).where( - SignupFlow.flow_token == request.flow_token - ) + select(SignupFlow).where(SignupFlow.flow_token == request.flow_token) ).scalar_one_or_none() if not flow: @@ -834,17 +830,15 @@ def SignupFlowChangeEmail( ) new_email = request.new_email.strip().lower() - + if not is_valid_email(new_email): context.abort_with_error_code( grpc.StatusCode.INVALID_ARGUMENT, "invalid_email", ) - existing_user = session.execute( - select(User).where(User.email == new_email) - ).scalar_one_or_none() - + existing_user = session.execute(select(User).where(User.email == new_email)).scalar_one_or_none() + if existing_user: if not existing_user.is_visible: context.abort_with_error_code( @@ -870,7 +864,7 @@ def SignupFlowChangeEmail( ) flow.email = new_email - + # Invalidate the old verification token. flow.email_token = None flow.email_token_expiry = None @@ -885,8 +879,6 @@ def SignupFlowChangeEmail( need_account=not flow.account_is_filled, need_feedback=False, need_verify_email=True, - need_accept_community_guidelines=( - flow.accepted_community_guidelines < GUIDELINES_VERSION - ), + need_accept_community_guidelines=(flow.accepted_community_guidelines < GUIDELINES_VERSION), need_motivations=not flow.filled_motivations, )