From b3b77846529dfc47982be5032faa4b2007796d42 Mon Sep 17 00:00:00 2001 From: AliameenXBT Date: Mon, 8 Jun 2026 18:47:59 +0100 Subject: [PATCH] feat(web): enable Continue with Google auth buttons --- apps/web/src/app/(auth)/login/page.tsx | 64 +++++++++++++++++------ apps/web/src/app/(auth)/register/page.tsx | 56 ++++++++++++++++++-- apps/web/src/lib/auth.ts | 54 +++++++++++++++++++ 3 files changed, 152 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index df5cf250..349d18a4 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -11,6 +11,11 @@ import { Input } from "@/components/ui/Input"; import { Button } from "@/components/ui/Button"; import { PageLoadingState } from "@/components/ui/brand-loader"; import { api, type ApiError } from "@/lib/api"; +import { + buildGoogleOAuthStartUrl, + getGoogleAuthErrorMessage, + getSafeAuthRedirect, +} from "@/lib/auth"; const loginSchema = z.object({ email: z.string().email("Enter a valid email address"), @@ -21,18 +26,6 @@ type LoginFields = z.infer; const DEFAULT_LOGIN_DESTINATION = "/home"; -function getSafeRedirectDestination(value: string | null): string | null { - if (!value || !value.startsWith("/") || value.startsWith("//")) { - return null; - } - - if (value === "/login" || value.startsWith("/login?")) { - return null; - } - - return value; -} - function LoginForm() { const router = useRouter(); const searchParams = useSearchParams(); @@ -49,9 +42,10 @@ function LoginForm() { setServerError(null); try { await api.post("/auth/login", data); - const destination = - getSafeRedirectDestination(searchParams.get("redirect")) ?? - DEFAULT_LOGIN_DESTINATION; + const destination = getSafeAuthRedirect( + searchParams.get("redirect"), + DEFAULT_LOGIN_DESTINATION, + ); router.push(destination); } catch (err) { setServerError( @@ -60,6 +54,17 @@ function LoginForm() { } }; + const handleGoogleSignIn = () => { + const destination = getSafeAuthRedirect( + searchParams.get("redirect"), + DEFAULT_LOGIN_DESTINATION, + ); + window.location.assign(buildGoogleOAuthStartUrl(destination)); + }; + + const oauthError = getGoogleAuthErrorMessage(searchParams.get("error")); + const visibleError = serverError ?? oauthError; + return (

@@ -69,6 +74,31 @@ function LoginForm() { Sign in to your twizrr account.

+
+ + +

+ Google verifies your email. Phone verification may still be required + before checkout. +

+ +
+
+ + or + +
+
+
+
- {serverError && ( + {visibleError && (

- {serverError} + {visibleError}

)} diff --git a/apps/web/src/app/(auth)/register/page.tsx b/apps/web/src/app/(auth)/register/page.tsx index 09ae936c..618ca231 100644 --- a/apps/web/src/app/(auth)/register/page.tsx +++ b/apps/web/src/app/(auth)/register/page.tsx @@ -1,15 +1,21 @@ "use client"; -import { useState } from "react"; +import { Suspense, useState } from "react"; import { useForm, type SubmitHandler } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; import { Input } from "@/components/ui/Input"; import { Button } from "@/components/ui/Button"; +import { PageLoadingState } from "@/components/ui/brand-loader"; import { api, type ApiError } from "@/lib/api"; +import { + buildGoogleOAuthStartUrl, + getGoogleAuthErrorMessage, + getSafeAuthRedirect, +} from "@/lib/auth"; function calcAge(dobString: string): number { const dob = new Date(dobString); @@ -120,8 +126,11 @@ function StepProgress({ step, total }: { step: number; total: number }) { ); } -export default function RegisterPage() { +const DEFAULT_REGISTER_DESTINATION = "/explore"; + +function RegisterForm() { const router = useRouter(); + const searchParams = useSearchParams(); const [step, setStep] = useState(1); const [serverError, setServerError] = useState(null); @@ -150,6 +159,14 @@ export default function RegisterPage() { router.push(`/verify-email?${params.toString()}`); } + function startGoogleAuth() { + const destination = getSafeAuthRedirect( + searchParams.get("redirect"), + DEFAULT_REGISTER_DESTINATION, + ); + window.location.assign(buildGoogleOAuthStartUrl(destination)); + } + const form2 = useForm({ resolver: zodResolver(step2Schema), defaultValues: { email: data.email, password: data.password }, @@ -210,6 +227,8 @@ export default function RegisterPage() { } }; + const oauthError = getGoogleAuthErrorMessage(searchParams.get("error")); + return (
{step > 1 && ( @@ -235,13 +254,32 @@ export default function RegisterPage() {

- +

+ Google verifies your email. Phone verification may still be + required before checkout. +

+ {oauthError && ( +

+ {oauthError} +

+ )} +
@@ -459,3 +497,11 @@ export default function RegisterPage() {
); } + +export default function RegisterPage() { + return ( + }> + + + ); +} diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts index 968ba0cb..3c388496 100644 --- a/apps/web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -1,5 +1,14 @@ import { api } from "@/lib/api"; +const API_BASE = (process.env.NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, ""); + +const AUTH_ROUTE_PREFIXES = [ + "/login", + "/register", + "/forgot-password", + "/verify-email", +]; + export type GoogleOnboardingPrefill = { providerEmail: string; providerEmailVerified: boolean; @@ -20,6 +29,51 @@ export type CompleteGoogleOnboardingResponse = { redirectTo: string; }; +export function getSafeAuthRedirect( + value: string | null | undefined, + fallback: string, +): string { + if (!value || !value.startsWith("/") || value.startsWith("//")) { + return fallback; + } + + if (AUTH_ROUTE_PREFIXES.some((prefix) => value.startsWith(prefix))) { + return fallback; + } + + return value; +} + +export function buildGoogleOAuthStartUrl(redirectPath: string): string { + const params = new URLSearchParams({ redirect: redirectPath }); + return `${API_BASE}/auth/google?${params.toString()}`; +} + +export function getGoogleAuthErrorMessage(error: string | null): string | null { + if (!error) { + return null; + } + + const normalized = error.trim().toUpperCase(); + + if ( + normalized.includes("EMAIL") && + (normalized.includes("UNVERIFIED") || normalized.includes("NOT_VERIFIED")) + ) { + return "Google email must be verified."; + } + + if (normalized.includes("SESSION") || normalized.includes("EXPIRED")) { + return "Google session expired. Please try again."; + } + + if (normalized.includes("GOOGLE") || normalized.includes("OAUTH")) { + return "Google sign-in failed. Please try again."; + } + + return "Authentication failed. Please try again."; +} + export function getGoogleOnboardingPrefill( sessionId: string, ): Promise {