From 9ccfb858e718ea44bdf9707e6615c69eb13bf6d6 Mon Sep 17 00:00:00 2001 From: Emmyt24 Date: Thu, 22 Jan 2026 22:03:56 +0100 Subject: [PATCH] feat: add reusable TransactionStatusCard component --- Aframp/README.md | 55 ++++++ .../app/examples/transaction-status/page.tsx | 78 ++++++++ Aframp/components/TransactionStatusCard.tsx | 168 ++++++++++++++++++ Aframp/package.json | 1 + Aframp/pnpm-lock.yaml | 36 ++++ 5 files changed, 338 insertions(+) create mode 100644 Aframp/app/examples/transaction-status/page.tsx create mode 100644 Aframp/components/TransactionStatusCard.tsx diff --git a/Aframp/README.md b/Aframp/README.md index a607a02..428ea30 100644 --- a/Aframp/README.md +++ b/Aframp/README.md @@ -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'; + + +``` + +#### 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. diff --git a/Aframp/app/examples/transaction-status/page.tsx b/Aframp/app/examples/transaction-status/page.tsx new file mode 100644 index 0000000..56c5e18 --- /dev/null +++ b/Aframp/app/examples/transaction-status/page.tsx @@ -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 ( +
+
+

+ Transaction Status Card Examples +

+
+ {examples.map((example, index) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/Aframp/components/TransactionStatusCard.tsx b/Aframp/components/TransactionStatusCard.tsx new file mode 100644 index 0000000..ff8f01d --- /dev/null +++ b/Aframp/components/TransactionStatusCard.tsx @@ -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 ( +
+
+
+ + {type} +
+
+ + {status} +
+
+ +
+
+ {amount} {asset} +
+
+ {formatDistanceToNow(timestamp, { addSuffix: true })} +
+
+ + {status === 'confirming' && confirmations !== undefined && ( +
+ Confirmations: {confirmations}/12 +
+ )} + + {status === 'failed' && errorMessage && ( +
+ {errorMessage} +
+ )} + +
+ {explorerUrl && ( + + + View on {capitalizedChain} Explorer + + )} + {status === 'failed' && onRetry && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/Aframp/package.json b/Aframp/package.json index da29be1..b9f9981 100644 --- a/Aframp/package.json +++ b/Aframp/package.json @@ -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", diff --git a/Aframp/pnpm-lock.yaml b/Aframp/pnpm-lock.yaml index 03ae600..1f426a6 100644 --- a/Aframp/pnpm-lock.yaml +++ b/Aframp/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: '@radix-ui/react-tooltip': specifier: 1.1.6 version: 1.1.6(@types/react-dom@19.0.0)(@types/react@19.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@stellar/freighter-api': + specifier: ^6.0.1 + version: 6.0.1 '@vercel/analytics': specifier: 1.3.1 version: 1.3.1(next@16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) @@ -1110,6 +1113,9 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@stellar/freighter-api@6.0.1': + resolution: {integrity: sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1267,6 +1273,9 @@ packages: peerDependencies: postcss: ^8.1.0 + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.14: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true @@ -1276,6 +1285,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + c12@3.3.3: resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} peerDependencies: @@ -1477,6 +1489,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -1800,6 +1815,11 @@ packages: scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -2851,6 +2871,11 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@stellar/freighter-api@6.0.1': + dependencies: + buffer: 6.0.3 + semver: 7.7.1 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2988,6 +3013,8 @@ snapshots: postcss: 8.5.0 postcss-value-parser: 4.2.0 + base64-js@1.5.1: {} + baseline-browser-mapping@2.9.14: {} browserslist@4.28.1: @@ -2998,6 +3025,11 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + c12@3.3.3: dependencies: chokidar: 5.0.0 @@ -3174,6 +3206,8 @@ snapshots: graceful-fs@4.2.11: {} + ieee754@1.2.1: {} + ignore@7.0.5: {} input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): @@ -3461,6 +3495,8 @@ snapshots: scule@1.3.0: {} + semver@7.7.1: {} + semver@7.7.3: {} server-only@0.0.1: {}