+ {request.refunds.paid_on_approval
+ ? `Approving pays back ${request.refunds.paid_on_approval} victim${request.refunds.paid_on_approval > 1 ? "s" : ""}, ${taka(request.refunds.paid_on_approval_claimed)} claimed`
+ : `${request.refunds.open} refund claim${request.refunds.open > 1 ? "s" : ""} waiting for a verdict`}
+
+ )}
+
{request.requester}
{when(request.created_at)}
@@ -129,7 +138,9 @@ export default function ApprovalsPage() {
you asked for this
) : (
-
+
)}
@@ -149,7 +160,11 @@ export default function ApprovalsPage() {
title={dialog.title}
label="Decision note"
intro={dialog.approve
- ? "An approved freeze stops the wallet from sending or receiving until a supervisor lifts it."
+ ? `An approved freeze stops the wallet from sending or receiving until a supervisor lifts it.${
+ dialog.requests.some((request) => request.refunds?.paid_on_approval)
+ ? " The fraud is already confirmed, so the victims who reported it are refunded from what is left in the wallet in the same step."
+ : ""
+ }`
: "The wallet stays as it is. The requester sees your note."}
confirm={dialog.approve ? "Approve freeze" : "Reject"}
variant={dialog.approve ? "danger" : "primary"}
diff --git a/frontend/app/(console)/phone/copy.ts b/frontend/app/(console)/phone/copy.ts
index de049c8..67c9206 100644
--- a/frontend/app/(console)/phone/copy.ts
+++ b/frontend/app/(console)/phone/copy.ts
@@ -171,7 +171,7 @@ export const T = {
newPayment: { bn: "নতুন লেনদেন", en: "New payment" },
reportForRefund: { bn: "প্রতারিত হয়েছেন? অভিযোগ করে টাকা ফেরত চান", en: "Scammed? Report it to claim your money back" },
refundTitle: { bn: "টাকা ফেরতের অবস্থা", en: "Your refund" },
- refundBy: { bn: "যত দেরিতেই হোক, জানাব", en: "We'll tell you by" },
+ refundBy: { bn: "উত্তর পাবেন সর্বশেষ", en: "We'll tell you by" },
refundNoFee: {
bn: "টাকা ফেরত পেতে কাউকে পিন, ওটিপি বা কোনো ফি দিতে হয় না। কেউ চাইলে সে প্রতারক।",
en: "You never give a PIN, OTP or fee to get a refund. Anyone who asks is a scammer.",
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 7f647f9..5933c46 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -7,11 +7,17 @@ export const metadata: Metadata = {
title: { default: "FraudLens", template: "%s · FraudLens" },
description: "Real-time fraud decisions, investigation and model oversight for mobile money.",
robots: { index: false, follow: false },
+ // Added to an iPhone's home screen, the console opens full screen like an app.
+ appleWebApp: { capable: true, title: "FraudLens", statusBarStyle: "black-translucent" },
};
export const viewport: Viewport = {
themeColor: "#1b1b1b",
colorScheme: "dark",
+ width: "device-width",
+ initialScale: 1,
+ // Draw under the notch and the home indicator; the bars pad themselves with the safe areas.
+ viewportFit: "cover",
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
diff --git a/frontend/components/mobile-nav.tsx b/frontend/components/mobile-nav.tsx
new file mode 100644
index 0000000..4b8c445
--- /dev/null
+++ b/frontend/components/mobile-nav.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import Link from "next/link";
+import { type ReactNode, useEffect, useRef } from "react";
+
+import { words } from "@/lib/format";
+import { current, isActive, NAV, type NavItem, slug, tabsFor } from "@/lib/nav";
+import type { Me } from "@/lib/types";
+
+import { TourButton } from "./tour/tour";
+import { cx } from "./ui";
+
+export function Glyph({ d, className }: { d: string; className?: string }) {
+ return (
+
+ );
+}
+
+const MORE = "M5 12h.01M12 12h.01M19 12h.01";
+
+/** The phone's title bar: the page's name in the middle, as an iOS navigation bar has it. */
+export function TopBar({ pathname }: { pathname: string }) {
+ const page = current(pathname);
+ return (
+
+
+
+
+ FraudLens
+
+ {page?.short ?? ""}
+
+
+
+ );
+}
+
+/** The bottom tab bar: the role's four most-used pages and More. Stays under the tour's overlay (z-100). */
+export function TabBar({ me, pathname, moreOpen, onMore }: { me: Me; pathname: string; moreOpen: boolean; onMore: () => void }) {
+ const tabs = tabsFor(me.role);
+ const onTab = tabs.some((tab) => isActive(pathname, tab.href));
+ const item = "flex min-h-[3.125rem] flex-1 flex-col items-center justify-center gap-0.5 text-[0.625rem] font-medium tracking-wide active:scale-95";
+ return (
+
+ );
+}
+
+function Row({ item, on, onPick }: { item: NavItem; on: boolean; onPick: () => void }) {
+ return (
+