diff --git a/apps/backoffice/src/app/features/question/pages/QuestionPage.tsx b/apps/backoffice/src/app/features/question/pages/QuestionPage.tsx
index 334de6e20..502b6c59b 100644
--- a/apps/backoffice/src/app/features/question/pages/QuestionPage.tsx
+++ b/apps/backoffice/src/app/features/question/pages/QuestionPage.tsx
@@ -15,6 +15,7 @@ import {
Select,
NumberInput,
Textarea,
+ Table,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
@@ -26,7 +27,7 @@ import {
IconSend,
IconGavel,
} from '@tabler/icons-react';
-import { notify } from '@ema-platform/ui';
+import { AdvancedColumn, ModalFooter, notify } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
import {
diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx
index 919df14d7..1952936c4 100644
--- a/apps/portal/src/app/router.tsx
+++ b/apps/portal/src/app/router.tsx
@@ -1,79 +1,91 @@
-import { createBrowserRouter, Navigate } from 'react-router-dom';
-import { I18nextProvider } from 'react-i18next';
-import { i18n } from './i18n/config';
-import { PortalLayout } from './layouts/PortalLayout';
-import { ProtectedRoute } from './components/ProtectedRoute';
+import { createBrowserRouter, Navigate } from "react-router-dom";
+import { I18nextProvider } from "react-i18next";
+import { i18n } from "./i18n/config";
+import { PortalLayout } from "./layouts/PortalLayout";
+import { ProtectedRoute } from "./components/ProtectedRoute";
// Auth (standalone pages, no portal chrome)
-import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage, SetPasswordPage } from '@ema-platform/auth';
+import {
+ LoginPage,
+ SignupPage,
+ OTPVerificationPage,
+ ForgotPasswordPage,
+ SetPasswordPage,
+} from "@ema-platform/auth";
// Portal feature pages
-import { DashboardPage } from './features/dashboard/pages/DashboardPage';
-import { RequireOperations } from './features/onboarding/components/RequireOperations';
-import { OperationsOnboardingPage } from './features/onboarding/pages/OperationsOnboardingPage';
-import { ProfilePage } from './features/profile/pages/ProfilePage';
-import { SupportPage } from './features/support/pages/SupportPage';
-import { SeafarerRegistrationPage } from './features/seafarer/pages/SeafarerRegistrationPage';
-import { MySeaRecordsPage } from './features/seafarer/pages/MySeaRecordsPage';
-import { VerifyCertificatePage } from './features/verify/pages/VerifyCertificatePage';
-import { ExamsPage } from './features/exams/pages/ExamsPage';
+import { DashboardPage } from "./features/dashboard/pages/DashboardPage";
+import { RequireOperations } from "./features/onboarding/components/RequireOperations";
+import { OperationsOnboardingPage } from "./features/onboarding/pages/OperationsOnboardingPage";
+import { ProfilePage } from "./features/profile/pages/ProfilePage";
+import { SupportPage } from "./features/support/pages/SupportPage";
+import { SeafarerRegistrationPage } from "./features/seafarer/pages/SeafarerRegistrationPage";
+import { MySeaRecordsPage } from "./features/seafarer/pages/MySeaRecordsPage";
+import { VerifyCertificatePage } from "./features/verify/pages/VerifyCertificatePage";
+import { ExamsPage } from "./features/exams/pages/ExamsPage";
// Phase 1 pages
-import { DocumentVaultPage } from './features/documents/pages/DocumentVaultPage';
-import { SeamanBookPage } from './features/seaman-book/pages/SeamanBookPage';
-import { SeamanBookApplicationPage } from './features/seaman-book/pages/SeamanBookApplicationPage';
-import { NotificationsPage } from './features/notifications/pages/NotificationsPage';
+import { DocumentVaultPage } from "./features/documents/pages/DocumentVaultPage";
+import { SeamanBookPage } from "./features/seaman-book/pages/SeamanBookPage";
+import { SeamanBookApplicationPage } from "./features/seaman-book/pages/SeamanBookApplicationPage";
+import { NotificationsPage } from "./features/notifications/pages/NotificationsPage";
// Phase 2 — CoC / CoP
-import { CertificatesPage } from './features/certificates/pages/CertificatesPage';
-import { CoCApplicationPage } from './features/certificates/pages/CoCApplicationPage';
+import { CertificatesPage } from "./features/certificates/pages/CertificatesPage";
+import { CoCApplicationPage } from "./features/certificates/pages/CoCApplicationPage";
// Phase 3 — Endorsement
-import { EndorsementPage } from './features/endorsement/pages/EndorsementPage';
-import { VesselRegistrationPage } from './features/vessel-registration/pages/VesselRegistrationPage';
-import { OwnershipTransferPage } from './features/vessel-registration/pages/OwnershipTransferPage';
-import { MyApplicationsPage } from './features/licensing/pages/MyApplicationsPage';
-import { PaymentCheckPage } from './features/payments/pages/PaymentCheckPage';
-import { PaymentSuccessPage } from './features/payments/pages/PaymentSuccessPage';
-import { PaymentFailurePage } from './features/payments/pages/PaymentFailurePage';
-import { LicenseApplicationPage } from './features/licensing/pages/LicenseApplicationPage';
-import { ApplicationRedirectPage } from './features/licensing/pages/ApplicationRedirectPage';
-import { WaiverPage } from './features/waiver/pages/WaiverPage';
-
+import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
+import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
+import { OwnershipTransferPage } from "./features/vessel-registration/pages/OwnershipTransferPage";
+import { MyApplicationsPage } from "./features/licensing/pages/MyApplicationsPage";
+import { PaymentCheckPage } from "./features/payments/pages/PaymentCheckPage";
+import { PaymentSuccessPage } from "./features/payments/pages/PaymentSuccessPage";
+import { PaymentFailurePage } from "./features/payments/pages/PaymentFailurePage";
+import { LicenseApplicationPage } from "./features/licensing/pages/LicenseApplicationPage";
+import { ApplicationRedirectPage } from "./features/licensing/pages/ApplicationRedirectPage";
+import { WaiverPage } from "./features/waiver/pages/WaiverPage";
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
-
export const router = createBrowserRouter([
// Public auth pages
- { path: '/login', element: },
- { path: '/signup', element: },
+ { path: "/login", element: },
+ { path: "/signup", element: },
// Public certificate verification — the target of every printed QR code.
// No auth: a verifier scanning a certificate has no portal account.
- { path: '/verify', element: },
- { path: '/verify/:code', element: },
+ { path: "/verify", element: },
+ { path: "/verify/:code", element: },
// Completes the forgot-password flow; the reset message links here. The
// IAM package generates `/reset-password` links, `/set-password` is the
// first-time-credential variant — one page serves both.
- { path: '/set-password', element: },
- { path: '/reset-password', element: },
+ { path: "/set-password", element: },
+ { path: "/reset-password", element: },
// Protected auth pages
{
- element: ,
- path: '/otp-verify',
+ element: (
+
+
+
+ ),
+ path: "/otp-verify",
},
{
- element: ,
- path: '/forgot-password',
+ element: (
+
+
+
+ ),
+ path: "/forgot-password",
},
// The two-step setup wizard is gone. Signing up lands on the dashboard, and
// profile details are collected where they are actually needed: on /profile,
// via the dashboard nudge, or inline in an application flow. The path stays
// as a redirect so existing bookmarks and emailed links do not 404.
- { path: '/profile-setup', element: },
+ { path: "/profile-setup", element: },
// Portal — protected.
{
@@ -89,83 +101,159 @@ export const router = createBrowserRouter([
),
children: [
- { path: '/', element: },
- { path: '/dashboard', element: },
- { path: '/onboarding/operations', element: },
+ { path: "/", element: },
+ { path: "/dashboard", element: },
+ { path: "/onboarding/operations", element: },
// Config-driven licensing: one set of pages serves every licence type.
- { path: '/licensing/applications', element: },
+ { path: "/licensing/applications", element: },
// Telebirr returns the applicant to these.
- { path: '/payments/check', element: },
- { path: '/payments/success', element: },
- { path: '/payments/failure', element: },
- { path: '/licensing/:typeCode/apply', element: },
+ { path: "/payments/check", element: },
+ { path: "/payments/success", element: },
+ { path: "/payments/failure", element: },
{
- path: '/licensing/:typeCode/applications/:applicationId',
+ path: "/licensing/:typeCode/apply",
+ element: ,
+ },
+ {
+ path: "/licensing/:typeCode/applications/:applicationId",
element: ,
},
// Notification / email deep links arrive as /applications/; resolve the
// licence type and forward to the canonical route.
- { path: '/applications/:applicationId', element: },
+ {
+ path: "/applications/:applicationId",
+ element: ,
+ },
// Seafarer
- { path: '/seafarer-registration', element: },
- { path: '/seafarer/records', element: },
- { path: '/exams', element: },
+ { path: "/seafarer-registration", element: },
+ { path: "/seafarer/records", element: },
+ { path: "/exams", element: },
// The public-facing registry was a hardcoded mock and does not belong in
// the applicant portal; officers browse seafarers in the backoffice.
- { path: '/seafarer-registry', element: },
- { path: '/seafarer-registry/:id', element: },
+ {
+ path: "/seafarer-registry",
+ element: ,
+ },
+ {
+ path: "/seafarer-registry/:id",
+ element: ,
+ },
// Phase 1
- { path: '/documents', element: },
- { path: '/seaman-book', element: },
- { path: '/seaman-book/apply', element: },
- { path: '/notifications', element: },
+ { path: "/documents", element: },
+ { path: "/seaman-book", element: },
+ { path: "/seaman-book/apply", element: },
+ { path: "/notifications", element: },
// Phase 2 — CoC / CoP
- { path: '/certificates', element: },
- { path: '/certificates/apply', element: },
+ { path: "/certificates", element: },
+ { path: "/certificates/apply", element: },
// Phase 3 — Endorsement
- { path: '/endorsements', element: },
- { path: '/vessel-registration', element: },
+ { path: "/endorsements", element: },
+ { path: "/vessel-registration", element: },
// The registration wizard is the config-driven licensing flow; the old
// standalone wizard posted to endpoints that never existed.
- { path: '/vessel-registration/apply', element: },
- { path: '/vessel-registration-dashboard', element: },
- { path: '/vessel-registration/transfer', element: },
+ {
+ path: "/vessel-registration/apply",
+ element: ,
+ },
+ {
+ path: "/vessel-registration-dashboard",
+ element: ,
+ },
+ {
+ path: "/vessel-registration/transfer",
+ element: ,
+ },
// Legacy per-licence-type URLs. Each once had its own hand-written page
// that posted to a `/logistics-licenses/*` endpoint the API never had,
// and dropped every uploaded document on the floor. They are kept as
// redirects so old bookmarks land somewhere real; the config-driven
// wizard below serves every licence type from one place.
- { path: '/logistics-dashboard', element: },
- { path: '/freight-forwarder-license', element: },
- { path: '/freight-forwarder-license/apply', element: },
- { path: '/freight-forwarder-license/:id/renew', element: },
- { path: '/shipping-agent-license', element: },
- { path: '/shipping-agent-license/apply', element: },
- { path: '/shipping-agent-license/:id/renew', element: },
- { path: '/combined-license', element: },
- { path: '/combined-license/apply', element: },
- { path: '/combined-license/:id/renew', element: },
- { path: '/joint-investment-license', element: },
- { path: '/joint-investment-license/apply', element: },
- { path: '/joint-investment-license/:id/renew', element: },
- { path: '/mto-license', element: },
- { path: '/mto-license/apply', element: },
- { path: '/mto-license/:id/renew', element: },
+ {
+ path: "/logistics-dashboard",
+ element: ,
+ },
+ {
+ path: "/freight-forwarder-license",
+ element: ,
+ },
+ {
+ path: "/freight-forwarder-license/apply",
+ element: ,
+ },
+ {
+ path: "/freight-forwarder-license/:id/renew",
+ element: ,
+ },
+ {
+ path: "/shipping-agent-license",
+ element: ,
+ },
+ {
+ path: "/shipping-agent-license/apply",
+ element: ,
+ },
+ {
+ path: "/shipping-agent-license/:id/renew",
+ element: ,
+ },
+ {
+ path: "/combined-license",
+ element: ,
+ },
+ {
+ path: "/combined-license/apply",
+ element: ,
+ },
+ {
+ path: "/combined-license/:id/renew",
+ element: ,
+ },
+ {
+ path: "/joint-investment-license",
+ element: ,
+ },
+ {
+ path: "/joint-investment-license/apply",
+ element: ,
+ },
+ {
+ path: "/joint-investment-license/:id/renew",
+ element: ,
+ },
+ {
+ path: "/mto-license",
+ element: ,
+ },
+ {
+ path: "/mto-license/apply",
+ element: (
+
+ ),
+ },
+ {
+ path: "/mto-license/:id/renew",
+ element: ,
+ },
// Waiver has no backend yet, so it says so rather than pretending.
- { path: '/waiver', element: },
- { path: '/waiver/apply', element: },
+ { path: "/waiver", element: },
+ { path: "/waiver/apply", element: },
// Vessel Registration
- { path: '/vessel-registrations', element: },
- { path: '/vessel-registrations/apply', element: },
- { path: '/vessel-registrations/:id', element: },
-
+ { path: "/vessel-registrations", element: },
+ // { path: '/vessel-registrations/apply', element: },
+ {
+ path: "/vessel-registrations/:id",
+ element: ,
+ },
// General
{ path: "/profile", element: },
@@ -175,12 +263,27 @@ export const router = createBrowserRouter([
// The separate vessel-owner login/portal was mock-only and called auth
// endpoints that never existed; vessel owners are ordinary portal users.
- { path: '/vessel-owner/login', element: },
- { path: '/vessel-owner/register', element: },
- { path: '/vessel-owner/dashboard', element: },
- { path: '/vessel-owner/registration', element: },
- { path: '/vessel-owner/registration/apply', element: },
- { path: '/vessel-owner/registration/transfer', element: },
+ { path: "/vessel-owner/login", element: },
+ {
+ path: "/vessel-owner/register",
+ element: ,
+ },
+ {
+ path: "/vessel-owner/dashboard",
+ element: ,
+ },
+ {
+ path: "/vessel-owner/registration",
+ element: ,
+ },
+ {
+ path: "/vessel-owner/registration/apply",
+ element: ,
+ },
+ {
+ path: "/vessel-owner/registration/transfer",
+ element: ,
+ },
- { path: '*', element: },
+ { path: "*", element: },
]);
diff --git a/apps/portal/src/app/store/index.ts b/apps/portal/src/app/store/index.ts
index 58ddfaf7c..f68e7bfaa 100644
--- a/apps/portal/src/app/store/index.ts
+++ b/apps/portal/src/app/store/index.ts
@@ -1,6 +1,5 @@
import { configureStore } from "@reduxjs/toolkit";
import { baseApi, configureTokenRefresh } from "@ema-platform/api";
-import { setToken } from "@ema-platform/auth";
import {
authReducer,
signupReducer,
@@ -8,6 +7,7 @@ import {
authStorage,
refreshAccessToken,
logout,
+ setToken,
} from "@ema-platform/auth";
import type { AuthUser, CurrentProfile } from "@ema-platform/auth";
diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts
index eac397353..b8ecc7986 100644
--- a/libs/auth/src/index.ts
+++ b/libs/auth/src/index.ts
@@ -1,17 +1,31 @@
-export { AuthConfigProvider, useAuthConfig } from './lib/AuthConfig';
-export type { AuthConfigValue } from './lib/AuthConfig';
-export { AuthShell, BrandMark } from './lib/components/AuthShell';
-export { ProtectedRoute } from './lib/components/ProtectedRoute';
-export { AuthBootstrap } from './lib/components/AuthBootstrap';
-export { LoginPage } from './lib/pages/LoginPage';
-export { SignupPage } from './lib/pages/SignupPage';
-export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
-export { SetPasswordPage } from './lib/pages/SetPasswordPage';
-export { OTPVerificationPage } from './lib/pages/OTPVerificationPage';
-export { authReducer, loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } from './lib/store/auth.slice';
-export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice';
-export { usePermissions } from './lib/hooks/usePermissions';
-export type { PermissionSet } from './lib/hooks/usePermissions';
+export { AuthConfigProvider, useAuthConfig } from "./lib/AuthConfig";
+export type { AuthConfigValue } from "./lib/AuthConfig";
+export { AuthShell, BrandMark } from "./lib/components/AuthShell";
+export { ProtectedRoute } from "./lib/components/ProtectedRoute";
+export { AuthBootstrap } from "./lib/components/AuthBootstrap";
+export { LoginPage } from "./lib/pages/LoginPage";
+export { SignupPage } from "./lib/pages/SignupPage";
+export { ForgotPasswordPage } from "./lib/pages/ForgotPasswordPage";
+export { SetPasswordPage } from "./lib/pages/SetPasswordPage";
+export { OTPVerificationPage } from "./lib/pages/OTPVerificationPage";
+export {
+ authReducer,
+ loginSuccess,
+ setUser,
+ setCurrentProfile,
+ clearCurrentProfile,
+ logout,
+ hydrateAuth,
+ setToken,
+} from "./lib/store/auth.slice";
+export {
+ signupReducer,
+ setSignupData,
+ setSignupStep,
+ resetSignup,
+} from "./lib/store/signup.slice";
+export { usePermissions } from "./lib/hooks/usePermissions";
+export type { PermissionSet } from "./lib/hooks/usePermissions";
export {
useCurrentProfile,
useGetMyProfileQuery,
diff --git a/libs/auth/src/lib/pages/LoginPage.tsx b/libs/auth/src/lib/pages/LoginPage.tsx
index 13347c548..502a4b18f 100644
--- a/libs/auth/src/lib/pages/LoginPage.tsx
+++ b/libs/auth/src/lib/pages/LoginPage.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState } from "react";
import {
Alert,
Anchor,
@@ -11,29 +11,56 @@ import {
Text,
TextInput,
Title,
-} from '@mantine/core';
+} from "@mantine/core";
import {
IconArrowRight,
IconDeviceMobile,
IconLock,
IconMail,
-} from '@tabler/icons-react';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
-import { useNavigate, Link } from 'react-router-dom';
-import { useDispatch } from 'react-redux';
-import { useApiMutation } from '@ema-platform/api';
-import { notify } from '@ema-platform/ui';
-import { AuthShell } from '../components/AuthShell';
-import { loginSuccess, setUser, setCurrentProfile } from '../store/auth.slice';
-import type { LoginPayload, AuthUser, CurrentProfile } from '../types/auth.types';
-import { useAuthConfig } from '../AuthConfig';
-import { authStorage } from '../utils/auth-storage';
+} from "@tabler/icons-react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useNavigate, Link } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { useApiMutation } from "@ema-platform/api";
+import { notify, useErrorHandler } from "@ema-platform/ui";
+import { AuthShell } from "../components/AuthShell";
+import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice";
+import type {
+ LoginPayload,
+ AuthUser,
+ CurrentProfile,
+} from "../types/auth.types";
+import { useAuthConfig } from "../AuthConfig";
+import { authStorage } from "../utils/auth-storage";
+const emailOrPhone = z
+ .string()
+ .trim()
+ .transform((value) => {
+ // Convert 09xxxxxxxx -> +2519xxxxxxxx
+ if (/^09\d{8}$/.test(value)) {
+ return `+251${value.substring(1)}`;
+ }
+ return value;
+ })
+ .refine(
+ (value) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const phoneRegex = /^\+2519\d{8}$/;
+
+ return emailRegex.test(value) || phoneRegex.test(value);
+ },
+ {
+ message: "Enter a valid email or phone number (+2519xxxxxxxx)",
+ },
+ );
const schema = z.object({
- email: z.string().email({ message: 'Enter a valid email' }),
- password: z.string().min(5, { message: 'Password must be at least 6 characters' }),
+ email: emailOrPhone,
+ password: z
+ .string()
+ .min(8, { message: "Password must be at least 8 characters" }),
});
type FormValues = z.infer;
@@ -46,9 +73,7 @@ export function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [serverError, setServerError] = useState(null);
- // MFA second step: set once /auth/login answers `mfaRequired` (US-IAM-006).
- const [mfaEmail, setMfaEmail] = useState(null);
- const [mfaOtp, setMfaOtp] = useState('');
+ const { handleError } = useErrorHandler();
const [loginTrigger] = useApiMutation();
const [meTrigger] = useApiMutation();
const [profileTrigger] = useApiMutation<{ profile: CurrentProfile | null }>();
@@ -65,57 +90,15 @@ export function LoginPage() {
setIsLoading(true);
try {
const data = await loginTrigger({
- url: '/auth/login',
- method: 'POST',
+ url: "/auth/login",
+ method: "POST",
body: values,
}).unwrap();
-
- // MFA-enabled accounts get no tokens yet — an OTP has been sent, and
- // the session only exists once /auth/mfa-verify accepts it (US-IAM-006).
- if (data.mfaRequired) {
- setMfaEmail(values.email);
- notify.success('Enter the verification code we just sent you');
- return;
- }
- await completeSession(data);
- } catch (err: unknown) {
- const msg =
- (err as { data?: { message?: string } })?.data?.message ??
- (err instanceof Error ? err.message : 'Something went wrong');
- setServerError(msg);
- notify.error(msg);
- } finally {
- setIsLoading(false);
- }
- };
-
- const verifyMfa = async () => {
- if (!mfaEmail || !mfaOtp.trim()) return;
- setIsLoading(true);
- try {
- const data = await loginTrigger({
- url: '/auth/mfa-verify',
- method: 'POST',
- body: { email: mfaEmail, otp: mfaOtp.trim() },
- }).unwrap();
- // The second factor proves possession of the verified phone.
- await completeSession({ ...data, isPhoneNumberVerified: true });
- } catch (err: unknown) {
- const msg =
- (err as { data?: { message?: string } })?.data?.message ??
- 'Verification failed';
- setServerError(msg === 'unable_to_log_in' ? 'Invalid or expired code' : msg);
- } finally {
- setIsLoading(false);
- }
- };
-
- const completeSession = async (data: LoginPayload) => {
- dispatch(loginSuccess(data));
+ dispatch(loginSuccess(data));
const me = await meTrigger({
- url: '/auth/me',
- method: 'GET',
+ url: "/auth/me",
+ method: "GET",
}).unwrap();
dispatch(setUser(me));
@@ -126,8 +109,8 @@ export function LoginPage() {
// `useCurrentProfile` resolves it again on demand.
try {
const { profile } = await profileTrigger({
- url: '/profiles/me',
- method: 'GET',
+ url: "/profiles/me",
+ method: "GET",
}).unwrap();
if (profile) {
authStorage.setProfileId(profile.id);
@@ -137,17 +120,23 @@ export function LoginPage() {
// Offline or a 5xx — the portal still works; the resolver retries.
}
- if (!me.isPhoneNumberVerified) {
- navigate('/otp-verify', {
- state: {
- email: me.email,
- phoneNumber: me.phoneNumber,
- },
- });
- return;
- }
+ if (!me.isPhoneNumberVerified) {
+ navigate("/otp-verify", {
+ state: {
+ email: me.email,
+ phoneNumber: me.phoneNumber,
+ },
+ });
+ return;
+ }
- navigate(loginRedirectPath);
+ navigate(loginRedirectPath);
+ } catch (err: unknown) {
+ setServerError(handleError(err));
+ } finally {
+ localStorage.setItem("rememberMe", String(rememberMe));
+ setIsLoading(false);
+ }
};
return (
@@ -163,47 +152,16 @@ export function LoginPage() {
{serverError && (
- setServerError(null)}>
+ setServerError(null)}
+ >
{serverError}
)}
- {mfaEmail ? (
-
- }>
- This account requires a second factor. Enter the code we sent to
- your registered phone.
-
- setMfaOtp(e.currentTarget.value)}
- onKeyDown={(e) => e.key === 'Enter' && verifyMfa()}
- />
- }
- >
- Verify and sign in
-
- {
- setMfaEmail(null);
- setMfaOtp('');
- setServerError(null);
- }}
- >
- Back to sign in
-
-
- ) : (
- )}
-
-
-
- }
- onClick={() => notify.info('Phone sign-in is coming soon.')}
- >
- Sign in with phone number
-
-
{enableSignup && (
- Don't have an account?{' '}
+ Don't have an account?{" "}
Create one
@@ -278,4 +222,4 @@ export function LoginPage() {
);
-}
\ No newline at end of file
+}