diff --git a/src/vertex/app/changelog.md b/src/vertex/app/changelog.md index 5d24b8520a..9554b8bf15 100644 --- a/src/vertex/app/changelog.md +++ b/src/vertex/app/changelog.md @@ -4,6 +4,50 @@ --- +## Version 1.23.41 +**Released:** May 16, 2026 + +### Scoped Banner System Migration, Login UX Fixes & Dead Code Cleanup + +Migrated auth-flow feedback from imperative toast notifications to scoped `InfoBanner` components, hardened the banner-context state management to prevent silent message drops, fixed post-login banner render timing, and removed the defunct local forgot-password page. + +
+Auth UX — Scoped Banner Migration (3) + +- **Login Page Banners**: Replaced all `ReusableToast` calls in the login flow (`app/login/page.tsx`) with scoped `showBanner` / `BannerSlot` calls. Success, error, and validation feedback is now rendered inline within the login form card rather than as floating toasts, giving users precise contextual feedback without losing their place. +- **Google Auth Section Banners**: Migrated `components/features/auth/google-auth-section.tsx` to use scoped `InfoBanner` for OAuth error states, replacing previous inline alert elements and ensuring visual consistency across all sign-in paths. +- **Login Render Timing Fix**: Fixed a race condition where the success banner was not visible before the page redirected after a successful credential login. Resolved by removing an extraneous `setTimeout` that was interfering with React's paint cycle, ensuring the welcome banner renders correctly before navigation occurs. + +
+ +
+Banner Context Hardening (1) + +- **Full Props Stored in State**: Refactored `context/banner-context.tsx` to store the complete banner props object in state rather than individual fields. This prevents silent message drops that occurred when rapid successive `showBanner` calls partially overwrote state before the component re-rendered, resulting in blank or stale banners being displayed. + +
+ +
+Dead Code Removal (1) + +- **Forgot Password Page Deleted**: Removed the local `/forgot-password` Next.js route (`app/forgot-password/page.tsx`), which was a dead stub that only `console.log`ed submitted emails without calling any API. The "Forgot password?" link in the login page continues to redirect users directly to AirQo Analytics (`NEXT_PUBLIC_ANALYTICS_URL/user/forgotPwd`) where actual password reset is handled. Cleaned up the middleware route matcher and `authProvider` auth-routes list accordingly. + +
+ +
+Files Modified/Deleted (5) + +- `src/vertex/app/forgot-password/page.tsx` [DELETED] +- `src/vertex/app/login/page.tsx` [MODIFIED] +- `src/vertex/components/features/auth/google-auth-section.tsx` [MODIFIED] +- `src/vertex/context/banner-context.tsx` [MODIFIED] +- `src/vertex/core/auth/authProvider.tsx` [MODIFIED] +- `src/vertex/middleware.ts` [MODIFIED] + +
+ +--- + ## Version 1.23.40 **Released:** May 16, 2026 diff --git a/src/vertex/app/forgot-password/page.tsx b/src/vertex/app/forgot-password/page.tsx deleted file mode 100644 index 1a7eba5d3b..0000000000 --- a/src/vertex/app/forgot-password/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - ExclamationTriangleIcon, - CheckCircledIcon, -} from "@radix-ui/react-icons"; - -const forgotPasswordSchema = z.object({ - email: z.string().email({ message: "Please enter a valid email address" }), -}); - -export default function ForgotPasswordPage() { - const [status, setStatus] = useState<"idle" | "success" | "error">("idle"); - - const form = useForm>({ - resolver: zodResolver(forgotPasswordSchema), - defaultValues: { - email: "", - }, - }); - - async function onSubmit(values: z.infer) { - setStatus("idle"); - try { - // Here you would typically send a request to your API to initiate the password reset process - console.log(values); - setStatus("success"); - } catch (error) { - console.error(error); - setStatus("error"); - } - } - - return ( -
- - - Forgot Password - - Enter your email to receive a password reset link. - - - -
- - ( - - Email - - - - - - )} - /> - {status === "success" && ( - - - Success - - A password reset link has been sent to your email. - - - )} - {status === "error" && ( - - - Error - - An error occurred. Please try again. - - - )} - - - -
- - - Back to Login - - -
-
- ); -} diff --git a/src/vertex/app/login/page.tsx b/src/vertex/app/login/page.tsx index 10873bb7f8..de6a7b5b05 100644 --- a/src/vertex/app/login/page.tsx +++ b/src/vertex/app/login/page.tsx @@ -12,7 +12,7 @@ import { Form, FormField } from "@/components/ui/form" import { signUpUrl, forgotPasswordUrl } from "@/core/urls" import ReusableInputField from "@/components/shared/inputfield/ReusableInputField" import ReusableButton from "@/components/shared/button/ReusableButton" -import ReusableToast from "@/components/shared/toast/ReusableToast" +import { useBanner, BannerSlot } from "@/context/banner-context" import logger from "@/lib/logger" import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage"; import { useAppDispatch } from "@/core/redux/hooks"; @@ -31,6 +31,7 @@ const loginSchema = z.object({ }) export default function LoginPage() { + const { showBanner, hideBanner } = useBanner(); const [isLoading, setIsLoading] = useState(false) const [step, setStep] = useState<'email' | 'password'>('email'); const searchParams = useSearchParams(); @@ -145,8 +146,9 @@ export default function LoginPage() { if (!session?.user) { throw new Error("Could not confirm session. Please try again."); } - ReusableToast({ message: "Welcome back!", type: "SUCCESS" }); - window.location.replace(result.url || redirectUrl); + showBanner({ severity: 'success', message: 'Welcome back!', scoped: true }); + + window.location.replace(result.url || redirectUrl); } else { let message = "Login failed. Please check your credentials."; if (result?.error) { @@ -164,10 +166,10 @@ export default function LoginPage() { if (!isMounted.current) return; const message = getApiErrorMessage(error); logger.error("Sign-in failed", { error: message }); - ReusableToast({ message, type: "ERROR" }); + showBanner({ severity: 'error', message, scoped: true }); setIsLoading(false); } - }, [callbackUrl, waitForSession, step, form]); + }, [callbackUrl, waitForSession, step, form, showBanner]); return (
@@ -276,6 +278,7 @@ export default function LoginPage() { transition={{ duration: 0.2 }} className="space-y-5" > +
@@ -291,6 +294,7 @@ export default function LoginPage() { onClick={() => { form.resetField('password'); form.clearErrors('password'); + hideBanner(); setStep('email'); }} className="text-xs font-medium text-primary border border-primary/40 rounded-md px-2.5 py-1 hover:bg-primary/10 active:bg-primary/20 transition-colors ml-3 shrink-0 disabled:opacity-50 disabled:cursor-not-allowed" @@ -306,7 +310,7 @@ export default function LoginPage() {
- + Forgot password?
diff --git a/src/vertex/components/features/auth/google-auth-section.tsx b/src/vertex/components/features/auth/google-auth-section.tsx index 78923b1e2a..dfeeb8afe1 100644 --- a/src/vertex/components/features/auth/google-auth-section.tsx +++ b/src/vertex/components/features/auth/google-auth-section.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'; import ReusableButton from '@/components/shared/button/ReusableButton'; import { cn } from '@/lib/utils'; import { buildOAuthInitiationUrl } from '@/core/auth/oauth-session'; -import ReusableToast from '@/components/shared/toast/ReusableToast'; +import { useBanner } from '@/context/banner-context'; import { FcGoogle } from 'react-icons/fc'; interface GoogleAuthSectionProps { @@ -20,6 +20,7 @@ export default function GoogleAuthSection({ className, callbackUrl, }: GoogleAuthSectionProps) { + const { showBanner } = useBanner(); const [isRedirecting, setIsRedirecting] = useState(false); const handleGoogleAuth = useCallback(() => { @@ -37,13 +38,14 @@ export default function GoogleAuthSection({ ); } catch (error) { setIsRedirecting(false); - ReusableToast({ + showBanner({ + severity: 'error', message: 'Google sign-in unavailable. Please try again in a moment.', - type: 'ERROR', + scoped: true, }); console.error('Failed to start Google OAuth flow:', error); } - }, [disabled, callbackUrl]); + }, [disabled, callbackUrl, showBanner]); return (
diff --git a/src/vertex/core/auth/authProvider.tsx b/src/vertex/core/auth/authProvider.tsx index 7c7ae57868..78e099094e 100644 --- a/src/vertex/core/auth/authProvider.tsx +++ b/src/vertex/core/auth/authProvider.tsx @@ -169,7 +169,7 @@ function useUserDetails(userId: string | null) { // --- Components --- -const authRoutes = ['/login', '/forgot-password', '/auth-error']; +const authRoutes = ['/login', '/auth-error']; const publicRoutes = [...authRoutes, '/download']; const matchesRoute = (pathname: string, route: string) => pathname === route || pathname.startsWith(`${route}/`); diff --git a/src/vertex/middleware.ts b/src/vertex/middleware.ts index 104060300e..08dae96c51 100644 --- a/src/vertex/middleware.ts +++ b/src/vertex/middleware.ts @@ -39,6 +39,6 @@ export const config = { * - favicon.ico (favicon file) * - public files with extensions */ - '/((?!api(?:$|/)|_next/static(?:$|/)|_next/image(?:$|/)|favicon\\.ico$|login(?:$|/)|download(?:$|/)|auth-error(?:$|/)|forgot-password(?:$|/)|.*\\.).*)', + '/((?!api(?:$|/)|_next/static(?:$|/)|_next/image(?:$|/)|favicon\\.ico$|login(?:$|/)|download(?:$|/)|auth-error(?:$|/)|.*\\.).*)', ], };