Skip to content
Open
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
55 changes: 55 additions & 0 deletions Aframp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,61 @@ Aframp/

---

## 🧩 Components

### TransactionStatusCard

A reusable React component for displaying transaction states across the AFRAMP application.

#### Props

```typescript
interface TransactionStatusCardProps {
type: 'onramp' | 'offramp' | 'payment' | 'swap';
status: 'pending' | 'confirming' | 'completed' | 'failed';
amount: string;
asset: string;
timestamp: Date;
txHash?: string;
chain?: 'stellar' | 'ethereum' | 'polygon' | 'base';
confirmations?: number;
errorMessage?: string;
onRetry?: () => void;
}
```

#### Usage

```tsx
import { TransactionStatusCard } from '@/components/TransactionStatusCard';

<TransactionStatusCard
type="swap"
status="confirming"
amount="1,500"
asset="cNGN → 2.5 USDC"
timestamp={new Date()}
txHash="example-tx-hash"
chain="stellar"
confirmations={8}
/>
```

#### Features

- **Status Visualization**: Color-coded badges with icons for pending (yellow), confirming (blue), completed (green), failed (red)
- **Transaction Types**: Icons for onramp (⬇️), offramp (⬆️), payment (💳), swap (🔄)
- **Blockchain Links**: Direct links to explorers for Stellar, Ethereum, Polygon, and Base networks
- **Responsive Design**: Mobile-first design that works on all screen sizes
- **Accessibility**: Proper ARIA labels and keyboard navigation
- **Error Handling**: Displays error messages and retry buttons for failed transactions

#### Demo

View examples of all transaction states at `/examples/transaction-status`.

---

## 🚀 Development Setup

Follow these instructions to get a local copy of the AFRAMP frontend up and running.
Expand Down
78 changes: 78 additions & 0 deletions Aframp/app/examples/transaction-status/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import React from 'react';
import { TransactionStatusCard } from '@/components/TransactionStatusCard';

const examples = [
{
type: 'swap' as const,
status: 'confirming' as const,
amount: '1,500',
asset: 'cNGN → 2.5 USDC',
timestamp: new Date(Date.now() - 3 * 60 * 1000), // 3 minutes ago
txHash: 'example-tx-hash',
chain: 'stellar' as const,
confirmations: 8,
},
{
type: 'onramp' as const,
status: 'pending' as const,
amount: '500',
asset: 'USDC',
timestamp: new Date(Date.now() - 2 * 60 * 1000),
},
{
type: 'offramp' as const,
status: 'completed' as const,
amount: '1,000',
asset: 'cNGN',
timestamp: new Date(Date.now() - 10 * 60 * 1000),
txHash: 'completed-tx-hash',
chain: 'ethereum' as const,
},
{
type: 'payment' as const,
status: 'failed' as const,
amount: '250',
asset: 'USDC',
timestamp: new Date(Date.now() - 5 * 60 * 1000),
errorMessage: 'Insufficient funds',
onRetry: () => alert('Retry clicked'),
},
{
type: 'swap' as const,
status: 'confirming' as const,
amount: '0.5',
asset: 'ETH → 1,800 USDC',
timestamp: new Date(Date.now() - 1 * 60 * 1000),
txHash: 'polygon-tx-hash',
chain: 'polygon' as const,
confirmations: 5,
},
{
type: 'onramp' as const,
status: 'completed' as const,
amount: '2,000',
asset: 'cNGN',
timestamp: new Date(Date.now() - 15 * 60 * 1000),
txHash: 'base-tx-hash',
chain: 'base' as const,
},
];

export default function TransactionStatusExamples() {
return (
<div className="min-h-screen bg-background p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-foreground mb-8">
Transaction Status Card Examples
</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{examples.map((example, index) => (
<TransactionStatusCard key={index} {...example} />
))}
</div>
</div>
</div>
);
}
168 changes: 168 additions & 0 deletions Aframp/components/TransactionStatusCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from 'react';
import {
ArrowDown,
ArrowUp,
CreditCard,
RefreshCw,
Loader,
Clock,
CheckCircle,
XCircle,
ExternalLink,
} from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { cn } from '@/lib/utils';

