From 2e0c1feb01e64c0a5a557f2010b09ebfa1729eaf Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Wed, 17 Jun 2026 23:28:31 +0100 Subject: [PATCH 1/4] https://github.com/Menjay7/Remitwise-Frontend.git --- app/emergency-transfer/page.tsx | 499 ++++++++++++++++++++++++++++++-- 1 file changed, 469 insertions(+), 30 deletions(-) diff --git a/app/emergency-transfer/page.tsx b/app/emergency-transfer/page.tsx index 9960d87..6659be3 100644 --- a/app/emergency-transfer/page.tsx +++ b/app/emergency-transfer/page.tsx @@ -1,7 +1,91 @@ +"use client"; + +import { useState } from "react"; import Link from "next/link"; -import { ArrowLeft, AlertTriangle, Zap } from "lucide-react"; +import { useToast } from "@/lib/context/ToastContext"; +import { + ArrowLeft, + AlertTriangle, + Zap, + Users, + Clock, + DollarSign, + Check, + ArrowRight, + Shield, +} from "lucide-react"; +import TransactionSuccessReceipt from "@/components/TransactionSuccessReceipt"; + +type Step = "recipient" | "amount" | "review" | "confirm"; export default function EmergencyTransferPage() { + const [step, setStep] = useState("recipient"); + const [recipientName, setRecipientName] = useState(""); + const [recipientAddress, setRecipientAddress] = useState(""); + const [amount, setAmount] = useState(0); + const [currency, setCurrency] = useState("USDC"); + const [speed, setSpeed] = useState<"emergency" | "regular">("emergency"); + const [confirmedUrgent, setConfirmedUrgent] = useState(false); + const [confirmedFee, setConfirmedFee] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [transactionData, setTransactionData] = useState(null); + const { toast } = useToast(); + + const priorityFee = speed === "emergency" ? 2.0 : 0.0; + const total = amount + priorityFee; + + const handleRecipientContinue = () => { + if (recipientName && recipientAddress) { + setStep("amount"); + } + }; + + const handleAmountReview = () => { + if (amount > 0) { + setStep("review"); + } + }; + + const handleReviewConfirm = () => { + setStep("confirm"); + }; + + const handleFinalConfirm = () => { + const mockData = { + hash: "GCF27P3Q" + Math.random().toString(36).substring(2, 15).toUpperCase(), + amount: amount, + currency: currency, + recipientName: recipientName, + recipientAddress: recipientAddress, + date: new Date().toLocaleString(), + fee: priorityFee, + speed: speed, + splits: { + dailySpending: amount * 0.5, + savings: amount * 0.3, + bills: amount * 0.15, + insurance: amount * 0.05, + }, + }; + + setTransactionData(mockData); + setIsSubmitted(true); + toast({ + variant: "success", + title: "Emergency transfer submitted", + description: `Successfully sent ${amount} ${currency} to ${recipientName}. Funds will arrive in ${speed === "emergency" ? "2-5 minutes" : "30-60 minutes"}.`, + }); + }; + + if (isSubmitted && transactionData) { + return ( + setIsSubmitted(false)} + /> + ); + } + return (
{/* Header */} @@ -14,43 +98,398 @@ export default function EmergencyTransferPage() { > -

- Emergency Transfer -

+
+
+ +
+

+ Emergency Transfer +

+
-
-
-
- -
-

- Emergency Transfer Feature -

-

- This feature is currently under development. Emergency transfers - will allow you to send money instantly to your family members in - urgent situations with priority processing. -

-
- - Back to Dashboard - - - Use Regular Transfer - + {/* Progress Indicator */} +
+
+
+ + {/* Step 1 */} +
+
+ 1 +
+ Recipient +
+ + {/* Step 2 */} +
+
+ 2 +
+ Amount +
+ + {/* Step 3 */} +
+
+ 3 +
+ Review +
+ + {/* Step 4 */} +
+
+ 4 +
+ Confirm +
+
+
+ + {/* Warning Banner */} +
+
+ +
+ + Emergency Transfer Warning + + + Emergency transfers are processed immediately and{" "} + cannot be reversed. + A ${priorityFee.toFixed(2)} priority fee applies. Only use this for urgent situations. +
+ + {/* Step Content */} +
+ {step === "recipient" && ( +
+

Who are you sending to?

+ +
+
+ + setRecipientName(e.target.value)} + placeholder="Enter recipient name" + className="w-full rounded-2xl border border-zinc-800 bg-zinc-900/40 p-4 text-white placeholder:text-zinc-600 focus:border-red-500/50 focus:ring-1 focus:ring-red-500/50 outline-none transition-all" + /> +
+ +
+ + setRecipientAddress(e.target.value)} + placeholder="GXXXXXXXXXXXXXXXXXXXXXXXX" + className="w-full rounded-2xl border border-zinc-800 bg-zinc-900/40 p-4 text-white placeholder:text-zinc-600 focus:border-red-500/50 focus:ring-1 focus:ring-red-500/50 outline-none transition-all font-mono" + /> +
+
+ +
+ + Cancel + + +
+
+ )} + + {step === "amount" && ( +
+

How much to send?

+ +
+
+ +
+ setAmount(Number(e.target.value))} + placeholder="0.00" + className="w-full rounded-2xl border border-zinc-800 bg-zinc-900/40 p-4 pr-20 text-white placeholder:text-zinc-600 focus:border-red-500/50 focus:ring-1 focus:ring-red-500/50 outline-none transition-all font-semibold text-2xl" + /> + + USDC + +
+
+ +
+ + + +
+ + {amount > 0 && ( +
+
+ Transfer Amount + + {amount.toLocaleString()} USDC + +
+
+ Priority Fee + + +{priorityFee.toFixed(2)} USDC + +
+
+ Total + + {total.toLocaleString(undefined, { minimumFractionDigits: 2 })}{" "} + USDC + +
+
+ )} +
+ +
+ + +
+
+ )} + + {step === "review" && ( +
+

Review your transfer

+ +
+
+
+ Recipient + {recipientName} +
+
+ Address + + {recipientAddress.slice(0, 8)}...{recipientAddress.slice(-4)} + +
+
+ Transfer Amount + + {amount.toLocaleString()} USDC + +
+
+ Speed + + {speed === "emergency" ? "Emergency (2-5 min)" : "Regular (30-60 min)"} + +
+
+ Priority Fee + + +{priorityFee.toFixed(2)} USDC + +
+
+ Total + + {total.toLocaleString(undefined, { minimumFractionDigits: 2 })}{" "} + USDC + +
+
+ +
+
+ +
+ + Final Confirmation Required + + + This transfer cannot be reversed once submitted. Please verify all details are correct. + +
+
+
+
+ +
+ + +
+
+ )} + + {step === "confirm" && ( +
+
+
+ +
+
+

Final Confirmation

+

+ {speed === "emergency" ? "Emergency transfer - funds arrive in 2-5 minutes" : "Regular transfer - funds arrive in 30-60 minutes"} +

+
+
+ +
+
+
+ Sending to + {recipientName} +
+
+ Total Amount + + {total.toLocaleString(undefined, { minimumFractionDigits: 2 })}{" "} + USDC + +
+
+ + + + +
+ +
+ + +
+
+ )} +
); From 7a13adb02810dbfaf85ae92fc7131a9405749c48 Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Wed, 17 Jun 2026 23:44:58 +0100 Subject: [PATCH 2/4] feat: build out emergency transfer page with confirm-guarded flow --- components/dashboard/SixMonthTrendsWidget.tsx | 256 +++++++++++------- 1 file changed, 160 insertions(+), 96 deletions(-) diff --git a/components/dashboard/SixMonthTrendsWidget.tsx b/components/dashboard/SixMonthTrendsWidget.tsx index a63684d..43955a2 100644 --- a/components/dashboard/SixMonthTrendsWidget.tsx +++ b/components/dashboard/SixMonthTrendsWidget.tsx @@ -1,12 +1,10 @@ 'use client' -import { useCallback, useState } from 'react' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts' import { TrendingUp, Target, FileText } from 'lucide-react' -import WidgetEmptyState from '@/components/ui/WidgetEmptyState' -import WidgetErrorState from '@/components/ui/WidgetErrorState' -const mockChartData = [ +// Sample data for the 6-month chart (Jul-Dec) +const chartData = [ { month: 'Jul', remittances: 2800, savings: 1200, bills: 420, insurance: 80 }, { month: 'Aug', remittances: 3050, savings: 1350, bills: 400, insurance: 80 }, { month: 'Sep', remittances: 3200, savings: 1400, bills: 380, insurance: 80 }, @@ -15,6 +13,7 @@ const mockChartData = [ { month: 'Dec', remittances: 3400, savings: 1600, bills: 550, insurance: 80 }, ] +// Color scheme matching Figma design const COLORS = { remittances: '#DC2626', savings: '#B91C1C', @@ -23,7 +22,10 @@ const COLORS = { } interface CustomLegendProps { - payload?: Array<{ value: string; color: string }> + payload?: Array<{ + value: string + color: string + }> } function CustomLegend({ payload }: CustomLegendProps) { @@ -31,8 +33,14 @@ function CustomLegend({ payload }: CustomLegendProps) {
{payload?.map((entry, index) => (
-
- +
+ {entry.value}
@@ -52,6 +60,7 @@ interface SummaryCardProps { function SummaryCard({ icon, label, value, subtitle, variant = 'default', valueColor }: SummaryCardProps) { const isHighlight = variant === 'highlight' + return (
+ {/* Label row */}
-
{icon}
- +
+ {icon} +
+ {label}
-
+ + {/* Value */} +
{value}
-
{subtitle}
+ + {/* Subtitle */} +
+ {subtitle} +
) } -interface SixMonthTrendsWidgetProps { - /** Pass an empty array to show the empty state */ - chartData?: typeof mockChartData - /** Pass true to show the error state */ - hasError?: boolean -} - -export default function SixMonthTrendsWidget({ - chartData = mockChartData, - hasError = false, -}: SixMonthTrendsWidgetProps) { - const [retryKey, setRetryKey] = useState(0) - const handleRetry = useCallback(() => setRetryKey((k) => k + 1), []) - - const isEmpty = !hasError && chartData.length === 0 - +export default function SixMonthTrendsWidget() { return (
{/* Header */}
-

6-Month Trends

+

+ 6-Month Trends +

Track your financial patterns

- {!isEmpty && !hasError && ( - - )} + + {/* View Details Button */} +
- {hasError ? ( - - ) : isEmpty ? ( - - ) : ( - <> - {/* Line Chart */} -
- - - - - `$${value}`} - domain={[0, 3400]} - ticks={[0, 850, 1700, 2550, 3400]} - width={45} - /> - [`$${Number(value).toLocaleString()}`, '']} - /> - } /> - - - - - - -
+ {/* Line Chart - 320px height */} +
+ + + + + `$${value}`} + domain={[0, 3400]} + ticks={[0, 850, 1700, 2550, 3400]} + width={45} + /> + [`$${Number(value).toLocaleString()}`, '']} + /> + } /> + + + + + + +
- {/* Summary Cards */} -
-
- } label="Highest Month" value="Dec 2025" subtitle="$5,630 total" variant="highlight" /> - } label="Average" value="$5,395" subtitle="Per month" /> - } label="Growth" value="+15.7%" subtitle="vs. Jul 2025" valueColor="#DC2626" /> -
-
- - )} + {/* Summary Cards Section */} +
+
+ } + label="Highest Month" + value="Dec 2025" + subtitle="$5,630 total" + variant="highlight" + /> + } + label="Average" + value="$5,395" + subtitle="Per month" + /> + } + label="Growth" + value="+15.7%" + subtitle="vs. Jul 2025" + valueColor="#DC2626" + /> +
+
) } From 38c0605d788d5bb4e5be1bda516b753f5bb4c87e Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Thu, 18 Jun 2026 00:09:42 +0100 Subject: [PATCH 3/4] https://github.com/Remitwise-Org/Remitwise-Frontend.git --- app/layout.tsx | 15 +- .../components/EmergencyTransferModal.tsx | 90 +++++---- components/AsyncOperationsPanel.tsx | 92 ++++++--- lib/context/ContractOperationsContext.tsx | 174 ++++++++++++++++++ 4 files changed, 311 insertions(+), 60 deletions(-) create mode 100644 lib/context/ContractOperationsContext.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 8810df9..52fce09 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import LayoutWrapper from "@/components/LayoutWrapper"; import { DensityProvider } from "@/lib/context/DensityContext"; import { ToastProvider } from "@/lib/context/ToastContext"; +import { ContractOperationsProvider } from "@/lib/context/ContractOperationsContext"; import ToastRegion from "@/components/ToastRegion"; import SessionExpiryProvider from "@/components/SessionExpiryProvider"; @@ -25,12 +26,14 @@ export default function RootLayout({ - - - {children} - - - + + + + {children} + + + + diff --git a/app/send/components/EmergencyTransferModal.tsx b/app/send/components/EmergencyTransferModal.tsx index e93f76a..807f687 100644 --- a/app/send/components/EmergencyTransferModal.tsx +++ b/app/send/components/EmergencyTransferModal.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { ArrowRight, Clock3, @@ -13,6 +13,7 @@ import { } from 'lucide-react' import AsyncOperationsPanel from '@/components/AsyncOperationsPanel' import AsyncSubmissionStatus from '@/components/AsyncSubmissionStatus' +import { useContractOperations } from '@/lib/context/ContractOperationsContext' interface EmergencyTransferModalProps { isOpen: boolean @@ -54,30 +55,6 @@ const emergencyStages = [ }, ] -const emergencyQueue = [ - { - title: 'Emergency transfer submission', - duration: 'Live', - detail: - 'The active transfer keeps the strongest visual treatment and remains pinned while waiting on network confirmation.', - status: 'active' as const, - }, - { - title: 'Wallet approval waiting', - duration: 'Queued', - detail: - 'If another transfer is in flight, collapse it into a smaller stacked card instead of replacing the active item.', - status: 'queued' as const, - }, - { - title: 'Transfer confirmed', - duration: '< 1 min', - detail: - 'Leave the final state visible briefly so users can trust the result before moving on.', - status: 'complete' as const, - }, -] - export default function EmergencyTransferModal({ isOpen, onClose, @@ -85,6 +62,57 @@ export default function EmergencyTransferModal({ const [confirmed, setConfirmed] = useState(false) const [amount, setAmount] = useState('') const [speed, setSpeed] = useState<'emergency' | 'regular'>('emergency') + const [isSubmitting, setIsSubmitting] = useState(false) + + const { addOperation, updateOperation } = useContractOperations() + const [currentOperationId, setCurrentOperationId] = useState(null) + + // Simulate contract operation lifecycle + useEffect(() => { + if (isSubmitting && !currentOperationId) { + const opId = addOperation({ + type: 'emergency_transfer', + title: 'Emergency transfer submission', + status: 'pending', + detail: 'The active transfer keeps the strongest visual treatment and remains pinned while waiting on network confirmation.', + duration: 'Live', + metadata: { amount, speed }, + }) + setCurrentOperationId(opId) + + // Simulate the operation lifecycle + setTimeout(() => { + updateOperation(opId, { status: 'building' }) + }, 1000) + + setTimeout(() => { + updateOperation(opId, { status: 'signing' }) + }, 3000) + + setTimeout(() => { + updateOperation(opId, { status: 'submitting' }) + }, 5000) + + setTimeout(() => { + updateOperation(opId, { status: 'confirming' }) + }, 8000) + + setTimeout(() => { + updateOperation(opId, { + status: 'complete', + transactionHash: 'GCF27P3Q' + Math.random().toString(36).substring(2, 15).toUpperCase() + }) + setIsSubmitting(false) + setCurrentOperationId(null) + }, 12000) + } + }, [isSubmitting, currentOperationId, addOperation, updateOperation, amount, speed]) + + const handleSubmit = () => { + if (confirmed && amount) { + setIsSubmitting(true) + } + } if (!isOpen) return null @@ -265,10 +293,11 @@ export default function EmergencyTransferModal({
@@ -276,13 +305,8 @@ export default function EmergencyTransferModal({
diff --git a/components/AsyncOperationsPanel.tsx b/components/AsyncOperationsPanel.tsx index e594db1..65f4d72 100644 --- a/components/AsyncOperationsPanel.tsx +++ b/components/AsyncOperationsPanel.tsx @@ -12,6 +12,7 @@ import { type LucideIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; +import { useContractOperations, type ContractOperation } from "@/lib/context/ContractOperationsContext"; type AsyncStage = { label: string; @@ -19,6 +20,7 @@ type AsyncStage = { detail: string; placement: string; icon?: LucideIcon; + status?: "pending" | "active" | "complete"; }; type QueueItem = { @@ -29,14 +31,15 @@ type QueueItem = { }; interface AsyncOperationsPanelProps { - eyebrow: string; - title: string; - description: string; - stages: AsyncStage[]; - queueTitle: string; - queueDescription: string; - queueItems: QueueItem[]; + eyebrow?: string; + title?: string; + description?: string; + stages?: AsyncStage[]; + queueTitle?: string; + queueDescription?: string; + queueItems?: QueueItem[]; footer?: string; + useLiveContext?: boolean; } const queueStatusStyles = { @@ -72,31 +75,78 @@ const queueStatusStyles = { } as const; export default function AsyncOperationsPanel({ - eyebrow, - title, - description, + eyebrow = "Async behavior", + title = "Emergency Submission Pattern", + description = "Urgent transfers carry the highest stakes in the product. Each stage below shows what the user sees and how long to expect it to take.", stages, - queueTitle, - queueDescription, + queueTitle = "Stack behavior", + queueDescription = "The modal owns the review and build stages. After wallet signature, progress moves into the global stack so it stays visible if the user navigates away.", queueItems, footer, + useLiveContext = false, }: AsyncOperationsPanelProps) { const [expanded, setExpanded] = useState(false); const [openIndex, setOpenIndex] = useState(null); - const activeIndex = useMemo(() => queueItems.findIndex((i) => i.status === "active"), [queueItems]); + // Use live context data when enabled + const contractOps = useLiveContext ? useContractOperations() : null; + + // Convert contract operations to queue items format + const liveQueueItems = useMemo(() => { + if (!contractOps || !useLiveContext) return queueItems || []; + + return contractOps.operations.map((op): QueueItem => { + const statusMap: Record = { + pending: "queued", + building: "active", + signing: "active", + submitting: "active", + confirming: "active", + complete: "complete", + failed: "failed", + }; + + const durationMap: Partial> = { + pending: "Queued", + building: "Building...", + signing: "Signing...", + submitting: "Submitting...", + confirming: "Confirming...", + complete: "< 1 min", + failed: "Failed", + }; + + return { + title: op.title, + duration: durationMap[op.status] || op.duration, + detail: op.detail, + status: statusMap[op.status], + }; + }); + }, [contractOps, useLiveContext, queueItems]); + + // Use live stages when context is enabled + const liveStages = useMemo(() => { + if (!contractOps || !useLiveContext) return stages; + return contractOps.stages; + }, [contractOps, useLiveContext, stages]); + + const finalQueueItems = liveQueueItems || []; + const finalStages = liveStages || []; + + const activeIndex = useMemo(() => finalQueueItems.findIndex((i: QueueItem) => i.status === "active"), [finalQueueItems]); useEffect(() => { // Close open detail if item list changes and index no longer valid - if (openIndex !== null && openIndex >= queueItems.length) setOpenIndex(null); - }, [queueItems, openIndex]); + if (openIndex !== null && openIndex >= finalQueueItems.length) setOpenIndex(null); + }, [finalQueueItems, openIndex]); // Live announcement for major state: active started/completed/failed const [liveText, setLiveText] = useState(""); useEffect(() => { - const active = queueItems[activeIndex]; + const active = finalQueueItems[activeIndex]; if (active) setLiveText(`${active.title} ${active.status}`); - }, [queueItems, activeIndex]); + }, [finalQueueItems, activeIndex]); return (
@@ -108,7 +158,7 @@ export default function AsyncOperationsPanel({
- {stages.map((stage, index) => { + {finalStages.map((stage: AsyncStage, index: number) => { const StageIcon = stage.icon ?? (index < 2 ? ShieldCheck : Wallet); return ( @@ -156,11 +206,11 @@ export default function AsyncOperationsPanel({

- {queueItems.length} total + {finalQueueItems.length} total
- {queueItems.map((item, index) => { + {finalQueueItems.map((item: QueueItem, index: number) => { const statusConfig = queueStatusStyles[item.status] ?? queueStatusStyles.queued; const StatusIcon = statusConfig.Icon; const isActive = item.status === "active"; diff --git a/lib/context/ContractOperationsContext.tsx b/lib/context/ContractOperationsContext.tsx new file mode 100644 index 0000000..26025e2 --- /dev/null +++ b/lib/context/ContractOperationsContext.tsx @@ -0,0 +1,174 @@ +"use client"; + +import React, { createContext, useContext, useCallback, useState, useEffect } from "react"; + +export type ContractOperationStatus = "pending" | "building" | "signing" | "submitting" | "confirming" | "complete" | "failed"; + +export interface ContractOperation { + id: string; + type: "emergency_transfer" | "regular_transfer" | "bill_payment" | "savings_goal" | "insurance_payment"; + title: string; + status: ContractOperationStatus; + detail: string; + duration: string; + timestamp: number; + transactionHash?: string; + error?: string; + metadata?: Record; +} + +export interface ContractOperationStage { + label: string; + duration: string; + detail: string; + placement: string; + status: "pending" | "active" | "complete"; +} + +interface ContractOperationsContextValue { + operations: ContractOperation[]; + stages: ContractOperationStage[]; + addOperation: (operation: Omit) => string; + updateOperation: (id: string, updates: Partial) => void; + removeOperation: (id: string) => void; + getActiveOperation: () => ContractOperation | undefined; + getOperationsByType: (type: ContractOperation["type"]) => ContractOperation[]; + clearCompletedOperations: () => void; +} + +const ContractOperationsContext = createContext(undefined); + +export function ContractOperationsProvider({ children }: { children: React.ReactNode }) { + const [operations, setOperations] = useState([]); + const [stages, setStages] = useState([ + { + label: "Review transfer inputs", + duration: "0-2 sec", + detail: "Surface fees, speed, and recipient details in the same view before an emergency contract request is built.", + placement: "Inline in the modal body", + status: "pending", + }, + { + label: "Build emergency payload", + duration: "2-5 sec", + detail: "Show the contract-build state close to the confirm action so the user knows the request is still being prepared.", + placement: "Above the modal footer", + status: "pending", + }, + { + label: "Collect wallet signature", + duration: "15-45 sec", + detail: "Escalate only when the payload is ready and keep the amount summary visible while the wallet prompt is open.", + placement: "Wallet sheet or modal", + status: "pending", + }, + { + label: "Submit and confirm", + duration: "5-30 sec", + detail: "Once the modal closes, confirmation should move into the global stack so the user can continue sending flows without losing context.", + placement: "Top-right desktop, inline mobile", + status: "pending", + }, + ]); + + const addOperation = useCallback((operation: Omit) => { + const id = `op-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + const newOperation: ContractOperation = { + ...operation, + id, + timestamp: Date.now(), + }; + setOperations((prev) => [...prev, newOperation]); + return id; + }, []); + + const updateOperation = useCallback((id: string, updates: Partial) => { + setOperations((prev) => + prev.map((op) => (op.id === id ? { ...op, ...updates } : op)) + ); + }, []); + + const removeOperation = useCallback((id: string) => { + setOperations((prev) => prev.filter((op) => op.id !== id)); + }, []); + + const getActiveOperation = useCallback(() => { + return operations.find((op) => op.status === "submitting" || op.status === "confirming"); + }, [operations]); + + const getOperationsByType = useCallback((type: ContractOperation["type"]) => { + return operations.filter((op) => op.type === type); + }, [operations]); + + const clearCompletedOperations = useCallback(() => { + setOperations((prev) => prev.filter((op) => op.status !== "complete" && op.status !== "failed")); + }, []); + + // Auto-update stages based on active operation status + useEffect(() => { + const activeOp = getActiveOperation(); + if (activeOp) { + setStages((prev) => { + const updated = [...prev]; + if (activeOp.status === "building") { + updated[0].status = "complete"; + updated[1].status = "active"; + } else if (activeOp.status === "signing") { + updated[0].status = "complete"; + updated[1].status = "complete"; + updated[2].status = "active"; + } else if (activeOp.status === "submitting" || activeOp.status === "confirming") { + updated[0].status = "complete"; + updated[1].status = "complete"; + updated[2].status = "complete"; + updated[3].status = "active"; + } else if (activeOp.status === "complete") { + updated.forEach((stage) => (stage.status = "complete")); + } + return updated; + }); + } else { + setStages((prev) => prev.map((stage) => ({ ...stage, status: "pending" as const }))); + } + }, [getActiveOperation]); + + // Auto-remove completed operations after 5 minutes + useEffect(() => { + const interval = setInterval(() => { + const now = Date.now(); + setOperations((prev) => + prev.filter((op) => { + if (op.status === "complete" || op.status === "failed") { + return now - op.timestamp < 5 * 60 * 1000; // 5 minutes + } + return true; + }) + ); + }, 60000); // Check every minute + + return () => clearInterval(interval); + }, []); + + return ( + + {children} + + ); +} + +export function useContractOperations(): ContractOperationsContextValue { + const ctx = useContext(ContractOperationsContext); + if (!ctx) throw new Error("useContractOperations must be used within a ContractOperationsProvider"); + return ctx; +} From f31ffdfe43de6287dd7da6072dde3ac75a0ca647 Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Thu, 18 Jun 2026 01:01:36 +0100 Subject: [PATCH 4/4] https://github.com/Menjay7/Remitwise-Frontend.git --- app/send/page.tsx | 12 ++++--- app/split/page.tsx | 58 +++++++++++++++--------------- components/LocaleSwitcher.tsx | 68 +++++++++++++++++++++++++++++++++++ components/Nav/PrimaryNav.tsx | 4 ++- lib/i18n/client.ts | 11 +++++- lib/i18n/locales/en.json | 59 ++++++++++++++++++++++++++++++ lib/i18n/locales/es.json | 59 ++++++++++++++++++++++++++++++ 7 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 components/LocaleSwitcher.tsx diff --git a/app/send/page.tsx b/app/send/page.tsx index 166a403..3bbfedd 100644 --- a/app/send/page.tsx +++ b/app/send/page.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { useToast } from "@/lib/context/ToastContext"; +import { useClientTranslator } from "@/lib/i18n/client"; import EmergencyTransferModal from "./components/EmergencyTransferModal"; import SendHeader from "./components/SendHeader"; import RecipientAddressInput from "./components/RecipientAddressInput"; @@ -21,6 +22,7 @@ export default function SendMoney() { const [isSubmitted, setIsSubmitted] = useState(false); const [transactionData, setTransactionData] = useState(null); const { toast } = useToast(); + const { t } = useClientTranslator(); const handleRecipientContinue = () => { if (recipient) { @@ -56,8 +58,8 @@ export default function SendMoney() { setIsSubmitted(true); toast({ variant: "success", - title: "Transfer submitted", - description: `Successfully sent ${amount} ${currency} to ${mockData.recipientAddress}.`, + title: t("send.toast.transferSubmitted"), + description: t("send.toast.transferSuccess", { amount, currency, address: mockData.recipientAddress }), }); console.log(`Send ${amount} ${currency} to ${recipient}`); }; @@ -83,7 +85,7 @@ export default function SendMoney() {
Recipient + }`}>{t("send.steps.recipient")}
{/* Step 2 */} @@ -95,7 +97,7 @@ export default function SendMoney() {
Amount + }`}>{t("send.steps.amount")} {/* Step 3 */} @@ -107,7 +109,7 @@ export default function SendMoney() { Review + }`}>{t("send.steps.review")} diff --git a/app/split/page.tsx b/app/split/page.tsx index 4737046..36d16d4 100644 --- a/app/split/page.tsx +++ b/app/split/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { Save, ShieldCheck, Wallet, Clock3, Layers3 } from "lucide-react"; +import { useClientTranslator } from "@/lib/i18n/client"; import SmartMoneySplitHeader from "@/components/SmartMoneySplitHeader"; import HowItWorks from "@/components/HowItWorksModal"; import AsyncOperationsPanel from "@/components/AsyncOperationsPanel"; @@ -67,6 +68,8 @@ const splitQueue = [ ]; export default function SplitConfiguration() { + const { t } = useClientTranslator(); + return (
@@ -76,44 +79,42 @@ export default function SplitConfiguration() {

- Allocation editor + {t("split.allocationEditor")}

- Current Allocation + {t("split.currentAllocation")}

- Customize how your remittances are distributed, then keep the - contract submission states anchored to the same area so the - workflow never feels detached. + {t("split.currentAllocationDescription")}

- Allocation changes are saved as a USDC smart contract action. The payload is prepared in-app and the wallet signs it locally. + {t("split.allocationNote")}

@@ -121,10 +122,9 @@ export default function SplitConfiguration() {
-

Total

+

{t("split.total")}

- Every contract build should block submission until this is - exactly 100%. + {t("split.totalDescription")}

100% @@ -133,10 +133,10 @@ export default function SplitConfiguration() { @@ -145,14 +145,14 @@ export default function SplitConfiguration() { - Cancel + {t("split.buttons.cancel")}
@@ -160,14 +160,14 @@ export default function SplitConfiguration() {
diff --git a/components/LocaleSwitcher.tsx b/components/LocaleSwitcher.tsx new file mode 100644 index 0000000..a46a427 --- /dev/null +++ b/components/LocaleSwitcher.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState } from "react"; +import { useClientTranslator, useClientLocale } from "@/lib/i18n/client"; +import { Globe } from "lucide-react"; + +type SupportedLocale = "en" | "es"; + +export default function LocaleSwitcher() { + const { t } = useClientTranslator(); + const locale = useClientLocale(); + const [isOpen, setIsOpen] = useState(false); + + const locales: { code: SupportedLocale; name: string; flag: string }[] = [ + { code: "en", name: t("localeSwitcher.english"), flag: "🇺🇸" }, + { code: "es", name: t("localeSwitcher.spanish"), flag: "🇪🇸" }, + ]; + + const currentLocale = locales.find((l) => l.code === locale) || locales[0]; + + const handleLocaleChange = (newLocale: SupportedLocale) => { + localStorage.setItem("locale", newLocale); + window.location.reload(); + }; + + return ( +
+ + + {isOpen && ( + <> +
setIsOpen(false)} + /> +
+
+ {locales.map((loc) => ( + + ))} +
+
+ + )} +
+ ); +} diff --git a/components/Nav/PrimaryNav.tsx b/components/Nav/PrimaryNav.tsx index e243d7e..34e89d9 100644 --- a/components/Nav/PrimaryNav.tsx +++ b/components/Nav/PrimaryNav.tsx @@ -6,6 +6,7 @@ import { Send, LayoutDashboard, FileText, Shield, Users, Settings, Bell } from " import Logo from "./Logo"; import WalletButton from "../WalletButton"; import MobileNav from "./MobileNav"; +import LocaleSwitcher from "@/components/LocaleSwitcher"; import { useWhatsNewOptional } from "@/lib/context/WhatsNewContext"; import WhatsNewBadge from "@/components/Dashboard/WhatsNewBadge"; @@ -59,8 +60,9 @@ const PrimaryNav = () => { ))} - {/* Right: Wallet & Mobile Menu Toggle */} + {/* Right: Locale Switcher, Wallet & Mobile Menu Toggle */}
+ {whatsNew && (