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
33 changes: 33 additions & 0 deletions README_SPLIT_TRANSACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ This feature provides backend services for building unsigned Soroban transaction
- ✅ Optional custodial mode support
- ✅ Comprehensive error handling

## Send Flow Integration

The `POST /api/send` flow in `app/send/page.tsx` uses `computeAllocation()` from
`lib/remittance/split.ts` to compute the split breakdown **client-side** after a
successful API response:

```typescript
// app/send/page.tsx – handleConfirm (simplified)
const data = await response.json(); // { success, transactionId }
const splits = computeAllocation(amount, getSplitConfig(recipient));
// → { spending, savings, bills, insurance } (sums exactly to amount)

// Feed into TransactionSuccessReceipt:
<TransactionSuccessReceipt
hash={data.transactionId}
splits={splits} // ← real allocation, not inline amount * 0.5 math
...
/>
```

**Why client-side?** The `/api/send` endpoint returns `{ success, transactionId }` —
a "build transaction" stub. Keeping split math in `computeAllocation()` means:

- The allocation is consistent between the Send page and the Split settings page.
- `computeAllocation()` guarantees integer rounding with no float drift (spending
bucket absorbs the remainder).
- When the backend eventually returns fee/exchange-rate adjustments, only
`getSplitConfig()` needs updating.

See `lib/remittance/split.ts` for the full API.



## Architecture

```
Expand Down
68 changes: 62 additions & 6 deletions app/api/send/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/auth';
import type {
SendTransactionRequest,
SendTransactionResponse,
SendTransactionErrorResponse,
} from '@/lib/types/api';

async function handler(request: NextRequest, session: string) {
const body = await request.json();
// TODO: Create and submit Stellar transaction
return NextResponse.json({
transactionId: 'placeholder',
success: true
/**
* POST /api/send
*
* Builds a remittance send transaction (non-custodial: returns unsigned XDR
* or a placeholder until Stellar broadcasting is wired).
*
* Request body: {@link SendTransactionRequest}
* Success response: {@link SendTransactionResponse}
* Error response: {@link SendTransactionErrorResponse}
*
* @param request - The incoming Next.js request containing the send payload.
* @param _session - The authenticated user address (resolved by withAuth).
*/
async function handler(
request: NextRequest,
_session: string,
): Promise<NextResponse<SendTransactionResponse | SendTransactionErrorResponse>> {
let body: Partial<SendTransactionRequest>;

try {
body = await request.json();
} catch {
return NextResponse.json<SendTransactionErrorResponse>(
{ success: false, error: 'Invalid JSON body.' },
{ status: 400 },
);
}

const { recipient, amount, currency } = body;

if (!recipient || typeof recipient !== 'string' || recipient.trim() === '') {
return NextResponse.json<SendTransactionErrorResponse>(
{ success: false, error: 'recipient is required.' },
{ status: 400 },
);
}

if (typeof amount !== 'number' || amount <= 0) {
return NextResponse.json<SendTransactionErrorResponse>(
{ success: false, error: 'amount must be a number greater than zero.' },
{ status: 400 },
);
}

if (!currency || typeof currency !== 'string' || currency.trim() === '') {
return NextResponse.json<SendTransactionErrorResponse>(
{ success: false, error: 'currency is required.' },
{ status: 400 },
);
}

// TODO: Build and optionally sign a Stellar/Soroban transaction here.
// For now, return a placeholder transactionId so the UI flow can be
// exercised end-to-end before Stellar wiring is complete.
return NextResponse.json<SendTransactionResponse>({
success: true,
transactionId: `TX_PLACEHOLDER_${Date.now()}`,
});
}

Expand Down
50 changes: 45 additions & 5 deletions app/send/components/ReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface ReviewStepProps {
onConfirm: () => void;
onBack: () => void;
onEmergencyAction: () => void;
/** When true the confirm button is disabled and shows a loading spinner. */
isPending?: boolean;
}

export default function ReviewStep({
Expand All @@ -19,6 +21,7 @@ export default function ReviewStep({
onConfirm,
onBack,
onEmergencyAction,
isPending = false,
}: ReviewStepProps) {
return (
<div className="mx-auto max-w-4xl animate-in fade-in slide-in-from-bottom-4 duration-500">
Expand Down Expand Up @@ -67,16 +70,51 @@ export default function ReviewStep({

<div className="mt-10 space-y-4">
<button
id="send-confirm-btn"
onClick={onConfirm}
className="w-full py-4 bg-red-600 hover:bg-red-700 text-white rounded-2xl text-lg font-bold transition-all transform active:scale-[0.98] shadow-lg shadow-red-900/40 flex items-center justify-center gap-3"
disabled={isPending}
aria-busy={isPending}
aria-label={isPending ? "Processing your transfer, please wait" : "Confirm and send remittance"}
className="w-full py-4 bg-red-600 hover:bg-red-700 disabled:bg-red-900 disabled:cursor-not-allowed text-white rounded-2xl text-lg font-bold transition-all transform active:scale-[0.98] shadow-lg shadow-red-900/40 flex items-center justify-center gap-3"
>
<Zap className="w-5 h-5 fill-current" />
Confirm & Send Remittance
{isPending ? (
<>
{/* Inline SVG spinner — no extra dependency, works with prefers-reduced-motion via CSS media query */}
<svg
className="w-5 h-5 animate-spin motion-reduce:hidden"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Processing&hellip;
</>
) : (
<>
<Zap className="w-5 h-5 fill-current" />
Confirm &amp; Send Remittance
</>
)}
</button>

<button
onClick={onBack}
className="w-full py-4 bg-transparent hover:bg-white/5 text-zinc-400 rounded-2xl text-sm font-medium transition-colors flex items-center justify-center gap-2"
disabled={isPending}
className="w-full py-4 bg-transparent hover:bg-white/5 disabled:opacity-40 disabled:cursor-not-allowed text-zinc-400 rounded-2xl text-sm font-medium transition-colors flex items-center justify-center gap-2"
>
<ArrowLeft className="w-5 h-5" />
Back to Amount
Expand All @@ -97,7 +135,8 @@ export default function ReviewStep({
</p>
<button
onClick={onEmergencyAction}
className="text-red-500 text-sm font-bold hover:text-red-400 transition-colors"
disabled={isPending}
className="text-red-500 text-sm font-bold hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Switch to Emergency Transfer →
</button>
Expand All @@ -113,3 +152,4 @@ export default function ReviewStep({
</div>
);
}

Loading