The PayOnProof frontend is a Next.js 16 App Router application that provides a clean, performant UI for comparing remittance routes and executing cross-border transfers.
Directory structure
services/web/
app/ # Next.js App Router pages
layout.tsx # Root layout with providers
page.tsx # Landing page
send/
page.tsx # Main app: compare routes & execute
components/ # React components
ui/ # shadcn/ui primitives
pop-header.tsx # App header
remittance-form.tsx # Search form
route-card.tsx # Route comparison card
transaction-execution.tsx
proof-of-payment.tsx
lib/ # Frontend utilities
api.ts # API base URL configuration
anchors-api.ts # Backend API client
types.ts # Shared TypeScript types
wallet-context.tsx # Stellar wallet provider
hooks/ # React hooks
use-toast.ts
public/ # Static assets
isotipo.png
App Router pages
Landing page (app/page.tsx)
Public marketing page with:
Hero section explaining PayOnProof
Feature highlights
CTA to /send
Send page (app/send/page.tsx)
Main application with a 4-step flow :
type AppStep = "search" | "routes" | "execute" | "proof" ;
Step 1: Search
< RemittanceForm
countries = { countries }
onSearch = { handleSearch }
loading = { loading }
/>
User inputs:
Origin country
Destination country
Amount to send
Step 2: Routes
{ sortedRoutes . map (( route ) => (
< RouteCard
key = { route . id }
route = { route }
onSelect = { handleSelectRoute }
selectable = { route . available && isMoneyGramRoute }
/>
))}
Displays:
All available routes sorted by recommendation, fee, or speed
Fee breakdown (on-ramp + bridge + off-ramp)
Estimated time
Exchange rate
Amount recipient receives
Currently, only MoneyGram-to-MoneyGram routes are executable (MVP constraint).
Step 3: Execute
< TransactionExecution
route = { selectedRoute }
amount = { amount }
onBack = { handleBackToRoutes }
onComplete = { handleTransactionComplete }
/>
Simulates:
SEP-24 interactive deposit flow
SEP-10 authentication
Stellar transaction submission
Anchor callback confirmation
Step 4: Proof
< ProofOfPaymentView
transaction = { transaction }
onNewTransfer = { handleNewTransfer }
/>
Shows:
Transaction hash (Stellar)
Verification link to StellarExpert
Summary of transfer details
Shareable proof of payment
Component architecture
Component hierarchy
SendPage (client component)
└─ WalletProvider
├─ PopHeader
├─ GradientMesh (background)
└─ [Current Step]
├─ RemittanceForm
├─ RouteCard (multiple)
├─ TransactionExecution
└─ ProofOfPaymentView
Key components
Form with validation using react-hook-form and zod:
interface RemittanceFormProps {
countries : AnchorCountry [];
onSearch : ( origin : string , destination : string , amount : number ) => void ;
loading : boolean ;
}
Features:
Country dropdowns populated from /api/anchors/countries
Amount input with validation
Loading state during route comparison
RouteCard
Displays a single remittance route:
interface RouteCardProps {
route : RemittanceRoute ;
onSelect : ( route : RemittanceRoute ) => void ;
selectable : boolean ;
selectionHint ?: string ;
index : number ;
}
Visual elements:
Origin/destination anchor names
Fee percentage badge
Estimated time
“Recommended” indicator (⚡)
Escrow status
Select button (disabled if not selectable)
TransactionExecution
Multi-step execution flow:
const steps = [
"Authenticating with anchor" , // SEP-10
"Initiating deposit flow" , // SEP-24
"Submitting to Stellar" , // Transaction
"Confirming with destination" , // SEP-24 callback
"Verifying on-chain" , // Horizon query
];
Simulates execution with realistic delays and error handling.
ProofOfPaymentView
Final confirmation screen:
< div className = "proof-container" >
< CheckCircle className = "success-icon" />
< h1 > Transfer complete </ h1 >
< p > Transaction hash: { transaction . stellarTxHash } </ p >
< a href = { stellarExpertUrl } target = "_blank" > View on StellarExpert </ a >
</ div >
API communication
API client (lib/anchors-api.ts)
import { apiUrl } from "./api" ;
export async function fetchAnchorCountries () : Promise < AnchorCountry []> {
const response = await fetch ( apiUrl ( "/api/anchors/countries" ));
if ( ! response . ok ) throw new Error ( "Failed to fetch countries" );
return response . json ();
}
export async function compareRoutes ( input : {
origin : string ;
destination : string ;
amount : number ;
}) : Promise <{ routes : RemittanceRoute []; noRouteReason ?: string }> {
const response = await fetch ( apiUrl ( "/api/compare-routes" ), {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ( input ),
});
if ( ! response . ok ) throw new Error ( "Failed to compare routes" );
return response . json ();
}
Environment configuration
# services/web/.env.local
NEXT_PUBLIC_API_BASE_URL = http://localhost:3001
Production:
NEXT_PUBLIC_API_BASE_URL = https://api.payonproof.com
The NEXT_PUBLIC_ prefix makes the variable accessible in browser-side code.
State management
Local state with React hooks
No global state management library is needed. The app uses:
const [ step , setStep ] = useState < AppStep >( "search" );
const [ routes , setRoutes ] = useState < RemittanceRoute []>([]);
const [ selectedRoute , setSelectedRoute ] = useState < RemittanceRoute | null >( null );
const [ transaction , setTransaction ] = useState < Transaction | null >( null );
Why no Redux/Zustand?
Simple data flow : Single-page app with linear progression
Ephemeral state : Route comparison results don’t need persistence
No cross-component communication : Parent-child props are sufficient
Performance : React 19’s automatic batching handles updates efficiently
Wallet integration
Freighter wallet context
// lib/wallet-context.tsx
export function WalletProvider ({ children } : { children : React . ReactNode }) {
const [ publicKey , setPublicKey ] = useState < string | null >( null );
const [ isConnected , setIsConnected ] = useState ( false );
const connect = async () => {
if ( await isConnected ()) {
const key = await getPublicKey ();
setPublicKey ( key );
setIsConnected ( true );
}
};
return (
< WalletContext . Provider value = {{ publicKey , isConnected , connect }} >
{ children }
</ WalletContext . Provider >
);
}
Usage:
const { publicKey , connect } = useWallet ();
Freighter is a browser extension wallet for Stellar. Users must install it to interact with the blockchain.
Styling approach
Tailwind CSS + shadcn/ui
< Button
variant = "outline"
size = "sm"
className = "rounded-xl bg-transparent hover:bg-primary/10"
>
Select route
</ Button >
Design system:
Colors : CSS variables defined in styles/globals.css
Components : Radix UI primitives with Tailwind styling
Animations : tailwindcss-animate plugin
Responsive : Mobile-first breakpoints
Dark mode support
import { ThemeProvider } from "next-themes" ;
export default function RootLayout ({ children }) {
return (
< html lang = "en" suppressHydrationWarning >
< body >
< ThemeProvider attribute = "class" defaultTheme = "system" enableSystem >
{ children }
</ ThemeProvider >
</ body >
</ html >
);
}
Image optimization
import Image from "next/image" ;
< Image
src = "/isotipo.png"
alt = "POP"
width = { 72 }
height = { 72 }
priority // Load immediately for hero image
/>
Next.js automatically optimizes images with WebP conversion and lazy loading.
Font optimization
import { Inter } from "next/font/google" ;
const inter = Inter ({ subsets: [ "latin" ] });
Fonts are self-hosted and preloaded for better performance.
Code splitting
Next.js automatically splits code by route:
Landing page bundle: ~45 KB
Send page bundle: ~120 KB
Shared vendor chunk: ~180 KB
Type safety
Shared types
// lib/types.ts
export interface RemittanceRoute {
id : string ;
originAnchor : {
id : string ;
name : string ;
country : string ;
currency : string ;
type : "on-ramp" ;
};
destinationAnchor : {
id : string ;
name : string ;
country : string ;
currency : string ;
type : "off-ramp" ;
};
feePercentage : number ;
feeAmount : number ;
estimatedTime : string ;
exchangeRate : number ;
receivedAmount : number ;
available : boolean ;
recommended : boolean ;
}
Types are manually kept in sync with backend response shapes. Future improvement: generate types from OpenAPI schema.
Error handling
try {
const payload = await compareRoutes ({ origin , destination , amount });
setRoutes ( payload . routes ?? []);
setNoRouteReason ( payload . noRouteReason ?? null );
setStep ( "routes" );
} catch ( error ) {
const message = error instanceof Error ? error . message : "Failed to fetch routes" ;
setRoutes ([]);
setSearchError ( message );
} finally {
setLoading ( false );
}
Error display:
{ searchError && (
< p className = "mt-2 text-xs text-destructive" > { searchError } </ p >
)}
Local development
cd services/web
npm install
npm run dev
Runs on http://localhost:3000 by default.
Environment setup:
cp .env.example .env.local
# Edit NEXT_PUBLIC_API_BASE_URL to point to backend
Next steps
Backend architecture Learn how the API processes route comparisons
Stellar integration Understand blockchain interactions and anchor flows