diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8f54c0b..1db4193 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import type { RiskAssessment } from "@custos/core"; -import { requiresFriction, requiresHardBlock } from "@custos/core"; import { analyzeSendIntent } from "./compositionRoot.js"; +import { RiskResult } from "./RiskResult.js"; export function App() { const [address, setAddress] = useState(""); @@ -62,20 +62,7 @@ export function App() { {busy ? "Analyzing on-device..." : "Check before sending"} - {assessment && ( -
- Risk level: {assessment.level} -

{assessment.summary}

- {requiresHardBlock(assessment.level) &&

This send would be blocked — WDK signing must not proceed.

} -
- )} + {assessment && } ); } diff --git a/apps/web/src/RiskResult.tsx b/apps/web/src/RiskResult.tsx new file mode 100644 index 0000000..7267653 --- /dev/null +++ b/apps/web/src/RiskResult.tsx @@ -0,0 +1,27 @@ +import type { RiskAssessment } from "@custos/core"; +import { requiresHardBlock } from "@custos/core"; +import { describeRiskLevel, describeScamCategory } from "./riskPresentation.js"; + +export function RiskResult({ assessment }: { assessment: RiskAssessment }) { + const { label, color, icon } = describeRiskLevel(assessment.level); + + return ( +
+ + {icon} {label} + +

{assessment.summary}

+ {assessment.matches.length > 0 && ( + + )} + {requiresHardBlock(assessment.level) &&

This send would be blocked — WDK signing must not proceed.

} +
+ ); +} diff --git a/apps/web/src/riskPresentation.ts b/apps/web/src/riskPresentation.ts new file mode 100644 index 0000000..5717754 --- /dev/null +++ b/apps/web/src/riskPresentation.ts @@ -0,0 +1,39 @@ +import { RiskLevel } from "@custos/core"; +import type { ScamCategory } from "@custos/core"; + +export interface RiskLevelPresentation { + readonly label: string; + readonly color: string; + readonly icon: string; +} + +const RISK_LEVEL_PRESENTATION: Record = { + [RiskLevel.None]: { label: "No known risk", color: "#27ae60", icon: "✅" }, + [RiskLevel.Low]: { label: "Low — minor signals detected", color: "#16a085", icon: "ℹ️" }, + [RiskLevel.Elevated]: { label: "Elevated — review before sending", color: "#e67e22", icon: "⚠️" }, + [RiskLevel.High]: { label: "High — strong scam signals", color: "#d35400", icon: "🔶" }, + [RiskLevel.Critical]: { label: "Critical — send blocked", color: "#c0392b", icon: "🛑" }, +}; + +export function describeRiskLevel(level: RiskLevel): RiskLevelPresentation { + return RISK_LEVEL_PRESENTATION[level]; +} + +const SCAM_CATEGORY_LABEL: Record = { + "pig-butchering": "Pig-butchering scam", + "fake-support": "Fake support impersonation", + "recovery-scam": "Fund recovery scam", + "romance-scam": "Romance scam", + "fake-investment-platform": "Fake investment platform", + "urgency-pressure": "Urgency/pressure tactics", + "address-poisoning": "Lookalike address (poisoning attempt)", + "task-scam": "Task/job scam", + "impersonation-authority": "Authority impersonation", + "airdrop-claim-fee": "Fake airdrop claim fee", + "otc-escrow-scam": "OTC escrow scam", + "phishing-approval-drain": "Phishing / approval drain", +}; + +export function describeScamCategory(category: ScamCategory): string { + return SCAM_CATEGORY_LABEL[category]; +}