Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
499 changes: 469 additions & 30 deletions app/emergency-transfer/page.tsx

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -25,12 +26,14 @@ export default function RootLayout({
<body className={`${inter.className} starry-bg min-h-screen`}>
<ToastProvider>
<DensityProvider>
<SessionExpiryProvider>
<LayoutWrapper>
{children}
</LayoutWrapper>
<ToastRegion />
</SessionExpiryProvider>
<ContractOperationsProvider>
<SessionExpiryProvider>
<LayoutWrapper>
{children}
</LayoutWrapper>
<ToastRegion />
</SessionExpiryProvider>
</ContractOperationsProvider>
</DensityProvider>
</ToastProvider>
</body>
Expand Down
90 changes: 57 additions & 33 deletions app/send/components/EmergencyTransferModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState } from 'react'
import { useState, useEffect } from 'react'
import {
ArrowRight,
Clock3,
Expand All @@ -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
Expand Down Expand Up @@ -54,37 +55,64 @@ 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,
}: EmergencyTransferModalProps) {
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<string | null>(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

Expand Down Expand Up @@ -265,24 +293,20 @@ export default function EmergencyTransferModal({
</button>
<button
type="button"
disabled={!confirmed || numericAmount <= 0}
disabled={!confirmed || numericAmount <= 0 || isSubmitting}
onClick={handleSubmit}
className="flex flex-1 items-center justify-center gap-2 rounded-2xl bg-gradient-to-b from-red-600 to-red-700 px-6 py-3 font-semibold text-white transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#101010] disabled:cursor-not-allowed disabled:opacity-50"
>
Review Transfer
{isSubmitting ? 'Processing...' : 'Review Transfer'}
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>

<aside className="space-y-6">
<AsyncOperationsPanel
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."
useLiveContext={true}
stages={emergencyStages}
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={emergencyQueue}
/>
</aside>
</div>
Expand Down
12 changes: 7 additions & 5 deletions app/send/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -21,6 +22,7 @@ export default function SendMoney() {
const [isSubmitted, setIsSubmitted] = useState(false);
const [transactionData, setTransactionData] = useState<any>(null);
const { toast } = useToast();
const { t } = useClientTranslator();

const handleRecipientContinue = () => {
if (recipient) {
Expand Down Expand Up @@ -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}`);
};
Expand All @@ -83,7 +85,7 @@ export default function SendMoney() {
</div>
<span className={`text-xs font-bold uppercase tracking-wider ${
step === "recipient" ? "text-red-500" : "text-zinc-500"
}`}>Recipient</span>
}`}>{t("send.steps.recipient")}</span>
</div>

{/* Step 2 */}
Expand All @@ -95,7 +97,7 @@ export default function SendMoney() {
</div>
<span className={`text-xs font-bold uppercase tracking-wider ${
step === "amount" ? "text-red-500" : "text-zinc-500"
}`}>Amount</span>
}`}>{t("send.steps.amount")}</span>
</div>

{/* Step 3 */}
Expand All @@ -107,7 +109,7 @@ export default function SendMoney() {
</div>
<span className={`text-xs font-bold uppercase tracking-wider ${
step === "review" ? "text-red-500" : "text-zinc-500"
}`}>Review</span>
}`}>{t("send.steps.review")}</span>
</div>
</div>
</div>
Expand Down
58 changes: 29 additions & 29 deletions app/split/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -67,6 +68,8 @@ const splitQueue = [
];

export default function SplitConfiguration() {
const { t } = useClientTranslator();

return (
<div className='min-h-screen bg-[#010101] safari-safe-bottom'>
<SmartMoneySplitHeader />
Expand All @@ -76,55 +79,52 @@ export default function SplitConfiguration() {
<div className='rounded-3xl border border-white/[0.08] bg-[linear-gradient(180deg,rgba(18,18,18,0.98),rgba(10,10,10,0.98))] p-5 320:p-6 375:p-7 sm:p-8'>
<div className='border-b border-white/[0.08] pb-5 375:pb-6'>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-red-300'>
Allocation editor
{t("split.allocationEditor")}
</p>
<h2 className='mt-3 text-xl 375:text-2xl font-semibold text-white'>
Current Allocation
{t("split.currentAllocation")}
</h2>
<p className='mt-2 text-sm 375:text-base leading-6 text-gray-300'>
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")}
</p>
</div>

<p className='mt-3 text-sm leading-6 text-gray-300'>
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")}
</p>

<form className='mt-6 space-y-5 375:space-y-6'>
<SplitInput
label='Daily Spending'
description='For immediate family expenses'
label={t("split.inputs.dailySpending")}
description={t("split.inputs.dailySpendingDescription")}
value={50}
color='bg-blue-500'
/>
<SplitInput
label='Savings'
description='Allocated to savings goals'
label={t("split.inputs.savings")}
description={t("split.inputs.savingsDescription")}
value={30}
color='bg-green-500'
/>
<SplitInput
label='Bills'
description='Automated bill payments'
label={t("split.inputs.bills")}
description={t("split.inputs.billsDescription")}
value={15}
color='bg-yellow-500'
/>
<SplitInput
label='Insurance'
description='Micro-insurance premiums'
label={t("split.inputs.insurance")}
description={t("split.inputs.insuranceDescription")}
value={5}
color='bg-violet-500'
/>

<div className='rounded-2xl border border-white/[0.08] bg-[#141414] p-4 375:p-5'>
<div className='flex items-center justify-between gap-4'>
<div>
<p className='text-sm font-medium text-gray-300'>Total</p>
<p className='text-sm font-medium text-gray-300'>{t("split.total")}</p>
<p className='mt-1 text-xs 375:text-sm text-gray-500'>
Every contract build should block submission until this is
exactly 100%.
{t("split.totalDescription")}
</p>
</div>
<span className='text-2xl 375:text-3xl font-semibold text-white'>100%</span>
Expand All @@ -133,10 +133,10 @@ export default function SplitConfiguration() {

<AsyncSubmissionStatus
pending={false}
idleTitle='Contract submission placement'
idleDescription='Keep validation and build feedback inline with the form. Reserve stacked status cards for submit and confirmation so they can persist after navigation.'
pendingTitle='Building contract request'
pendingDescription='Show the user that the remittance_split payload is being prepared before the wallet step opens.'
idleTitle={t("split.asyncStatus.idleTitle")}
idleDescription={t("split.asyncStatus.idleDescription")}
pendingTitle={t("split.asyncStatus.pendingTitle")}
pendingDescription={t("split.asyncStatus.pendingDescription")}
/>

<HowItWorks />
Expand All @@ -145,29 +145,29 @@ export default function SplitConfiguration() {
<Link
href='/'
className='touch-target-wide flex-1 rounded-2xl border border-white/10 bg-[#161616] px-6 py-3.5 text-center text-sm 375:text-base font-semibold text-white transition hover:bg-[#202020] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#101010]'>
Cancel
{t("split.buttons.cancel")}
</Link>
<button
type='submit'
className='touch-target-wide flex flex-1 items-center justify-center gap-2 rounded-2xl bg-gradient-to-b from-red-600 to-red-700 px-6 py-3.5 text-sm 375:text-base font-semibold text-white transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-[#101010] disabled:cursor-not-allowed disabled:opacity-60'
disabled>
<Save className='h-5 w-5' />
<span>Connect Contract First</span>
<span>{t("split.buttons.connectContract")}</span>
</button>
</div>
</form>
</div>

<aside className='space-y-6 xl:sticky xl:top-6'>
<AsyncOperationsPanel
eyebrow='Async behavior'
title='Duration, Stacking, and Placement'
description='This route is the clearest contract-configuration example, so it sets the pattern for where each submission state should appear.'
eyebrow={t("split.asyncPanel.eyebrow")}
title={t("split.asyncPanel.title")}
description={t("split.asyncPanel.description")}
stages={splitStages}
queueTitle='Stack behavior'
queueDescription='Keep no more than three visible submission cards at a time. Newest actions stay highest in the stack and mobile collapses the stack inline below the initiating form.'
queueTitle={t("split.asyncPanel.queueTitle")}
queueDescription={t("split.asyncPanel.queueDescription")}
queueItems={splitQueue}
footer='No new Tailwind tokens are required for this pattern. The implementation reuses existing reds, neutrals, focus rings, and arbitrary-value gradients already used in the app.'
footer={t("split.asyncPanel.footer")}
/>
</aside>
</div>
Expand Down
Loading