From fe3b34b6caae2f8a46f8130e7282690ee1e0925f Mon Sep 17 00:00:00 2001 From: BwanikaRobert Date: Sat, 16 May 2026 13:06:48 +0300 Subject: [PATCH 1/8] migration(vertex/login): replace toasts with scoped InfoBanners --- src/vertex/app/login/page.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vertex/app/login/page.tsx b/src/vertex/app/login/page.tsx index 10873bb7f8..a6097580af 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,7 +146,7 @@ export default function LoginPage() { if (!session?.user) { throw new Error("Could not confirm session. Please try again."); } - ReusableToast({ message: "Welcome back!", type: "SUCCESS" }); + showBanner({ severity: 'success', message: 'Welcome back!', scoped: true }); window.location.replace(result.url || redirectUrl); } else { let message = "Login failed. Please check your credentials."; @@ -164,10 +165,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 +277,7 @@ export default function LoginPage() { transition={{ duration: 0.2 }} className="space-y-5" > +
@@ -291,6 +293,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" From f25eebfe5a75456f808445734859e6c6652d710e Mon Sep 17 00:00:00 2001 From: BwanikaRobert Date: Sat, 16 May 2026 13:16:05 +0300 Subject: [PATCH 2/8] migration(vertex/forgot-password): replace inline alerts with scoped InfoBanner --- src/vertex/app/forgot-password/page.tsx | 33 ++++--------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/src/vertex/app/forgot-password/page.tsx b/src/vertex/app/forgot-password/page.tsx index 1a7eba5d3b..6e32a1a252 100644 --- a/src/vertex/app/forgot-password/page.tsx +++ b/src/vertex/app/forgot-password/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; @@ -23,18 +22,14 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - ExclamationTriangleIcon, - CheckCircledIcon, -} from "@radix-ui/react-icons"; +import { useBanner, BannerSlot } from "@/context/banner-context"; 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 { showBanner } = useBanner(); const form = useForm>({ resolver: zodResolver(forgotPasswordSchema), @@ -44,14 +39,13 @@ export default function ForgotPasswordPage() { }); 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"); + showBanner({ severity: 'success', message: 'A password reset link has been sent to your email.', scoped: true }); } catch (error) { console.error(error); - setStatus("error"); + showBanner({ severity: 'error', message: 'An error occurred. Please try again.', scoped: true }); } } @@ -67,6 +61,7 @@ export default function ForgotPasswordPage() {
+ )} /> - {status === "success" && ( - - - Success - - A password reset link has been sent to your email. - - - )} - {status === "error" && ( - - - Error - - An error occurred. Please try again. - - - )} From 97ac00373bfd85a87136a922870aee91ecb36f0f Mon Sep 17 00:00:00 2001 From: BwanikaRobert Date: Sat, 16 May 2026 13:24:03 +0300 Subject: [PATCH 3/8] migration(vertex/components/features/auth/google-auth-section.tsx): replace inline alerts with scoped InfoBanner --- .../components/features/auth/google-auth-section.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 (
From 529c72415c1da5290a43f24f4fadae2251f16725 Mon Sep 17 00:00:00 2001 From: BwanikaRobert Date: Sat, 16 May 2026 14:00:29 +0300 Subject: [PATCH 4/8] fix(vertex/login/page.tsx adda setTimeout of 500ms to allow react to paint the success banner --- src/vertex/app/login/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vertex/app/login/page.tsx b/src/vertex/app/login/page.tsx index a6097580af..dcffb60bf5 100644 --- a/src/vertex/app/login/page.tsx +++ b/src/vertex/app/login/page.tsx @@ -147,7 +147,8 @@ export default function LoginPage() { throw new Error("Could not confirm session. Please try again."); } showBanner({ severity: 'success', message: 'Welcome back!', scoped: true }); - window.location.replace(result.url || redirectUrl); + const destination = result.url || redirectUrl; + window.setTimeout(() => window.location.replace(destination), 300); } else { let message = "Login failed. Please check your credentials."; if (result?.error) { From e10e815819249e038c1e22afdc4a426bf0592791 Mon Sep 17 00:00:00 2001 From: BwanikaRobert Date: Sat, 16 May 2026 17:25:22 +0300 Subject: [PATCH 5/8] fix removed setimeout after a succesful login in --- src/vertex/app/login/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vertex/app/login/page.tsx b/src/vertex/app/login/page.tsx index dcffb60bf5..f7cc16827b 100644 --- a/src/vertex/app/login/page.tsx +++ b/src/vertex/app/login/page.tsx @@ -147,8 +147,8 @@ export default function LoginPage() { throw new Error("Could not confirm session. Please try again."); } showBanner({ severity: 'success', message: 'Welcome back!', scoped: true }); - const destination = result.url || redirectUrl; - window.setTimeout(() => window.location.replace(destination), 300); + + window.location.replace(result.url || redirectUrl); } else { let message = "Login failed. Please check your credentials."; if (result?.error) { From 68000fe849c3cf8888f1e061de649469f37831e5 Mon Sep 17 00:00:00 2001 From: Codebmk Date: Sat, 16 May 2026 20:02:32 +0300 Subject: [PATCH 6/8] remove forgot password dead code --- src/vertex/app/forgot-password/page.tsx | 92 ------------------------- src/vertex/core/auth/authProvider.tsx | 2 +- src/vertex/middleware.ts | 2 +- 3 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 src/vertex/app/forgot-password/page.tsx diff --git a/src/vertex/app/forgot-password/page.tsx b/src/vertex/app/forgot-password/page.tsx deleted file mode 100644 index 6e32a1a252..0000000000 --- a/src/vertex/app/forgot-password/page.tsx +++ /dev/null @@ -1,92 +0,0 @@ -"use client"; - -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 { useBanner, BannerSlot } from "@/context/banner-context"; - -const forgotPasswordSchema = z.object({ - email: z.string().email({ message: "Please enter a valid email address" }), -}); - -export default function ForgotPasswordPage() { - const { showBanner } = useBanner(); - - const form = useForm>({ - resolver: zodResolver(forgotPasswordSchema), - defaultValues: { - email: "", - }, - }); - - async function onSubmit(values: z.infer) { - try { - // Here you would typically send a request to your API to initiate the password reset process - console.log(values); - showBanner({ severity: 'success', message: 'A password reset link has been sent to your email.', scoped: true }); - } catch (error) { - console.error(error); - showBanner({ severity: 'error', message: 'An error occurred. Please try again.', scoped: true }); - } - } - - return ( -
- - - Forgot Password - - Enter your email to receive a password reset link. - - - - - - - ( - - Email - - - - - - )} - /> - - - - - - - Back to Login - - - -
- ); -} 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(?:$|/)|.*\\.).*)', ], }; From 2e9d54cf2d1bd0221a435cb30b5b4c740fc597da Mon Sep 17 00:00:00 2001 From: Codebmk Date: Sat, 16 May 2026 20:06:06 +0300 Subject: [PATCH 7/8] Update changelog.md --- src/vertex/app/changelog.md | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) 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 From 6ad9c0d2ff535eef0d2d334ba7ad17e92beb1733 Mon Sep 17 00:00:00 2001 From: Codebmk Date: Sat, 16 May 2026 20:12:38 +0300 Subject: [PATCH 8/8] open in new tab --- src/vertex/app/login/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vertex/app/login/page.tsx b/src/vertex/app/login/page.tsx index f7cc16827b..de6a7b5b05 100644 --- a/src/vertex/app/login/page.tsx +++ b/src/vertex/app/login/page.tsx @@ -310,7 +310,7 @@ export default function LoginPage() {
- + Forgot password?