+
+
+
+
{transaction.name}
+
+ {transaction.accountName} · {formatDateShort(transaction.date)}
+
+
+
+
+
+
+
+ {isIncome
+ ? "This looks like it could be money moving in from another account rather than income — is it a transfer?"
+ : "This looks like it could be money moving to another account rather than a purchase — is it a transfer?"}
+
+
+ );
+}
diff --git a/src/components/molecules/transfer-review-nudge.tsx b/src/components/molecules/transfer-review-nudge.tsx
new file mode 100644
index 00000000..804140e2
--- /dev/null
+++ b/src/components/molecules/transfer-review-nudge.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useSyncExternalStore } from "react";
+import Link from "next/link";
+import { X, ArrowLeftRight } from "lucide-react";
+import { Button, buttonVariants } from "@/components/ui/button";
+import { Alert, AlertTitle, AlertDescription, AlertAction } from "@/components/ui/alert";
+import { cn } from "@/lib/utils";
+
+const DISMISS_KEY = "ledgr:transfer-review-nudge-dismissed";
+const DISMISS_EVENT = "ledgr:transfer-review-nudge-dismiss";
+
+// Same useSyncExternalStore approach as ReviewNudge, for the same reason:
+// sessionStorage doesn't exist during SSR, so the server snapshot is always
+// "not dismissed" and React reconciles to the real value on hydration.
+function subscribe(onChange: () => void) {
+ window.addEventListener(DISMISS_EVENT, onChange);
+ return () => window.removeEventListener(DISMISS_EVENT, onChange);
+}
+
+let dismissedInMemory = false;
+
+function isDismissed() {
+ if (dismissedInMemory) return true;
+ try {
+ return sessionStorage.getItem(DISMISS_KEY) === "1";
+ } catch {
+ return false;
+ }
+}
+
+function notDismissedOnServer() {
+ return false;
+}
+
+interface TransferReviewNudgeProps {
+ /** Single-leg transfer suggestions awaiting confirmation. Does not render at zero. */
+ suggestedCount: number;
+}
+
+export function TransferReviewNudge({ suggestedCount }: TransferReviewNudgeProps) {
+ const dismissed = useSyncExternalStore(subscribe, isDismissed, notDismissedOnServer);
+
+ function dismiss() {
+ dismissedInMemory = true;
+ try {
+ sessionStorage.setItem(DISMISS_KEY, "1");
+ } catch {
+ // Storage unavailable. The event below still hides it for this view.
+ }
+ window.dispatchEvent(new Event(DISMISS_EVENT));
+ }
+
+ if (suggestedCount === 0 || dismissed) return null;
+
+ return (
+