diff --git a/src/vertex/app/changelog.md b/src/vertex/app/changelog.md
index 484f671d68..6761cfbe60 100644
--- a/src/vertex/app/changelog.md
+++ b/src/vertex/app/changelog.md
@@ -4,6 +4,38 @@
---
+## Version 1.23.49
+**Released:** May 26, 2026
+
+### Grid Management Banner Migration
+
+Migrated all toast-based notifications in the Grid Management module to the centralized `InfoBanner` system powered by `useBanner`. Since most grid actions occur inside dialogs, banner scoping was applied to keep errors inline in the active dialog while deferring success banners until the dialog has closed.
+
+
+Grid Management Banner Migration (4)
+
+- **Dialog Error Feedback**: `create-grid.tsx`, `edit-grid-details-dialog.tsx`, and `admin-levels-modal.tsx` now use `scoped: true` so validation and action errors remain visible inside the active dialog without closing it.
+- **Post-Dialog Success Feedback**: `create-grid.tsx` and `edit-grid-details-dialog.tsx` now use `scoped: false` with a `setTimeout(100ms)` to allow the dialog to unmount before showing the global success banner.
+ - **Admin Level Error Feedback Migrated**: `create-admin-level.tsx` now routes existing error feedback through the centralized banner flow instead of the previous toast-based mechanism.
+- **Copy Feedback Hardened**: `grid-details-card.tsx` and `grid-measurements-api-card.tsx` now use `scoped: false` and handle async copy failures with a fallback error message.
+
+
+
+
+Files Updated (7)
+
+- `src/vertex/components/features/grids/create-grid.tsx` [MODIFIED]
+- `src/vertex/components/features/grids/edit-grid-details-dialog.tsx` [MODIFIED]
+- `src/vertex/components/features/grids/admin-levels-modal.tsx` [MODIFIED]
+- `src/vertex/components/features/grids/create-admin-level.tsx` [MODIFIED]
+- `src/vertex/components/features/grids/grid-details-card.tsx` [MODIFIED]
+- `src/vertex/components/features/grids/grid-measurements-api-card.tsx` [MODIFIED]
+- `src/vertex/core/hooks/useGrids.ts` [MODIFIED]
+
+
+
+---
+
## Version 1.23.48
**Released:** May 23, 2026
diff --git a/src/vertex/components/features/grids/admin-levels-modal.tsx b/src/vertex/components/features/grids/admin-levels-modal.tsx
index 76b04456f7..2ddf49fec4 100644
--- a/src/vertex/components/features/grids/admin-levels-modal.tsx
+++ b/src/vertex/components/features/grids/admin-levels-modal.tsx
@@ -6,7 +6,8 @@ import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { Copy, Edit2, Check, X } from "lucide-react";
-import ReusableToast from "@/components/shared/toast/ReusableToast";
+import { useBanner } from "@/context/banner-context";
+import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
interface AdminLevelsModalProps {
isOpen: boolean;
@@ -15,21 +16,43 @@ interface AdminLevelsModalProps {
export function AdminLevelsModal({ isOpen, onClose }: AdminLevelsModalProps) {
const { adminLevels, isLoading: isLoadingLevels } = useAdminLevels();
- const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel();
+ const { showBanner } = useBanner();
+
const [editingId, setEditingId] = useState(null);
const [editValue, setEditValue] = useState("");
+ const { updateAdminLevel, isLoading: isUpdating } = useUpdateAdminLevel({
+ onSuccess: (data) => {
+ showBanner({
+ message: `Admin level updated to '${data.admin_levels.name}' successfully`,
+ severity: "success",
+ scoped: true,
+ });
+ setEditingId(null);
+ setEditValue("");
+ },
+ onError: (error) => {
+ showBanner({
+ message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
+ severity: "error",
+ scoped: true,
+ });
+ }
+ });
+
const handleCopyId = async (id: string) => {
try {
await navigator.clipboard.writeText(id);
- ReusableToast({
+ showBanner({
message: "ID copied to clipboard",
- type: "SUCCESS",
+ severity: "success",
+ scoped: true,
});
} catch {
- ReusableToast({
+ showBanner({
message: "Failed to copy ID",
- type: "ERROR",
+ severity: "error",
+ scoped: true,
});
}
};
@@ -49,15 +72,7 @@ export function AdminLevelsModal({ isOpen, onClose }: AdminLevelsModalProps) {
if (!name) {
return;
}
- updateAdminLevel(
- { levelId, data: { name } },
- {
- onSuccess: () => {
- setEditingId(null);
- setEditValue("");
- },
- }
- );
+ updateAdminLevel({ levelId, data: { name } });
};
return (
diff --git a/src/vertex/components/features/grids/create-admin-level.tsx b/src/vertex/components/features/grids/create-admin-level.tsx
index 5d6021f00d..de52605348 100644
--- a/src/vertex/components/features/grids/create-admin-level.tsx
+++ b/src/vertex/components/features/grids/create-admin-level.tsx
@@ -17,6 +17,9 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AdminLevelsModal } from "./admin-levels-modal";
+import { useBanner } from "@/context/banner-context";
+import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
+import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";
const adminLevelFormSchema = z.object({
name: z.string().min(2, {
@@ -29,7 +32,8 @@ type AdminLevelFormValues = z.infer;
export function CreateAdminLevel() {
const [open, setOpen] = useState(false);
const [viewModalOpen, setViewModalOpen] = useState(false);
- const { createAdminLevel, isLoading } = useCreateAdminLevel();
+ const { showBanner } = useBanner();
+ const { showDeferredBanner } = useDeferredBanner();
const form = useForm({
resolver: zodResolver(adminLevelFormSchema),
@@ -43,12 +47,22 @@ export function CreateAdminLevel() {
form.reset();
};
+ const { createAdminLevel, isLoading } = useCreateAdminLevel({
+ onSuccess: () => {
+ handleClose();
+ showDeferredBanner({ message: "Admin level created successfully", severity: "success", scoped: false });
+ },
+ onError: (error) => {
+ showBanner({
+ message: `Failed to create admin level: ${getApiErrorMessage(error)}`,
+ severity: "error",
+ scoped: true,
+ });
+ },
+ });
+
const onSubmit = (data: AdminLevelFormValues) => {
- createAdminLevel(data, {
- onSuccess: () => {
- handleClose();
- },
- });
+ createAdminLevel(data);
};
return (
diff --git a/src/vertex/components/features/grids/create-grid.tsx b/src/vertex/components/features/grids/create-grid.tsx
index b028f9a82d..f3a2eb7b30 100644
--- a/src/vertex/components/features/grids/create-grid.tsx
+++ b/src/vertex/components/features/grids/create-grid.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState,useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
@@ -15,6 +15,9 @@ import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableInputField from "@/components/shared/inputfield/ReusableInputField";
import { useNetworks } from "@/core/hooks/useNetworks";
import ReusableSelectInput from "@/components/shared/select/ReusableSelectInput";
+import { useBanner } from "@/context/banner-context";
+import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
+import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";
// Lazy load MiniMap to reduce initial bundle size
const MiniMap = dynamic(() => import("@/components/features/mini-map/mini-map"), {
@@ -58,7 +61,9 @@ type GridFormValues = z.infer;
export function CreateGridForm() {
const [open, setOpen] = useState(false);
const polygon = useAppSelector((state) => state.grids.polygon);
- const { createGrid, isLoading } = useCreateGrid();
+ const { showBanner } = useBanner();
+ const { showDeferredBanner } = useDeferredBanner();
+
const { networks, isLoading: isLoadingNetworks } = useNetworks();
const form = useForm({
@@ -71,6 +76,25 @@ export function CreateGridForm() {
},
});
+ const handleClose = () => {
+ setOpen(false);
+ form.reset();
+ };
+
+ const { createGrid, isLoading } = useCreateGrid({
+ onSuccess: () => {
+ handleClose();
+ showDeferredBanner({ message: `New grid added!`, severity: "success", scoped: false });
+ },
+ onError: (error) => {
+ showBanner({
+ message: `Failed to create grid: ${getApiErrorMessage(error)}`,
+ severity: "error",
+ scoped: true,
+ });
+ }
+ });
+
useEffect(() => {
if (polygon) {
form.setValue("shapefile", JSON.stringify(polygon), { shouldValidate: true });
@@ -79,11 +103,6 @@ export function CreateGridForm() {
}
}, [polygon, form]);
- const handleClose = () => {
- setOpen(false);
- form.reset();
- };
-
const onSubmit = (data: GridFormValues) => {
const gridData = {
name: data.name,
@@ -92,11 +111,7 @@ export function CreateGridForm() {
network: data.network,
};
- createGrid(gridData, {
- onSuccess: () => {
- handleClose();
- },
- });
+ createGrid(gridData);
};
return (
diff --git a/src/vertex/components/features/grids/edit-grid-details-dialog.tsx b/src/vertex/components/features/grids/edit-grid-details-dialog.tsx
index 3d0141e0bd..94bc93ee12 100644
--- a/src/vertex/components/features/grids/edit-grid-details-dialog.tsx
+++ b/src/vertex/components/features/grids/edit-grid-details-dialog.tsx
@@ -6,6 +6,9 @@ import { useUpdateGridDetails } from "@/core/hooks/useGrids";
import { usePermission } from "@/core/hooks/usePermissions";
import { PERMISSIONS } from "@/core/permissions/constants";
import { Grid } from "@/app/types/grids";
+import { useBanner } from "@/context/banner-context";
+import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
+import { useDeferredBanner } from "@/core/hooks/useDeferredBanner";
interface EditGridDetailsDialogProps {
open: boolean;
@@ -19,7 +22,23 @@ const EditGridDetailsDialog: React.FC = ({
onClose,
}) => {
const [form, setForm] = useState({ name: "", visibility: false, admin_level: "" });
- const { updateGridDetails, isLoading } = useUpdateGridDetails(grid._id);
+ const { showBanner } = useBanner();
+ const { showDeferredBanner } = useDeferredBanner();
+
+ const { updateGridDetails, isLoading } = useUpdateGridDetails(grid._id, {
+ onSuccess: () => {
+ onClose();
+ showDeferredBanner({ message: "Grid details updated successfully", severity: "success", scoped: false });
+ },
+ onError: (error) => {
+ showBanner({
+ message: `Failed to update grid: ${getApiErrorMessage(error)}`,
+ severity: "error",
+ scoped: true,
+ });
+ }
+ });
+
const canUpdate = usePermission(PERMISSIONS.SITE.UPDATE);
useEffect(() => {
@@ -39,6 +58,11 @@ const EditGridDetailsDialog: React.FC = ({
const trimmedAdminLevel = form.admin_level.trim();
if (trimmedName.length === 0 || trimmedAdminLevel.length === 0) {
+ showBanner({
+ severity: "error",
+ message: "Grid Name and Admin Level cannot be empty.",
+ scoped: true,
+ });
return;
}
@@ -56,9 +80,8 @@ const EditGridDetailsDialog: React.FC = ({
try {
await updateGridDetails(updates);
- onClose();
} catch {
- return
+ return;
}
};
diff --git a/src/vertex/components/features/grids/grid-details-card.tsx b/src/vertex/components/features/grids/grid-details-card.tsx
index d29329b8a3..2ff938ac8a 100644
--- a/src/vertex/components/features/grids/grid-details-card.tsx
+++ b/src/vertex/components/features/grids/grid-details-card.tsx
@@ -5,8 +5,8 @@ import { Badge } from "@/components/ui/badge";
import { Loader2 } from "lucide-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01, AqEdit01 } from "@airqo/icons-react";
-import ReusableToast from "@/components/shared/toast/ReusableToast";
import { Grid } from "@/app/types/grids";
+import { useBanner } from "@/context/banner-context";
interface GridDetailsCardProps {
grid: Grid;
@@ -15,14 +15,19 @@ interface GridDetailsCardProps {
}
const GridDetailsCard: React.FC = ({ grid, onEdit, loading }) => {
+ const { showBanner } = useBanner();
+
if (loading) {
return ;
}
- const handleCopy = (text: string) => {
- if (text) {
- navigator.clipboard.writeText(text);
- ReusableToast({ message: "Copied to clipboard", type: "SUCCESS" });
+ const handleCopy = async (text: string) => {
+ if (!text) return;
+ try {
+ await navigator.clipboard.writeText(text);
+ showBanner({ message: "Copied to clipboard", severity: "success", scoped: false });
+ } catch {
+ showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
diff --git a/src/vertex/components/features/grids/grid-measurements-api-card.tsx b/src/vertex/components/features/grids/grid-measurements-api-card.tsx
index 7ba270fe01..435ce88c90 100644
--- a/src/vertex/components/features/grids/grid-measurements-api-card.tsx
+++ b/src/vertex/components/features/grids/grid-measurements-api-card.tsx
@@ -4,8 +4,8 @@ import { Card } from "@/components/ui/card";
import { Loader2 } from "lucide-react";
import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01 } from "@airqo/icons-react";
-import ReusableToast from "@/components/shared/toast/ReusableToast";
import { Grid } from "@/app/types/grids";
+import { useBanner } from "@/context/banner-context";
interface GridMeasurementsApiCardProps {
grid: Grid;
@@ -13,6 +13,8 @@ interface GridMeasurementsApiCardProps {
}
const GridMeasurementsApiCard: React.FC = ({ grid, loading }) => {
+ const { showBanner } = useBanner();
+
if (loading) {
return ;
}
@@ -20,21 +22,10 @@ const GridMeasurementsApiCard: React.FC = ({ grid,
const handleCopy = async (text: string) => {
if (!text) return;
try {
- if (navigator?.clipboard?.writeText) {
- await navigator.clipboard.writeText(text);
- } else {
- const el = document.createElement("textarea");
- el.value = text;
- el.style.position = "fixed";
- el.style.opacity = "0";
- document.body.appendChild(el);
- el.select();
- document.execCommand("copy");
- document.body.removeChild(el);
- }
- ReusableToast({ message: "API URL copied!", type: "SUCCESS" });
+ await navigator.clipboard.writeText(text);
+ showBanner({ message: "API URL copied!", severity: "success", scoped: false });
} catch {
- ReusableToast({ message: "Failed to copy to clipboard", type: "ERROR" });
+ showBanner({ message: "Failed to copy to clipboard", severity: "error", scoped: false });
}
};
diff --git a/src/vertex/context/banner-context.tsx b/src/vertex/context/banner-context.tsx
index 8fb984e875..0bf29966e0 100644
--- a/src/vertex/context/banner-context.tsx
+++ b/src/vertex/context/banner-context.tsx
@@ -15,7 +15,7 @@ interface BannerState {
isScoped: boolean;
}
-type ShowBannerOptions = BannerDisplayProps & { scoped?: boolean };
+export type ShowBannerOptions = BannerDisplayProps & { scoped?: boolean };
interface BannerContextValue {
state: BannerState;
diff --git a/src/vertex/core/constants/ui.ts b/src/vertex/core/constants/ui.ts
new file mode 100644
index 0000000000..d413b88f97
--- /dev/null
+++ b/src/vertex/core/constants/ui.ts
@@ -0,0 +1,5 @@
+/**
+ * Delay (ms) before showing a global banner after a dialog closes.
+ * Allows the dialog's unmount cleanup (which clears scoped banners) to complete first.
+ */
+export const AFTER_DIALOG_CLOSE_MS = 100;
diff --git a/src/vertex/core/hooks/useDeferredBanner.ts b/src/vertex/core/hooks/useDeferredBanner.ts
new file mode 100644
index 0000000000..f7540f4642
--- /dev/null
+++ b/src/vertex/core/hooks/useDeferredBanner.ts
@@ -0,0 +1,23 @@
+import { useEffect, useRef } from 'react';
+import { useBanner, type ShowBannerOptions } from '@/context/banner-context';
+import { AFTER_DIALOG_CLOSE_MS } from '@/core/constants/ui';
+
+export const useDeferredBanner = () => {
+ const { showBanner } = useBanner();
+ const timerRef = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, []);
+
+ const showDeferredBanner = (options: ShowBannerOptions) => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ timerRef.current = setTimeout(() => {
+ showBanner(options);
+ }, AFTER_DIALOG_CLOSE_MS);
+ };
+
+ return { showDeferredBanner };
+};
diff --git a/src/vertex/core/hooks/useGrids.ts b/src/vertex/core/hooks/useGrids.ts
index 44fd98bf6f..88a3f2496f 100644
--- a/src/vertex/core/hooks/useGrids.ts
+++ b/src/vertex/core/hooks/useGrids.ts
@@ -9,8 +9,7 @@ import { CreateGrid, Grid, GridsSummaryResponse } from "@/app/types/grids";
import { setError, setGrids } from "../redux/slices/gridsSlice";
import { useDispatch } from "react-redux";
import React from "react";
-import ReusableToast from "@/components/shared/toast/ReusableToast";
-import { getApiErrorMessage } from "../utils/getApiErrorMessage";
+
interface ErrorResponse {
message: string;
@@ -95,8 +94,13 @@ export const useGridDetails = (gridId: string) => {
};
};
+interface UseUpdateGridDetailsOptions {
+ onSuccess?: (data: Grid) => void;
+ onError?: (error: AxiosError) => void;
+}
+
// Hook to update grid details
-export const useUpdateGridDetails = (gridId: string) => {
+export const useUpdateGridDetails = (gridId: string, options?: UseUpdateGridDetailsOptions) => {
const queryClient = useQueryClient();
const {
mutateAsync: updateGridDetails,
@@ -105,81 +109,62 @@ export const useUpdateGridDetails = (gridId: string) => {
} = useMutation, { name?: string; visibility?: boolean; admin_level?: string }>({
mutationFn: (updatedFields: { name?: string; visibility?: boolean; admin_level?: string }) =>
grids.updateGridDetailsApi(gridId, updatedFields),
- onSuccess: () => {
- ReusableToast({
- message: "Grid details updated successfully",
- type: "SUCCESS",
- });
+ onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["gridDetails", gridId] });
queryClient.invalidateQueries({ queryKey: ["grids"] });
+ options?.onSuccess?.(data);
},
onError: (error: AxiosError) => {
- ReusableToast({
- message: `Failed to update grid: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
},
});
- return {
- updateGridDetails,
- isLoading,
- error,
- };
+ return { updateGridDetails, isLoading, error };
};
+interface UseCreateGridOptions {
+ onSuccess?: (data: Grid) => void;
+ onError?: (error: AxiosError) => void;
+}
+
// Hook to create a new grid
-export const useCreateGrid = () => {
+export const useCreateGrid = (options?: UseCreateGridOptions) => {
const queryClient = useQueryClient();
const { mutate: createGrid, isPending: isLoading, error } = useMutation, CreateGrid>({
mutationFn: async (newGrid: CreateGrid) =>
await grids.createGridApi(newGrid),
onSuccess: (data) => {
- ReusableToast({
- message: `New grid added!`,
- type: "SUCCESS",
- });
queryClient.invalidateQueries({ queryKey: ["grids"] });
+ options?.onSuccess?.(data);
},
onError: (error: AxiosError) => {
- ReusableToast({
- message: `Failed to create grid: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
},
});
- return {
- createGrid,
- isLoading,
- error,
- };
+ return { createGrid, isLoading, error };
};
-export const useCreateAdminLevel = () => {
+interface UseCreateAdminLevelOptions {
+ onSuccess?: (data: AdminLevelResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useCreateAdminLevel = (options?: UseCreateAdminLevelOptions) => {
const queryClient = useQueryClient();
const { mutate: createAdminLevel, isPending: isLoading, error } = useMutation, { name: string }>({
mutationFn: (data: { name: string }) => grids.createAdminLevelApi(data),
onSuccess: (data) => {
- ReusableToast({
- message: `Admin level '${data.admin_levels.name}' created successfully`,
- type: "SUCCESS",
- });
queryClient.invalidateQueries({ queryKey: ["admin-levels"] });
+ options?.onSuccess?.(data);
},
onError: (error: AxiosError) => {
- ReusableToast({
- message: `Failed to create admin level: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
+
},
});
- return {
- createAdminLevel,
- isLoading,
- error,
- };
+ return { createAdminLevel, isLoading, error };
};
export const useAdminLevels = () => {
@@ -196,28 +181,23 @@ export const useAdminLevels = () => {
};
};
-export const useUpdateAdminLevel = () => {
+interface UseUpdateAdminLevelOptions {
+ onSuccess?: (data: AdminLevelResponse) => void;
+ onError?: (error: AxiosError) => void;
+}
+
+export const useUpdateAdminLevel = (options?: UseUpdateAdminLevelOptions) => {
const queryClient = useQueryClient();
const { mutate: updateAdminLevel, isPending: isLoading, error } = useMutation, { levelId: string; data: { name: string } }>({
mutationFn: ({ levelId, data }) => grids.updateAdminLevelApi(levelId, data),
onSuccess: (data) => {
- ReusableToast({
- message: `Admin level updated to '${data.admin_levels.name}' successfully`,
- type: "SUCCESS",
- });
queryClient.invalidateQueries({ queryKey: ["admin-levels"] });
+ options?.onSuccess?.(data);
},
onError: (error: AxiosError) => {
- ReusableToast({
- message: `Failed to update admin level: ${getApiErrorMessage(error)}`,
- type: "ERROR",
- });
+ options?.onError?.(error);
},
});
- return {
- updateAdminLevel,
- isLoading,
- error,
- };
+ return { updateAdminLevel, isLoading, error };
};
diff --git a/src/vertex/core/utils/getApiErrorMessage.ts b/src/vertex/core/utils/getApiErrorMessage.ts
index 1c410e7b0d..e70d94c070 100644
--- a/src/vertex/core/utils/getApiErrorMessage.ts
+++ b/src/vertex/core/utils/getApiErrorMessage.ts
@@ -13,6 +13,14 @@ interface ErrorWithData extends Error {
data?: ApiErrorResponse | string;
}
+const HTML_PATTERN = /<\/?[a-z][\s\S]*>/i;
+const GENERIC_ERROR = 'An unexpected error occurred. Please try again.';
+
+const fallbackIfHtml = (msg: string): string => {
+ const trimmed = msg.trim();
+ return !trimmed || HTML_PATTERN.test(trimmed) ? GENERIC_ERROR : trimmed;
+};
+
const getMessageFromApiData = (data: ApiErrorResponse | string): string | null => {
if (typeof data === 'string') {
return data;
@@ -64,21 +72,21 @@ export const getApiErrorMessage = (error: unknown): string => {
if (error.response?.data) {
const message = getMessageFromApiData(error.response.data as ApiErrorResponse | string);
- if (message) return message;
+ if (message) return fallbackIfHtml(message);
}
}
// 5. Fetch/custom errors with preserved backend payload: Object.assign(new Error(...), { data })
if (error instanceof Error && 'data' in error) {
const message = getMessageFromApiData((error as ErrorWithData).data ?? {});
- if (message) return message;
+ if (message) return fallbackIfHtml(message);
}
// 6. Fallback to standard Error message
if (error instanceof Error) {
- return error.message;
+ return fallbackIfHtml(error.message);
}
// 7. Generic fallback
- return 'An unexpected error occurred. Please try again.';
+ return GENERIC_ERROR;
};