mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 00:45:41 +00:00
Update get payment methods from static to api
This commit is contained in:
@@ -1,148 +1,163 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { useBookingStore } from '@/lib/booking-store';
|
import { useBookingStore } from "@/lib/booking-store";
|
||||||
import { usePaymentStore } from '@/lib/payment-store';
|
import { usePaymentStore } from "@/lib/payment-store";
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from "@/lib/api-client";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { CreditCard, Smartphone, Wallet, Loader2, CheckCircle } from 'lucide-react';
|
import {
|
||||||
|
CreditCard,
|
||||||
|
Smartphone,
|
||||||
|
Wallet,
|
||||||
|
Loader2,
|
||||||
|
CheckCircle,
|
||||||
|
ExternalLink,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
// Mock payment methods with Ethiopian providers
|
const METHOD_ICONS: Record<string, any> = {
|
||||||
const paymentMethods = [
|
TELEBIRR: Smartphone,
|
||||||
{
|
CBE_BIRR: Smartphone,
|
||||||
id: 'TELEBIRR',
|
EBIRR: Smartphone,
|
||||||
name: 'Telebirr',
|
CARD: CreditCard,
|
||||||
icon: Smartphone,
|
WALLET: Wallet,
|
||||||
description: 'Pay with Telebirr mobile money',
|
};
|
||||||
color: 'bg-orange-50 border-orange-200 hover:border-orange-400'
|
|
||||||
},
|
const METHOD_COLORS: Record<string, string> = {
|
||||||
{
|
TELEBIRR: "bg-orange-50 dark:bg-orange-900/20",
|
||||||
id: 'CBE_BIRR',
|
CBE_BIRR: "bg-blue-50 dark:bg-blue-900/20",
|
||||||
name: 'CBE Birr',
|
EBIRR: "bg-green-50 dark:bg-green-900/20",
|
||||||
icon: Smartphone,
|
CARD: "bg-purple-50 dark:bg-purple-900/20",
|
||||||
description: 'Pay with CBE Birr',
|
WALLET: "bg-indigo-50 dark:bg-indigo-900/20",
|
||||||
color: 'bg-blue-50 border-blue-200 hover:border-blue-400'
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'EBIRR',
|
|
||||||
name: 'eBirr',
|
|
||||||
icon: Smartphone,
|
|
||||||
description: 'Pay with eBirr',
|
|
||||||
color: 'bg-green-50 border-green-200 hover:border-green-400'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'CARD',
|
|
||||||
name: 'Card Payment',
|
|
||||||
icon: CreditCard,
|
|
||||||
description: 'Pay with credit/debit card',
|
|
||||||
color: 'bg-purple-50 border-purple-200 hover:border-purple-400'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'WALLET',
|
|
||||||
name: 'Wallet',
|
|
||||||
icon: Wallet,
|
|
||||||
description: 'Pay from your wallet balance',
|
|
||||||
color: 'bg-indigo-50 border-indigo-200 hover:border-indigo-400'
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PaymentPage() {
|
export default function PaymentPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { bookingId, pnr, selectedSchedule, passengers } = useBookingStore();
|
const { bookingId, pnr, selectedSchedule, passengers } = useBookingStore();
|
||||||
const { selectedCurrency, setPaymentIntent, updateStatus } = usePaymentStore();
|
const { selectedCurrency, setPaymentIntent, updateStatus } =
|
||||||
|
usePaymentStore();
|
||||||
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
const [selectedMethod, setSelectedMethod] = useState<string | null>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
// Calculate total amount
|
const baseFare = passengers.reduce(
|
||||||
const baseFare = passengers.reduce((sum) => sum + (selectedSchedule?.baseFareAdult || 0), 0);
|
(sum) => sum + (selectedSchedule?.baseFareAdult || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
const totalAmount = baseFare;
|
const totalAmount = baseFare;
|
||||||
|
|
||||||
|
// Fetch real payment methods
|
||||||
|
const { data: methodsData, isLoading: methodsLoading } = useQuery({
|
||||||
|
queryKey: ["payment-methods"],
|
||||||
|
queryFn: () => apiClient.get("/payments/methods") as Promise<any>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentMethods: any[] = Array.isArray(methodsData)
|
||||||
|
? methodsData
|
||||||
|
: (methodsData as any)?.methods || (methodsData as any)?.data || [];
|
||||||
|
|
||||||
|
// Find paymentMethodId for the selected method from API response
|
||||||
|
const getPaymentMethodId = (methodCode: string): string => {
|
||||||
|
const found = paymentMethods.find(
|
||||||
|
(m: any) =>
|
||||||
|
m.code === methodCode ||
|
||||||
|
m.type === methodCode ||
|
||||||
|
m.name?.toUpperCase().replace(/\s/g, "_") === methodCode ||
|
||||||
|
m.id === methodCode,
|
||||||
|
);
|
||||||
|
return found?.id || found?.paymentMethodId || methodCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TELEBIRR initiate mutation
|
||||||
|
const initiateMutation = useMutation({
|
||||||
|
mutationFn: async (methodCode: string) => {
|
||||||
|
const paymentMethodId = getPaymentMethodId(methodCode);
|
||||||
|
return apiClient.post("/payments/initiate", {
|
||||||
|
bookingId,
|
||||||
|
method: methodCode,
|
||||||
|
paymentMethodId,
|
||||||
|
platform: "web",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (data: any) => {
|
||||||
|
setPaymentIntent(data?.paymentIntentId || data?.id || "");
|
||||||
|
updateStatus("PROCESSING");
|
||||||
|
setIsProcessing(false);
|
||||||
|
|
||||||
|
// If there's a redirect URL (e.g. Telebirr checkout page), open it
|
||||||
|
if (data?.checkoutUrl || data?.redirectUrl || data?.paymentUrl) {
|
||||||
|
window.open(
|
||||||
|
data.checkoutUrl || data.redirectUrl || data.paymentUrl,
|
||||||
|
"_blank",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
router.push("/booking/confirmation");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
console.error("Payment initiation failed:", error);
|
||||||
|
updateStatus("FAILED");
|
||||||
|
const msg =
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
"Payment failed. Please try again.";
|
||||||
|
alert(msg);
|
||||||
|
setIsProcessing(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generic payment intent mutation (for non-Telebirr methods)
|
||||||
const paymentMutation = useMutation({
|
const paymentMutation = useMutation({
|
||||||
mutationFn: async (data: any) => {
|
mutationFn: async (data: any) => {
|
||||||
// Try to call the real API, fallback to mock if it fails
|
|
||||||
try {
|
try {
|
||||||
return await apiClient.post('/payments/intent', data);
|
return await apiClient.post("/payments/intent", data);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.log('Payment API not available, using mock payment');
|
|
||||||
// Mock payment response
|
|
||||||
return {
|
return {
|
||||||
paymentIntentId: `mock-payment-${Date.now()}`,
|
paymentIntentId: `mock-payment-${Date.now()}`,
|
||||||
status: 'PENDING',
|
status: "PENDING",
|
||||||
amountMinor: data.amountMinor,
|
|
||||||
currency: data.currency,
|
|
||||||
method: data.method,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: async (data: any) => {
|
onSuccess: async (data: any) => {
|
||||||
setPaymentIntent(data.paymentIntentId);
|
setPaymentIntent(data.paymentIntentId);
|
||||||
updateStatus('PROCESSING');
|
updateStatus("PROCESSING");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
// Simulate payment processing
|
updateStatus("SUCCEEDED");
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
router.push("/booking/confirmation");
|
||||||
|
|
||||||
// Generate tickets after successful payment
|
|
||||||
try {
|
|
||||||
await generateTickets();
|
|
||||||
updateStatus('SUCCEEDED');
|
|
||||||
router.push('/booking/confirmation');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ticket generation failed:', error);
|
|
||||||
// Still proceed to confirmation even if ticket generation fails
|
|
||||||
updateStatus('SUCCEEDED');
|
|
||||||
router.push('/booking/confirmation');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
console.error('Payment failed:', error);
|
updateStatus("FAILED");
|
||||||
updateStatus('FAILED');
|
alert(
|
||||||
const errorMessage = error?.response?.data?.message || error?.message || 'Payment failed. Please try again.';
|
error?.response?.data?.message || "Payment failed. Please try again.",
|
||||||
alert(errorMessage);
|
);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateTickets = async () => {
|
|
||||||
// Try to generate tickets via API, fallback to mock
|
|
||||||
try {
|
|
||||||
await apiClient.post('/tickets/generate', {
|
|
||||||
bookingId,
|
|
||||||
pnr,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Ticket API not available, tickets will be generated on confirmation page');
|
|
||||||
// Mock ticket generation - tickets will be displayed on confirmation page
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePayment = async () => {
|
const handlePayment = async () => {
|
||||||
if (!selectedMethod || !bookingId) {
|
if (!selectedMethod || !bookingId) {
|
||||||
alert('Please select a payment method');
|
alert("Please select a payment method");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
paymentMutation.mutate({
|
if (selectedMethod === "TELEBIRR") {
|
||||||
bookingId,
|
debugger;
|
||||||
method: selectedMethod,
|
initiateMutation.mutate(selectedMethod);
|
||||||
currency: selectedCurrency,
|
} else {
|
||||||
amountMinor: totalAmount,
|
paymentMutation.mutate({
|
||||||
});
|
bookingId,
|
||||||
|
method: selectedMethod,
|
||||||
|
currency: selectedCurrency,
|
||||||
|
amountMinor: totalAmount,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Redirect if no booking data (but not during navigation)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Add a small delay to allow state to be set from previous page
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (!bookingId || !pnr) {
|
if (!bookingId || !pnr) router.push("/booking/search");
|
||||||
console.log('Payment page: Missing booking data, redirecting to search');
|
|
||||||
console.log('bookingId:', bookingId, 'pnr:', pnr);
|
|
||||||
router.push('/booking/search');
|
|
||||||
}
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [bookingId, pnr, router]);
|
}, [bookingId, pnr, router]);
|
||||||
|
|
||||||
@@ -157,63 +172,79 @@ export default function PaymentPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isBusy =
|
||||||
|
isProcessing || initiateMutation.isPending || paymentMutation.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<h1 className="text-3xl font-bold mb-2 text-gray-900 dark:text-gray-100">Complete payment</h1>
|
<h1 className="text-3xl font-bold mb-2 text-gray-900 dark:text-gray-100">
|
||||||
|
Complete payment
|
||||||
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
Booking reference: <span className="font-bold text-primary">{pnr}</span>
|
Booking reference:{" "}
|
||||||
|
<span className="font-bold text-primary">{pnr}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Payment Processing Overlay */}
|
{/* Processing overlay */}
|
||||||
{isProcessing && (
|
{isBusy && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 max-w-md text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl p-8 max-w-sm w-full text-center shadow-2xl">
|
||||||
{paymentMutation.isSuccess ? (
|
<Loader2 className="w-14 h-14 text-primary animate-spin mx-auto mb-4" />
|
||||||
<>
|
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-gray-100">
|
||||||
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
Processing payment
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-gray-100">Payment successful!</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">Generating your tickets...</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
<Loader2 className="w-8 h-8 text-primary animate-spin mx-auto" />
|
Please wait…
|
||||||
</>
|
</p>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-16 h-16 text-primary animate-spin mx-auto mb-4" />
|
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-gray-100">Processing payment</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400">Please wait while we process your payment...</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Order Summary */}
|
{/* Order summary */}
|
||||||
<div className="card mb-6">
|
<div className="card mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Order summary</h2>
|
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
|
Order summary
|
||||||
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Route</span>
|
<span className="text-gray-600 dark:text-gray-400">Route</span>
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule?.origin} → {selectedSchedule?.destination}</span>
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{selectedSchedule?.origin} → {selectedSchedule?.destination}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Train</span>
|
<span className="text-gray-600 dark:text-gray-400">Train</span>
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule?.trainNumber}</span>
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{selectedSchedule?.trainNumber}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selectedSchedule?.selectedSeatClassName && (
|
{selectedSchedule?.selectedSeatClassName && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Class</span>
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">{selectedSchedule.selectedSeatClassName.replace(/_/g, ' ')}</span>
|
Class
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{selectedSchedule.selectedSeatClassName.replace(/_/g, " ")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600 dark:text-gray-400">Passengers</span>
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
<span className="font-medium text-gray-900 dark:text-gray-100">{passengers.length} passenger{passengers.length !== 1 ? 's' : ''}</span>
|
Passengers
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{passengers.length} passenger
|
||||||
|
{passengers.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
|
||||||
<div className="flex justify-between text-lg font-bold">
|
<div className="flex justify-between text-lg font-bold">
|
||||||
<span className="text-gray-900 dark:text-gray-100">Total amount</span>
|
<span className="text-gray-900 dark:text-gray-100">
|
||||||
<span className="text-primary dark:text-gray-100">
|
Total amount
|
||||||
|
</span>
|
||||||
|
<span className="text-primary">
|
||||||
ETB {(totalAmount / 100).toFixed(2)}
|
ETB {(totalAmount / 100).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,87 +252,122 @@ export default function PaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment Methods */}
|
{/* Payment methods */}
|
||||||
<div className="card mb-6">
|
<div className="card mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Select payment method</h2>
|
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||||
<div className="space-y-3">
|
Select payment method
|
||||||
{paymentMethods.map((method) => {
|
</h2>
|
||||||
const Icon = method.icon;
|
|
||||||
const isSelected = selectedMethod === method.id;
|
{methodsLoading ? (
|
||||||
return (
|
<div className="flex items-center justify-center py-8 gap-3 text-gray-500">
|
||||||
<button
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
key={method.id}
|
<span className="text-sm">Loading payment methods…</span>
|
||||||
onClick={() => setSelectedMethod(method.id)}
|
</div>
|
||||||
disabled={isProcessing}
|
) : paymentMethods.length === 0 ? (
|
||||||
className={`w-full p-4 rounded-lg border-2 transition-all text-left ${
|
<p className="text-sm text-gray-400 text-center py-6">
|
||||||
isSelected
|
No payment methods available.
|
||||||
? 'border-primary bg-primary/10 dark:bg-primary/20 shadow-md'
|
</p>
|
||||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-primary dark:hover:border-primary'
|
) : (
|
||||||
} ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}
|
<div className="space-y-3">
|
||||||
>
|
{paymentMethods.map((method: any) => {
|
||||||
<div className="flex items-center gap-3">
|
const code: string =
|
||||||
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
|
method.code || method.type || method.id || "";
|
||||||
isSelected ? 'bg-primary' : 'bg-gray-100 dark:bg-gray-700'
|
const name: string =
|
||||||
}`}>
|
method.name || method.displayName || code;
|
||||||
<Icon className={`w-6 h-6 ${isSelected ? 'text-white' : 'text-primary'}`} />
|
const description: string =
|
||||||
</div>
|
method.description || `Pay with ${name}`;
|
||||||
<div className="flex-1">
|
const isSelected = selectedMethod === code;
|
||||||
<p className="font-semibold text-gray-900 dark:text-gray-100">{method.name}</p>
|
const Icon = METHOD_ICONS[code] || Smartphone;
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">{method.description}</p>
|
const bgColor =
|
||||||
</div>
|
METHOD_COLORS[code] || "bg-gray-50 dark:bg-gray-800/50";
|
||||||
{isSelected && (
|
|
||||||
<div className="w-6 h-6 bg-primary rounded-full flex items-center justify-center">
|
return (
|
||||||
<CheckCircle className="w-5 h-5 text-white" />
|
<button
|
||||||
|
key={code}
|
||||||
|
onClick={() => setSelectedMethod(code)}
|
||||||
|
disabled={isBusy}
|
||||||
|
className={`w-full p-4 rounded-xl border-2 transition-all text-left ${
|
||||||
|
isSelected
|
||||||
|
? "border-primary bg-primary/10 dark:bg-primary/20 shadow-md"
|
||||||
|
: `border-gray-200 dark:border-gray-700 ${bgColor} hover:border-primary`
|
||||||
|
} ${isBusy ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-11 h-11 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||||
|
isSelected
|
||||||
|
? "bg-primary"
|
||||||
|
: "bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={`w-5 h-5 ${isSelected ? "text-white" : "text-primary"}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-1 min-w-0">
|
||||||
</div>
|
<p className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
</button>
|
{name}
|
||||||
);
|
</p>
|
||||||
})}
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
</div>
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{code === "TELEBIRR" && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-orange-600 dark:text-orange-400 font-medium flex-shrink-0">
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
Redirect
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="w-6 h-6 bg-primary rounded-full flex items-center justify-center flex-shrink-0">
|
||||||
|
<CheckCircle className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handlePayment}
|
onClick={handlePayment}
|
||||||
disabled={!selectedMethod || isProcessing}
|
disabled={!selectedMethod || isBusy || methodsLoading}
|
||||||
className={`btn-primary w-full py-4 text-lg font-semibold ${
|
className="btn-primary w-full py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
!selectedMethod || isProcessing ? 'opacity-50 cursor-not-allowed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isBusy ? (
|
||||||
<span className="flex items-center justify-center gap-2">
|
<span className="flex items-center justify-center gap-2">
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
Processing...
|
Processing…
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
`Pay ETB ${(totalAmount / 100).toFixed(2)}`
|
`Pay ETB ${(totalAmount / 100).toFixed(2)}`
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
disabled={isProcessing}
|
disabled={isBusy}
|
||||||
className="btn-secondary w-full py-2"
|
className="btn-secondary w-full py-2"
|
||||||
>
|
>
|
||||||
Back to review
|
Back to review
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{(initiateMutation.isError || paymentMutation.isError) && (
|
||||||
{paymentMutation.isError && (
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4 mt-4">
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mt-4">
|
<p className="text-red-700 dark:text-red-300 text-sm font-medium">
|
||||||
<p className="text-red-800 dark:text-red-200 text-sm font-medium">
|
⚠️ Payment failed. Please try again or contact support.
|
||||||
⚠️ Payment failed. Please try again or contact support if the problem persists.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Security Notice */}
|
<div className="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl">
|
||||||
<div className="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400 text-center">
|
🔒 Your payment is secure and encrypted. We do not store your
|
||||||
🔒 Your payment is secure and encrypted. We do not store your payment information.
|
payment information.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user