From 8e30eda8552ac0e519be8892713cd4f8476ee0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ccaneryy=E2=80=9D?= Date: Mon, 29 Jun 2026 11:46:03 +0300 Subject: [PATCH 1/3] feat(frontend): integrate Allbridge cross-chain deposit flow in Create Plan form --- .../app/asset-owner/plans/create/page.tsx | 15 + .../components/plans/BridgeFeeBreakdown.tsx | 81 ++++ .../plans/BridgeProgressStepper.tsx | 97 +++++ .../plans/CreateInheritancePlanPanel.tsx | 351 ++++++++++++++++++ .../plans/CrossChainDepositSection.tsx | 236 ++++++++++++ frontend/context/CrossChainWalletContext.tsx | 137 +++++++ frontend/hooks/useBridgeDeposit.ts | 178 +++++++++ frontend/lib/bridge/allbridgeClient.ts | 313 ++++++++++++++++ frontend/lib/bridge/chains.ts | 115 ++++++ frontend/lib/bridge/index.ts | 5 + frontend/lib/bridge/steps.ts | 40 ++ frontend/lib/bridge/types.ts | 88 +++++ frontend/lib/bridge/wallets.ts | 133 +++++++ frontend/next.config.ts | 1 + frontend/package.json | 1 + .../components/BridgeProgressStepper.test.tsx | 27 ++ .../CreateInheritancePlanPanel.test.tsx | 92 +++++ .../tests/lib/bridge/allbridgeClient.test.ts | 48 +++ frontend/tests/setup.ts | 28 +- 19 files changed, 1985 insertions(+), 1 deletion(-) create mode 100644 frontend/app/asset-owner/plans/create/page.tsx create mode 100644 frontend/components/plans/BridgeFeeBreakdown.tsx create mode 100644 frontend/components/plans/BridgeProgressStepper.tsx create mode 100644 frontend/components/plans/CreateInheritancePlanPanel.tsx create mode 100644 frontend/components/plans/CrossChainDepositSection.tsx create mode 100644 frontend/context/CrossChainWalletContext.tsx create mode 100644 frontend/hooks/useBridgeDeposit.ts create mode 100644 frontend/lib/bridge/allbridgeClient.ts create mode 100644 frontend/lib/bridge/chains.ts create mode 100644 frontend/lib/bridge/index.ts create mode 100644 frontend/lib/bridge/steps.ts create mode 100644 frontend/lib/bridge/types.ts create mode 100644 frontend/lib/bridge/wallets.ts create mode 100644 frontend/tests/components/BridgeProgressStepper.test.tsx create mode 100644 frontend/tests/components/CreateInheritancePlanPanel.test.tsx create mode 100644 frontend/tests/lib/bridge/allbridgeClient.test.ts diff --git a/frontend/app/asset-owner/plans/create/page.tsx b/frontend/app/asset-owner/plans/create/page.tsx new file mode 100644 index 000000000..d158a43fd --- /dev/null +++ b/frontend/app/asset-owner/plans/create/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { KYCRequiredGuard } from "@/components/kyc/KYCRequiredGuard"; +import { CrossChainWalletProvider } from "@/context/CrossChainWalletContext"; +import { CreateInheritancePlanPanel } from "@/components/plans/CreateInheritancePlanPanel"; + +export default function CreatePlanPage() { + return ( + + + + + + ); +} diff --git a/frontend/components/plans/BridgeFeeBreakdown.tsx b/frontend/components/plans/BridgeFeeBreakdown.tsx new file mode 100644 index 000000000..079cd5ac5 --- /dev/null +++ b/frontend/components/plans/BridgeFeeBreakdown.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Loader2 } from "lucide-react"; +import type { BridgeQuote } from "@/lib/bridge/types"; + +interface BridgeFeeBreakdownProps { + quote: BridgeQuote | null; + isLoading?: boolean; +} + +function formatUsd(value: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + }).format(value); +} + +export function BridgeFeeBreakdown({ quote, isLoading }: BridgeFeeBreakdownProps) { + if (isLoading) { + return ( +
+ +

Fetching Allbridge route and fees…

+
+ ); + } + + if (!quote) { + return ( +
+

+ Enter a deposit amount to see bridging fees and estimated delivery time. +

+
+ ); + } + + const rows = [ + { label: "Relayer fee", value: quote.relayerFeeUsd }, + { label: "Source chain gas", value: quote.gasFeeUsd }, + { label: "Destination fee", value: quote.destinationFeeUsd }, + ]; + + return ( +
+
+

+ Bridging Fee Breakdown +

+ + ~{quote.estimatedDurationMinutes} min via Allbridge + +
+ +
+ {rows.map((row) => ( +
+
{row.label}
+
{formatUsd(row.value)}
+
+ ))} +
+
Total fees
+
+ {formatUsd(quote.totalFeeUsd)} +
+
+
+ +
+ Estimated receive on Stellar:{" "} + + {quote.estimatedReceiveAmount} {quote.estimatedReceiveSymbol} + +
+
+ ); +} + +export default BridgeFeeBreakdown; diff --git a/frontend/components/plans/BridgeProgressStepper.tsx b/frontend/components/plans/BridgeProgressStepper.tsx new file mode 100644 index 000000000..a29ba4201 --- /dev/null +++ b/frontend/components/plans/BridgeProgressStepper.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Check, Circle, Loader2, X } from "lucide-react"; +import type { BridgeStep, BridgeStepStatus } from "@/lib/bridge/types"; + +interface BridgeProgressStepperProps { + steps: BridgeStep[]; +} + +function StepIcon({ status }: { status: BridgeStepStatus }) { + if (status === "completed") { + return ( + + + + ); + } + if (status === "active") { + return ( + + + + ); + } + if (status === "error") { + return ( + + + + ); + } + return ( + + + + ); +} + +function statusLabel(status: BridgeStepStatus): string { + switch (status) { + case "completed": + return "Completed"; + case "active": + return "In progress"; + case "error": + return "Failed"; + default: + return "Pending"; + } +} + +export function BridgeProgressStepper({ steps }: BridgeProgressStepperProps) { + return ( +
    + {steps.map((step, index) => ( +
  1. + {index < steps.length - 1 && ( +
  2. + ))} +
+ ); +} + +export default BridgeProgressStepper; diff --git a/frontend/components/plans/CreateInheritancePlanPanel.tsx b/frontend/components/plans/CreateInheritancePlanPanel.tsx new file mode 100644 index 000000000..d5bed0d71 --- /dev/null +++ b/frontend/components/plans/CreateInheritancePlanPanel.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + AlertCircle, + CheckCircle, + Loader2, + Plus, + Trash2, +} from "lucide-react"; +import { plansAPI } from "@/app/lib/api/plans"; +import type { Beneficiary } from "@/app/lib/api/plans"; +import { useWallet } from "@/context/WalletContext"; +import { CrossChainDepositSection } from "./CrossChainDepositSection"; + +const DEFAULT_BENEFICIARY: Omit = { + wallet_address: "", + name: "", + allocation_percentage: 100, +}; + +function totalAllocation(beneficiaries: Beneficiary[]): number { + return beneficiaries.reduce( + (sum, b) => sum + (b.allocation_percentage || 0), + 0 + ); +} + +function isAllocationValid(beneficiaries: Beneficiary[]): boolean { + const total = totalAllocation(beneficiaries); + return ( + total === 100 && + beneficiaries.every((b) => (b.allocation_percentage || 0) > 0) + ); +} + +type SubmitStatus = "idle" | "creating" | "success" | "error"; + +export function CreateInheritancePlanPanel() { + const { isConnected, openModal } = useWallet(); + + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [depositAmount, setDepositAmount] = useState(""); + const [inactivityDays, setInactivityDays] = useState(180); + const [beneficiaries, setBeneficiaries] = useState([ + { ...DEFAULT_BENEFICIARY }, + ]); + const [bridgeTransferId, setBridgeTransferId] = useState(null); + const [status, setStatus] = useState("idle"); + const [errorMessage, setErrorMessage] = useState(""); + + const allocationTotal = totalAllocation(beneficiaries); + const allocationOk = isAllocationValid(beneficiaries); + const parsedDeposit = Number.parseFloat(depositAmount) || 0; + + const handleBeneficiaryChange = useCallback( + (index: number, field: keyof Beneficiary, value: string | number) => { + setBeneficiaries((prev) => + prev.map((b, i) => (i === index ? { ...b, [field]: value } : b)) + ); + }, + [] + ); + + const addBeneficiary = () => { + setBeneficiaries((prev) => [...prev, { ...DEFAULT_BENEFICIARY }]); + }; + + const removeBeneficiary = (index: number) => { + setBeneficiaries((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleBridgeComplete = useCallback((transferId: string) => { + setBridgeTransferId(transferId); + }, []); + + const handleCreatePlan = async () => { + if (!title.trim() || !allocationOk || parsedDeposit <= 0) return; + + if (!isConnected) { + openModal(); + return; + } + + if (!bridgeTransferId) { + setErrorMessage( + "Complete the cross-chain deposit before creating the plan." + ); + return; + } + + setErrorMessage(""); + setStatus("creating"); + + try { + const feeEstimate = parsedDeposit * 0.01; + await plansAPI.createPlan({ + title: title.trim(), + description: description.trim() || undefined, + fee: feeEstimate, + net_amount: parsedDeposit - feeEstimate, + currency_preference: "USD", + two_fa_code: "000000", + beneficiary_name: beneficiaries[0]?.name, + }); + setStatus("success"); + } catch (error) { + setStatus("error"); + setErrorMessage( + error instanceof Error ? error.message : "Failed to create plan." + ); + } + }; + + return ( +
+
+

Create Plan

+

+ Lock cross-chain assets into a new inheritance plan on Stellar. +

+
+ +
+
+
+

+ Plan Details +

+
+
+ + setTitle(e.target.value)} + placeholder="Family Trust Plan" + className="bg-[#0A0F11] border border-[#2A3338] rounded-lg px-4 py-2.5 text-sm text-slate-200 placeholder-[#4A5568] focus:outline-none focus:border-[#33C5E0] transition-colors" + /> +
+
+ +