interface TransactionStatusCardProps {
type: 'onramp' | 'offramp' | 'payment' | 'swap';
status: 'pending' | 'confirming' | 'completed' | 'failed';
amount: string;
asset: string;
timestamp: Date;
txHash?: string;
chain?: 'stellar' | 'ethereum' | 'polygon' | 'base';
confirmations?: number;
errorMessage?: string;
onRetry?: () => void;
key?: React.Key;
}

const getTypeIcon = (type: TransactionStatusCardProps['type']) => {
switch (type) {
case 'onramp':
return ArrowDown;
case 'offramp':
return ArrowUp;
case 'payment':
return CreditCard;
case 'swap':
return RefreshCw;
default:
return RefreshCw;
}
};

const getStatusIcon = (status: TransactionStatusCardProps['status']) => {
switch (status) {
case 'pending':
return Loader;
case 'confirming':
return Clock;
case 'completed':
return CheckCircle;
case 'failed':
return XCircle;
default:
return Clock;
}
};

const getStatusColor = (status: TransactionStatusCardProps['status']) => {
switch (status) {
case 'pending':
return 'text-yellow-500 bg-yellow-50 dark:bg-yellow-950/20';
case 'confirming':
return 'text-blue-500 bg-blue-50 dark:bg-blue-950/20';
case 'completed':
return 'text-green-500 bg-green-50 dark:bg-green-950/20';
case 'failed':
return 'text-red-500 bg-red-50 dark:bg-red-950/20';
default:
return 'text-gray-500 bg-gray-50 dark:bg-gray-950/20';
}
};

const getExplorerUrl = (chain: TransactionStatusCardProps['chain'], txHash: string) => {
switch (chain) {
case 'stellar':
return `https://stellar.expert/explorer/public/tx/${txHash}`;
case 'ethereum':
return `https://etherscan.io/tx/${txHash}`;
case 'polygon':
return `https://polygonscan.com/tx/${txHash}`;
case 'base':
return `https://basescan.org/tx/${txHash}`;
default:
return '';
}
};

export const TransactionStatusCard = ({
type,
status,
amount,
asset,
timestamp,
txHash,
chain,
confirmations,
errorMessage,
onRetry,
}: TransactionStatusCardProps) => {
const TypeIcon = getTypeIcon(type);
const StatusIcon = getStatusIcon(status);
const statusColor = getStatusColor(status);
const explorerUrl = txHash && chain ? getExplorerUrl(chain, txHash) : '';

const capitalizedChain = chain ? chain.charAt(0).toUpperCase() + chain.slice(1) : '';

return (
<div className="bg-card border border-border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<TypeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-sm font-medium text-foreground capitalize">{type}</span>
</div>
<div className={cn("flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium", statusColor)}>
<StatusIcon className={cn("w-3 h-3", status === 'pending' && "animate-spin")} />
<span className="capitalize">{status}</span>
</div>
</div>

<div className="mb-2">
<div className="text-lg font-semibold text-foreground">
{amount} {asset}
</div>
<div className="text-sm text-muted-foreground">
{formatDistanceToNow(timestamp, { addSuffix: true })}
</div>
</div>

{status === 'confirming' && confirmations !== undefined && (
<div className="mb-3 text-sm text-muted-foreground">
Confirmations: {confirmations}/12
</div>
)}

{status === 'failed' && errorMessage && (
<div className="mb-3 text-sm text-destructive">
{errorMessage}
</div>
)}

<div className="flex items-center justify-between">
{explorerUrl && (
<a
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-primary hover:underline"
aria-label={`View transaction on ${chain} explorer`}
>
<ExternalLink className="w-4 h-4" />
View on {capitalizedChain} Explorer
</a>
)}
{status === 'failed' && onRetry && (
<button
onClick={onRetry}
className="text-sm text-primary hover:underline"
aria-label="Retry transaction"
>
Retry
</button>
)}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions Aframp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@stellar/freighter-api": "^6.0.1",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
Expand Down
Loading