mockpay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +207 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +270 -0
  4. package/dist/core/config.d.ts +13 -0
  5. package/dist/core/config.js +30 -0
  6. package/dist/core/db.d.ts +9 -0
  7. package/dist/core/db.js +104 -0
  8. package/dist/core/logger.d.ts +11 -0
  9. package/dist/core/logger.js +47 -0
  10. package/dist/core/runtime.d.ts +10 -0
  11. package/dist/core/runtime.js +34 -0
  12. package/dist/core/state.d.ts +18 -0
  13. package/dist/core/state.js +70 -0
  14. package/dist/core/utils.d.ts +2 -0
  15. package/dist/core/utils.js +8 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +3 -0
  18. package/dist/providers/flutterwave/index.d.ts +10 -0
  19. package/dist/providers/flutterwave/index.js +231 -0
  20. package/dist/providers/paystack/index.d.ts +10 -0
  21. package/dist/providers/paystack/index.js +226 -0
  22. package/dist/routes/logs.d.ts +2 -0
  23. package/dist/routes/logs.js +20 -0
  24. package/dist/server/index.d.ts +1 -0
  25. package/dist/server/index.js +59 -0
  26. package/dist/server/middleware/errorSimulation.d.ts +2 -0
  27. package/dist/server/middleware/errorSimulation.js +47 -0
  28. package/dist/server/middleware/logging.d.ts +2 -0
  29. package/dist/server/middleware/logging.js +7 -0
  30. package/dist/types/index.d.ts +59 -0
  31. package/dist/types/index.js +1 -0
  32. package/dist/webhooks/sender.d.ts +8 -0
  33. package/dist/webhooks/sender.js +94 -0
  34. package/package.json +31 -0
  35. package/src/cli/index.ts +291 -0
  36. package/src/core/config.ts +47 -0
  37. package/src/core/db.ts +123 -0
  38. package/src/core/logger.ts +57 -0
  39. package/src/core/runtime.ts +42 -0
  40. package/src/core/state.ts +91 -0
  41. package/src/core/utils.ts +10 -0
  42. package/src/index.ts +3 -0
  43. package/src/providers/flutterwave/index.ts +254 -0
  44. package/src/providers/paystack/index.ts +249 -0
  45. package/src/routes/logs.ts +28 -0
  46. package/src/server/index.ts +69 -0
  47. package/src/server/middleware/errorSimulation.ts +60 -0
  48. package/src/server/middleware/logging.ts +10 -0
  49. package/src/types/index.ts +64 -0
  50. package/src/webhooks/sender.ts +108 -0
  51. package/template/App.tsx +25 -0
  52. package/template/components/Button.tsx +45 -0
  53. package/template/components/Card.tsx +16 -0
  54. package/template/components/Input.tsx +27 -0
  55. package/template/components/PaymentMethodIcon.tsx +40 -0
  56. package/template/components/StatusScreen.tsx +117 -0
  57. package/template/hooks/useQueryParams.ts +22 -0
  58. package/template/index.html +29 -0
  59. package/template/index.tsx +16 -0
  60. package/template/package.json +25 -0
  61. package/template/pages/CancelledPage.tsx +20 -0
  62. package/template/pages/CheckoutPage.tsx +370 -0
  63. package/template/pages/FailedPage.tsx +20 -0
  64. package/template/pages/SuccessPage.tsx +20 -0
  65. package/template/pnpm-lock.yaml +1192 -0
  66. package/template/react-icons.d.ts +8 -0
  67. package/template/tsconfig.json +31 -0
  68. package/template/types.ts +25 -0
  69. package/template/vite.config.ts +23 -0
  70. package/tsconfig.json +16 -0
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+
3
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
+ label: string;
5
+ icon?: React.ReactNode;
6
+ }
7
+
8
+ const Input: React.FC<InputProps> = ({ label, icon, className = '', ...props }) => {
9
+ return (
10
+ <div className="flex flex-col space-y-1.5">
11
+ <label className="text-xs font-medium text-slate-500 uppercase tracking-wider">{label}</label>
12
+ <div className="relative group">
13
+ {icon && (
14
+ <div className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-indigo-500 transition-colors">
15
+ {icon}
16
+ </div>
17
+ )}
18
+ <input
19
+ className={`w-full bg-slate-50 border border-slate-200 rounded-xl py-3 px-4 ${icon ? 'pl-11' : ''} text-slate-700 placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all ${className}`}
20
+ {...props}
21
+ />
22
+ </div>
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export default Input;
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { HiCreditCard, HiLibrary, HiSwitchHorizontal, HiDeviceMobile } from 'react-icons/hi';
3
+ import { PaymentMethod } from '../types';
4
+
5
+ interface PaymentMethodIconProps {
6
+ method: PaymentMethod;
7
+ isActive: boolean;
8
+ onClick: () => void;
9
+ label: string;
10
+ }
11
+
12
+ const PaymentMethodIcon: React.FC<PaymentMethodIconProps> = ({ method, isActive, onClick, label }) => {
13
+ const getIcon = () => {
14
+ switch (method) {
15
+ case PaymentMethod.CARD: return <HiCreditCard size={26} />;
16
+ case PaymentMethod.BANK: return <HiLibrary size={26} />;
17
+ case PaymentMethod.TRANSFER: return <HiSwitchHorizontal size={26} />;
18
+ case PaymentMethod.USSD: return <HiDeviceMobile size={26} />;
19
+ }
20
+ };
21
+
22
+ return (
23
+ <button
24
+ onClick={onClick}
25
+ className={`flex flex-col items-center justify-center p-5 rounded-[1.5rem] border transition-all duration-300 w-full space-y-3 group
26
+ ${isActive
27
+ ? 'bg-indigo-50 border-indigo-200 text-indigo-600 shadow-sm ring-1 ring-indigo-200'
28
+ : 'bg-white border-slate-100 text-slate-400 hover:border-slate-300 hover:bg-slate-50'}`}
29
+ >
30
+ <div className={`transition-transform duration-300 ${isActive ? 'scale-110 text-indigo-600' : 'group-hover:scale-105 text-slate-400'}`}>
31
+ {getIcon()}
32
+ </div>
33
+ <span className={`text-[10px] font-black uppercase tracking-widest ${isActive ? 'text-indigo-600' : 'text-slate-400'}`}>
34
+ {label}
35
+ </span>
36
+ </button>
37
+ );
38
+ };
39
+
40
+ export default PaymentMethodIcon;
@@ -0,0 +1,117 @@
1
+ import React, { useEffect } from 'react';
2
+ import { HiCheckCircle, HiXCircle, HiInformationCircle } from 'react-icons/hi';
3
+ import Card from './Card';
4
+ import Button from './Button';
5
+ import { useNavigate } from 'react-router-dom';
6
+
7
+ interface StatusScreenProps {
8
+ status: 'success' | 'failed' | 'cancelled';
9
+ title: string;
10
+ message: string;
11
+ reference: string;
12
+ provider: 'paystack' | 'flutterwave';
13
+ callbackUrl?: string;
14
+ transactionId?: string;
15
+ }
16
+
17
+ const StatusScreen: React.FC<StatusScreenProps> = ({
18
+ status,
19
+ title,
20
+ message,
21
+ reference,
22
+ provider,
23
+ callbackUrl,
24
+ transactionId
25
+ }) => {
26
+ const navigate = useNavigate();
27
+
28
+ const configs = {
29
+ success: {
30
+ icon: <HiCheckCircle className="text-emerald-500 w-24 h-24 drop-shadow-lg" />,
31
+ color: 'text-emerald-600',
32
+ bgColor: 'bg-emerald-50',
33
+ btnText: 'Done',
34
+ btnVariant: 'primary' as const
35
+ },
36
+ failed: {
37
+ icon: <HiXCircle className="text-rose-500 w-24 h-24 drop-shadow-lg" />,
38
+ color: 'text-rose-600',
39
+ bgColor: 'bg-rose-50',
40
+ btnText: 'Try Again',
41
+ btnVariant: 'danger' as const
42
+ },
43
+ cancelled: {
44
+ icon: <HiInformationCircle className="text-slate-400 w-24 h-24 drop-shadow-lg" />,
45
+ color: 'text-slate-600',
46
+ bgColor: 'bg-slate-50',
47
+ btnText: 'Back to Checkout',
48
+ btnVariant: 'secondary' as const
49
+ }
50
+ };
51
+
52
+ const current = configs[status];
53
+
54
+ const continueToCallback = () => {
55
+ if (!callbackUrl) {
56
+ navigate('/checkout');
57
+ return;
58
+ }
59
+
60
+ let callback: URL;
61
+ try {
62
+ callback = new URL(callbackUrl);
63
+ } catch {
64
+ navigate('/checkout');
65
+ return;
66
+ }
67
+
68
+ if (provider === 'flutterwave') {
69
+ callback.searchParams.set('status', status === 'success' ? 'successful' : status);
70
+ callback.searchParams.set('tx_ref', reference);
71
+ if (transactionId) {
72
+ callback.searchParams.set('transaction_id', transactionId);
73
+ }
74
+ } else {
75
+ callback.searchParams.set('reference', reference);
76
+ callback.searchParams.set('status', status === 'cancelled' ? 'abandoned' : status);
77
+ }
78
+
79
+ window.location.assign(callback.toString());
80
+ };
81
+
82
+ useEffect(() => {
83
+ if (!callbackUrl) return;
84
+ const timer = window.setTimeout(() => {
85
+ continueToCallback();
86
+ }, 5000);
87
+ return () => window.clearTimeout(timer);
88
+ }, [callbackUrl, provider, reference, status, transactionId]);
89
+
90
+ return (
91
+ <Card>
92
+ <div className="p-8 flex flex-col items-center text-center">
93
+ <div className={`p-4 rounded-full ${current.bgColor} mb-6`}>
94
+ {current.icon}
95
+ </div>
96
+ <h2 className={`text-2xl font-bold mb-2 ${current.color}`}>{title}</h2>
97
+ <p className="text-slate-500 mb-8 max-w-[280px] leading-relaxed">
98
+ {message}
99
+ </p>
100
+
101
+ <div className="w-full p-4 bg-slate-50 rounded-2xl border border-slate-100 mb-8">
102
+ <p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mb-1">Payment Reference</p>
103
+ <p className="font-mono text-slate-700 break-all select-all">{reference}</p>
104
+ </div>
105
+
106
+ <Button
107
+ variant={current.btnVariant}
108
+ onClick={() => (callbackUrl ? continueToCallback() : navigate('/checkout'))}
109
+ >
110
+ {callbackUrl ? 'Continue Now' : current.btnText}
111
+ </Button>
112
+ </div>
113
+ </Card>
114
+ );
115
+ };
116
+
117
+ export default StatusScreen;
@@ -0,0 +1,22 @@
1
+ import { useLocation } from 'react-router-dom';
2
+ import { CheckoutParams } from '../types';
3
+
4
+ export const useQueryParams = (): CheckoutParams => {
5
+ const { search } = useLocation();
6
+ const query = new URLSearchParams(search);
7
+ const provider = query.get('provider');
8
+ const callbackUrl = query.get('callback_url') || query.get('redirect_url') || undefined;
9
+ const apiBase = query.get('api_base') || undefined;
10
+
11
+ return {
12
+ provider: provider === 'flutterwave' ? 'flutterwave' : 'paystack',
13
+ ref: query.get('ref') || 'N/A',
14
+ amount: query.get('amount') || '0',
15
+ currency: query.get('currency') || 'NGN',
16
+ email: query.get('email') || 'customer@example.com',
17
+ name: query.get('name') || 'Customer',
18
+ callbackUrl,
19
+ transactionId: query.get('transaction_id') || undefined,
20
+ apiBase,
21
+ };
22
+ };
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MockPay | Hosted Checkout</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #f8fafc;
15
+ }
16
+ @keyframes scale-in {
17
+ 0% { transform: scale(0.9); opacity: 0; }
18
+ 100% { transform: scale(1); opacity: 1; }
19
+ }
20
+ .animate-scale-in {
21
+ animation: scale-in 0.3s ease-out forwards;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body class="antialiased">
26
+ <div id="root"></div>
27
+ <script type="module" src="/index.tsx"></script>
28
+ </body>
29
+ </html>
@@ -0,0 +1,16 @@
1
+
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "mockpay-hosted-checkout",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.2.4",
13
+ "react-dom": "^19.2.4",
14
+ "react-icons": "^5.5.0",
15
+ "react-router-dom": "^7.13.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "@types/node": "^22.14.0",
21
+ "@vitejs/plugin-react": "^5.0.0",
22
+ "typescript": "~5.8.2",
23
+ "vite": "^6.2.0"
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import StatusScreen from '../components/StatusScreen';
3
+ import { useQueryParams } from '../hooks/useQueryParams';
4
+
5
+ const CancelledPage: React.FC = () => {
6
+ const { ref, provider, callbackUrl, transactionId } = useQueryParams();
7
+ return (
8
+ <StatusScreen
9
+ status="cancelled"
10
+ title="Payment Cancelled"
11
+ message="You have cancelled the payment process. You can return to the merchant site or try again."
12
+ reference={ref}
13
+ provider={provider}
14
+ callbackUrl={callbackUrl}
15
+ transactionId={transactionId}
16
+ />
17
+ );
18
+ };
19
+
20
+ export default CancelledPage;
@@ -0,0 +1,370 @@
1
+ import React, { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useQueryParams } from '../hooks/useQueryParams';
4
+ import { PaymentMethod, PaymentStatus } from '../types';
5
+ import Card from '../components/Card';
6
+ import Input from '../components/Input';
7
+ import Button from '../components/Button';
8
+ import PaymentMethodIcon from '../components/PaymentMethodIcon';
9
+ import {
10
+ HiCreditCard,
11
+ HiCalendar,
12
+ HiLockClosed,
13
+ HiArrowLeft,
14
+ HiDuplicate,
15
+ HiCheck,
16
+ HiChevronRight,
17
+ HiCurrencyDollar
18
+ } from 'react-icons/hi';
19
+
20
+ const MOCK_BANKS = [
21
+ { id: '1', name: 'Access Bank', code: '044' },
22
+ { id: '2', name: 'GTBank', code: '058' },
23
+ { id: '3', name: 'Zenith Bank', code: '057' },
24
+ { id: '4', name: 'Kuda Bank', code: '090' },
25
+ ];
26
+
27
+ function resolveFinalStatus(
28
+ provider: 'paystack' | 'flutterwave',
29
+ requestedStatus: PaymentStatus,
30
+ completionPayload: any
31
+ ): PaymentStatus {
32
+ const checkoutStatus = String(completionPayload?.data?.checkout_status ?? '').toLowerCase();
33
+ if (checkoutStatus === 'success' || checkoutStatus === 'failed' || checkoutStatus === 'cancelled') {
34
+ return checkoutStatus as PaymentStatus;
35
+ }
36
+
37
+ const rawStatus = String(completionPayload?.data?.status ?? '').toLowerCase();
38
+
39
+ if (!rawStatus) {
40
+ return requestedStatus;
41
+ }
42
+
43
+ if (provider === 'paystack') {
44
+ if (rawStatus === 'success') return 'success';
45
+ if (rawStatus === 'failed') return 'failed';
46
+ if (rawStatus === 'abandoned') return 'cancelled';
47
+ }
48
+
49
+ if (provider === 'flutterwave') {
50
+ if (rawStatus === 'successful') return 'success';
51
+ if (rawStatus === 'failed') return 'failed';
52
+ if (rawStatus === 'cancelled') return 'cancelled';
53
+ }
54
+
55
+ return requestedStatus;
56
+ }
57
+
58
+ const CheckoutPage: React.FC = () => {
59
+ const navigate = useNavigate();
60
+ const { provider, ref, amount, currency, email, name, callbackUrl, apiBase } = useQueryParams();
61
+ const [selectedMethod, setSelectedMethod] = useState<PaymentMethod>(PaymentMethod.CARD);
62
+ const [isProcessing, setIsProcessing] = useState<boolean>(false);
63
+ const [activeStep, setActiveStep] = useState<1 | 2>(1);
64
+ const [copied, setCopied] = useState(false);
65
+ const [selectedBank, setSelectedBank] = useState<string | null>(null);
66
+ const [cardNumber, setCardNumber] = useState('');
67
+ const [cardExpiry, setCardExpiry] = useState('');
68
+ const [cardCvv, setCardCvv] = useState('');
69
+
70
+ const formattedAmount = new Intl.NumberFormat('en-NG', {
71
+ style: 'currency',
72
+ currency,
73
+ }).format(parseFloat(amount) || 0);
74
+
75
+ const handlePayment = async (status: PaymentStatus) => {
76
+ setIsProcessing(true);
77
+ await new Promise(resolve => setTimeout(resolve, 2000));
78
+
79
+ let completionPayload: any = null;
80
+ const fallbackApiBase = provider === 'paystack' ? 'http://localhost:4010' : 'http://localhost:4020';
81
+ const completionUrl = new URL('/mock/complete', apiBase ?? fallbackApiBase).toString();
82
+ try {
83
+ const response = await fetch(completionUrl, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({ provider, reference: ref, status }),
87
+ });
88
+ completionPayload = await response.json().catch(() => null);
89
+ } catch (error) {
90
+ console.warn('Mock server unavailable, continuing with UI flow.');
91
+ } finally {
92
+ setIsProcessing(false);
93
+ const finalStatus = resolveFinalStatus(provider, status, completionPayload);
94
+ const params = new URLSearchParams();
95
+ params.set('ref', ref);
96
+ params.set('provider', provider);
97
+ if (callbackUrl) params.set('callback_url', callbackUrl);
98
+ if (provider === 'flutterwave' && completionPayload?.data?.transaction_id) {
99
+ params.set('transaction_id', String(completionPayload.data.transaction_id));
100
+ }
101
+ navigate(`/${finalStatus}?${params.toString()}`);
102
+ }
103
+ };
104
+
105
+ const copyToClipboard = (text: string) => {
106
+ navigator.clipboard.writeText(text);
107
+ setCopied(true);
108
+ setTimeout(() => setCopied(false), 2000);
109
+ };
110
+
111
+ const formatCardNumber = (value: string) => {
112
+ const digits = value.replace(/\D/g, '').slice(0, 19);
113
+ return digits.replace(/(\d{4})(?=\d)/g, '$1 ').trim();
114
+ };
115
+
116
+ const formatExpiry = (value: string) => {
117
+ const digits = value.replace(/\D/g, '').slice(0, 4);
118
+ if (digits.length <= 2) return digits;
119
+ return `${digits.slice(0, 2)}/${digits.slice(2)}`;
120
+ };
121
+
122
+ const formatCvv = (value: string) => value.replace(/\D/g, '').slice(0, 4);
123
+
124
+ const renderCardFlow = () => (
125
+ <div className="space-y-6 animate-scale-in">
126
+ <div className="space-y-4">
127
+ <Input
128
+ label="Card Number"
129
+ placeholder="0000 0000 0000 0000"
130
+ icon={<HiCreditCard size={20} />}
131
+ inputMode="numeric"
132
+ autoComplete="cc-number"
133
+ value={cardNumber}
134
+ onChange={(e) => setCardNumber(formatCardNumber(e.target.value))}
135
+ maxLength={23}
136
+ />
137
+ <div className="grid grid-cols-2 gap-4">
138
+ <Input
139
+ label="Expiry Date"
140
+ placeholder="MM / YY"
141
+ icon={<HiCalendar size={20} />}
142
+ inputMode="numeric"
143
+ autoComplete="cc-exp"
144
+ value={cardExpiry}
145
+ onChange={(e) => setCardExpiry(formatExpiry(e.target.value))}
146
+ maxLength={5}
147
+ />
148
+ <Input
149
+ label="CVV"
150
+ placeholder="123"
151
+ type="password"
152
+ icon={<HiLockClosed size={20} />}
153
+ inputMode="numeric"
154
+ autoComplete="cc-csc"
155
+ value={cardCvv}
156
+ onChange={(e) => setCardCvv(formatCvv(e.target.value))}
157
+ maxLength={4}
158
+ />
159
+ </div>
160
+ </div>
161
+ <div className="pt-2 space-y-3">
162
+ <Button isLoading={isProcessing} onClick={() => handlePayment('success')}>
163
+ Pay {formattedAmount}
164
+ </Button>
165
+ <Button variant="ghost" onClick={() => handlePayment('failed')}>
166
+ Simulate Failed Payment
167
+ </Button>
168
+ </div>
169
+ </div>
170
+ );
171
+
172
+ const renderTransferFlow = () => (
173
+ <div className="space-y-6 animate-scale-in">
174
+ <div className="bg-slate-50 border border-slate-100 rounded-2xl p-6 text-center">
175
+ <p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Transfer to the account below</p>
176
+
177
+ <div className="space-y-1 mb-6">
178
+ <p className="text-sm font-medium text-slate-500">Bank Name</p>
179
+ <p className="text-xl font-bold text-slate-800">MockPay Savings Bank</p>
180
+ </div>
181
+
182
+ <div className="space-y-1 mb-6">
183
+ <p className="text-sm font-medium text-slate-500">Account Number</p>
184
+ <div className="flex items-center justify-center space-x-2">
185
+ <p className="text-3xl font-black text-indigo-600 tracking-tighter">0123456789</p>
186
+ <button
187
+ onClick={() => copyToClipboard('0123456789')}
188
+ className="p-2 hover:bg-indigo-100 rounded-full transition-colors text-indigo-600"
189
+ >
190
+ {copied ? <HiCheck size={20} /> : <HiDuplicate size={20} />}
191
+ </button>
192
+ </div>
193
+ </div>
194
+
195
+ <div className="space-y-1">
196
+ <p className="text-sm font-medium text-slate-500">Account Name</p>
197
+ <p className="text-md font-semibold text-slate-700 uppercase">MOCKSTORE INC - CHECKOUT</p>
198
+ </div>
199
+ </div>
200
+
201
+ <div className="p-4 bg-amber-50 border border-amber-100 rounded-xl">
202
+ <p className="text-[11px] text-amber-700 leading-tight">
203
+ Please complete this transfer within 30 minutes. Your payment will be confirmed automatically once the transfer is detected.
204
+ </p>
205
+ </div>
206
+
207
+ <div className="pt-2 space-y-3">
208
+ <Button isLoading={isProcessing} onClick={() => handlePayment('success')}>
209
+ I've Sent the Money
210
+ </Button>
211
+ </div>
212
+ </div>
213
+ );
214
+
215
+ const renderBankFlow = () => (
216
+ <div className="space-y-6 animate-scale-in">
217
+ <p className="text-xs font-bold text-slate-400 uppercase tracking-widest">Select your Bank</p>
218
+ <div className="space-y-2 max-h-[250px] overflow-y-auto pr-2 custom-scrollbar">
219
+ {MOCK_BANKS.map((bank) => (
220
+ <button
221
+ key={bank.id}
222
+ onClick={() => setSelectedBank(bank.id)}
223
+ className={`w-full flex items-center justify-between p-4 rounded-xl border transition-all ${
224
+ selectedBank === bank.id
225
+ ? 'bg-indigo-50 border-indigo-200 ring-2 ring-indigo-500/20'
226
+ : 'bg-white border-slate-100 hover:border-indigo-200'
227
+ }`}
228
+ >
229
+ <div className="flex items-center space-x-3">
230
+ <div className="w-8 h-8 bg-slate-100 rounded-lg flex items-center justify-center font-bold text-xs text-slate-500">
231
+ {bank.name.charAt(0)}
232
+ </div>
233
+ <span className="font-semibold text-slate-700">{bank.name}</span>
234
+ </div>
235
+ <HiChevronRight className={selectedBank === bank.id ? 'text-indigo-500' : 'text-slate-300'} />
236
+ </button>
237
+ ))}
238
+ </div>
239
+ <Button
240
+ disabled={!selectedBank}
241
+ isLoading={isProcessing}
242
+ onClick={() => handlePayment('success')}
243
+ >
244
+ Authenticate with Bank
245
+ </Button>
246
+ </div>
247
+ );
248
+
249
+ const renderUSSDFlow = () => (
250
+ <div className="space-y-6 animate-scale-in">
251
+ <div className="bg-slate-900 text-white rounded-2xl p-8 text-center relative overflow-hidden">
252
+ <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/20 rounded-full -mr-16 -mt-16 blur-3xl"></div>
253
+ <p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Dial code below on your phone</p>
254
+ <p className="text-3xl font-mono font-black tracking-widest mb-2 text-indigo-400">
255
+ *737*000*821#
256
+ </p>
257
+ <p className="text-sm text-slate-400">Code expires in <span className="text-white font-mono">04:59</span></p>
258
+ </div>
259
+
260
+ <div className="space-y-3">
261
+ <p className="text-xs font-medium text-slate-500 text-center">
262
+ Wait for the USSD prompt on your mobile device and authorize with your PIN.
263
+ </p>
264
+ <Button isLoading={isProcessing} onClick={() => handlePayment('success')}>
265
+ I've Completed the Dial
266
+ </Button>
267
+ </div>
268
+ </div>
269
+ );
270
+
271
+ return (
272
+ <Card>
273
+ {/* Test Mode Ribbon */}
274
+ <div className="absolute -left-12 top-6 -rotate-45 bg-amber-400 text-amber-900 text-[10px] font-black py-1 px-12 shadow-sm z-10 uppercase tracking-tighter">
275
+ Test Mode
276
+ </div>
277
+
278
+ {/* Header */}
279
+ <div className="bg-[#3F51B5] px-8 pt-12 pb-10 text-white relative overflow-hidden">
280
+ <div className="absolute -right-10 -bottom-10 w-40 h-40 bg-white/5 rounded-full blur-2xl"></div>
281
+ <div className="flex justify-between items-start mb-6 relative z-10">
282
+ <div className="flex items-center space-x-2">
283
+ <div className="bg-white/10 p-2 rounded-xl backdrop-blur-md border border-white/20">
284
+ <HiCurrencyDollar size={20} className="text-white" />
285
+ </div>
286
+ <span className="font-bold text-lg tracking-tight">MockStore Inc.</span>
287
+ </div>
288
+ <button
289
+ onClick={() => handlePayment('cancelled')}
290
+ className="text-white/60 hover:text-white transition-colors text-xs font-bold uppercase tracking-widest"
291
+ >
292
+ Cancel
293
+ </button>
294
+ </div>
295
+
296
+ <div className="relative z-10">
297
+ <p className="text-white/60 text-xs font-bold uppercase tracking-widest mb-1">Paying Amount</p>
298
+ <h1 className="text-4xl font-extrabold tracking-tight">{formattedAmount}</h1>
299
+ <p className="text-indigo-200 text-sm mt-2 opacity-80">{name} · {email}</p>
300
+ </div>
301
+ </div>
302
+
303
+ <div className="p-8">
304
+ {/* Step Indicator */}
305
+ <div className="flex items-center space-x-2 mb-8">
306
+ <div className={`h-1.5 flex-1 rounded-full transition-all duration-500 ${activeStep >= 1 ? 'bg-indigo-500' : 'bg-slate-100'}`}></div>
307
+ <div className={`h-1.5 flex-1 rounded-full transition-all duration-500 ${activeStep >= 2 ? 'bg-indigo-500' : 'bg-slate-100'}`}></div>
308
+ </div>
309
+
310
+ {activeStep === 1 ? (
311
+ <div className="space-y-6 animate-scale-in">
312
+ <div>
313
+ <p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">How would you like to pay?</p>
314
+ <div className="grid grid-cols-2 gap-3">
315
+ <PaymentMethodIcon
316
+ method={PaymentMethod.CARD}
317
+ label="Card"
318
+ isActive={selectedMethod === PaymentMethod.CARD}
319
+ onClick={() => setSelectedMethod(PaymentMethod.CARD)}
320
+ />
321
+ <PaymentMethodIcon
322
+ method={PaymentMethod.TRANSFER}
323
+ label="Transfer"
324
+ isActive={selectedMethod === PaymentMethod.TRANSFER}
325
+ onClick={() => setSelectedMethod(PaymentMethod.TRANSFER)}
326
+ />
327
+ <PaymentMethodIcon
328
+ method={PaymentMethod.BANK}
329
+ label="Bank"
330
+ isActive={selectedMethod === PaymentMethod.BANK}
331
+ onClick={() => setSelectedMethod(PaymentMethod.BANK)}
332
+ />
333
+ <PaymentMethodIcon
334
+ method={PaymentMethod.USSD}
335
+ label="USSD"
336
+ isActive={selectedMethod === PaymentMethod.USSD}
337
+ onClick={() => setSelectedMethod(PaymentMethod.USSD)}
338
+ />
339
+ </div>
340
+ </div>
341
+
342
+ <Button onClick={() => setActiveStep(2)}>
343
+ Pay with {selectedMethod === PaymentMethod.USSD ? 'USSD' : selectedMethod.charAt(0).toUpperCase() + selectedMethod.slice(1)}
344
+ </Button>
345
+ </div>
346
+ ) : (
347
+ <div className="space-y-6">
348
+ <button
349
+ onClick={() => setActiveStep(1)}
350
+ className="flex items-center text-[10px] font-black text-slate-400 hover:text-indigo-600 transition-colors uppercase tracking-widest"
351
+ >
352
+ <HiArrowLeft className="mr-1" /> Change Payment Method
353
+ </button>
354
+
355
+ {selectedMethod === PaymentMethod.CARD && renderCardFlow()}
356
+ {selectedMethod === PaymentMethod.TRANSFER && renderTransferFlow()}
357
+ {selectedMethod === PaymentMethod.BANK && renderBankFlow()}
358
+ {selectedMethod === PaymentMethod.USSD && renderUSSDFlow()}
359
+
360
+ <p className="text-center text-[10px] text-slate-400 font-bold uppercase tracking-[0.2em] pt-4">
361
+ Secure Checkout by <span className="text-indigo-500">MockPay</span>
362
+ </p>
363
+ </div>
364
+ )}
365
+ </div>
366
+ </Card>
367
+ );
368
+ };
369
+
370
+ export default CheckoutPage;