Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/vertex/app/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary><strong>Auth UX — Scoped Banner Migration (3)</strong></summary>

- **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.

</details>

<details>
<summary><strong>Banner Context Hardening (1)</strong></summary>

- **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.

</details>

<details>
<summary><strong>Dead Code Removal (1)</strong></summary>

- **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.

</details>

<details>
<summary><strong>Files Modified/Deleted (5)</strong></summary>

- `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]

</details>

---

## Version 1.23.40
**Released:** May 16, 2026

Expand Down
115 changes: 0 additions & 115 deletions src/vertex/app/forgot-password/page.tsx

This file was deleted.

16 changes: 10 additions & 6 deletions src/vertex/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand Down Expand Up @@ -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);

@Codebmk Codebmk May 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only replace the toast, keep the window.location.... code as previously was @BwanikaRobert

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) {
Expand All @@ -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 (
<div className="flex min-h-screen lg:h-screen w-full flex-col bg-primary-50 text-foreground">
Expand Down Expand Up @@ -276,6 +278,7 @@ export default function LoginPage() {
transition={{ duration: 0.2 }}
className="space-y-5"
>
<BannerSlot />
<div className="rounded-lg bg-muted/50 p-3 flex items-center justify-between">
<div className="flex flex-col min-w-0">
<span className="text-xs text-muted-foreground">
Expand All @@ -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"
Expand All @@ -306,7 +310,7 @@ export default function LoginPage() {
<div>
<div className="flex items-center justify-between mb-1.5">
<label htmlFor="password" className="text-sm font-medium text-foreground">Password</label>
<Link href={forgotPasswordUrl} className="text-xs font-medium text-primary hover:text-primary/80 transition-colors">
<Link href={forgotPasswordUrl} target="_blank" className="text-xs font-medium text-primary hover:text-primary/80 transition-colors">
Forgot password?
</Link>
</div>
Expand Down
10 changes: 6 additions & 4 deletions src/vertex/components/features/auth/google-auth-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,6 +20,7 @@ export default function GoogleAuthSection({
className,
callbackUrl,
}: GoogleAuthSectionProps) {
const { showBanner } = useBanner();
const [isRedirecting, setIsRedirecting] = useState(false);

const handleGoogleAuth = useCallback(() => {
Expand All @@ -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 (
<div className={cn('w-full space-y-4', className)}>
Expand Down
2 changes: 1 addition & 1 deletion src/vertex/core/auth/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}/`);
Expand Down
2 changes: 1 addition & 1 deletion src/vertex/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(?:$|/)|.*\\.).*)',
],
};
Loading