FInal bank, stripe and paypal sandbox completed
This commit is contained in:
@@ -281,7 +281,16 @@ export default function BankTransferForm({
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<span className="font-medium text-gray-900 dark:text-white">Amount to Transfer</span>
|
||||
<span className="text-xl font-bold text-brand-600 dark:text-brand-400">
|
||||
{invoice.currency === 'PKR' ? 'PKR ' : '$'}{invoice.total_amount || invoice.total}
|
||||
{invoice.currency === 'PKR' ? 'PKR ' : '$'}
|
||||
{(() => {
|
||||
const amount = parseFloat(String(invoice.total_amount || invoice.total || 0));
|
||||
// Round PKR to nearest thousand
|
||||
if (invoice.currency === 'PKR') {
|
||||
const rounded = Math.round(amount / 1000) * 1000;
|
||||
return rounded.toLocaleString();
|
||||
}
|
||||
return amount.toFixed(2);
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* - Pakistan (PK): Stripe (Credit/Debit Card) + Bank Transfer
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
CreditCardIcon,
|
||||
Building2Icon,
|
||||
@@ -20,16 +20,19 @@ import {
|
||||
Loader2Icon,
|
||||
ArrowLeftIcon,
|
||||
LockIcon,
|
||||
RefreshCwIcon,
|
||||
} from '../../icons';
|
||||
import { Card } from '../ui/card';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import Button from '../ui/button/Button';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import BankTransferForm from './BankTransferForm';
|
||||
import {
|
||||
Invoice,
|
||||
getAvailablePaymentGateways,
|
||||
subscribeToPlan,
|
||||
getPayments,
|
||||
type PaymentGateway,
|
||||
} from '../../services/billing.api';
|
||||
|
||||
@@ -40,6 +43,18 @@ const PayPalIcon = ({ className }: { className?: string }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Currency symbol helper
|
||||
const getCurrencySymbol = (currency: string): string => {
|
||||
const symbols: Record<string, string> = {
|
||||
USD: '$',
|
||||
PKR: 'Rs.',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
INR: '₹',
|
||||
};
|
||||
return symbols[currency.toUpperCase()] || currency;
|
||||
};
|
||||
|
||||
interface PaymentOption {
|
||||
id: string;
|
||||
type: PaymentGateway;
|
||||
@@ -52,7 +67,10 @@ interface PendingPaymentViewProps {
|
||||
invoice: Invoice | null;
|
||||
userCountry: string;
|
||||
planName: string;
|
||||
planPrice: string;
|
||||
planPrice: string; // USD price (from plan)
|
||||
planPricePKR?: string; // PKR price (from invoice, if available)
|
||||
currency?: string;
|
||||
hasPendingBankTransfer?: boolean; // True if user has submitted bank transfer awaiting approval
|
||||
onPaymentSuccess: () => void;
|
||||
}
|
||||
|
||||
@@ -61,26 +79,79 @@ export default function PendingPaymentView({
|
||||
userCountry,
|
||||
planName,
|
||||
planPrice,
|
||||
planPricePKR,
|
||||
currency = 'USD',
|
||||
hasPendingBankTransfer = false,
|
||||
onPaymentSuccess,
|
||||
}: PendingPaymentViewProps) {
|
||||
const toast = useToast();
|
||||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||||
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [gatewaysLoading, setGatewaysLoading] = useState(true);
|
||||
const [paymentOptions, setPaymentOptions] = useState<PaymentOption[]>([]);
|
||||
const [showBankTransfer, setShowBankTransfer] = useState(false);
|
||||
// Initialize bankTransferSubmitted from prop (persisted state)
|
||||
const [bankTransferSubmitted, setBankTransferSubmitted] = useState(hasPendingBankTransfer);
|
||||
const [checkingStatus, setCheckingStatus] = useState(false);
|
||||
|
||||
const isPakistan = userCountry === 'PK';
|
||||
|
||||
// SIMPLIFIED: Always show USD price, with PKR equivalent for Pakistan bank transfer users
|
||||
const showPKREquivalent = isPakistan && selectedGateway === 'manual';
|
||||
// Round PKR to nearest thousand for cleaner display
|
||||
const pkrRaw = planPricePKR ? parseFloat(planPricePKR) : parseFloat(planPrice) * 278;
|
||||
const pkrEquivalent = Math.round(pkrRaw / 1000) * 1000;
|
||||
|
||||
// Check if bank transfer has been approved
|
||||
const checkPaymentStatus = useCallback(async () => {
|
||||
if (!bankTransferSubmitted) return;
|
||||
|
||||
setCheckingStatus(true);
|
||||
try {
|
||||
// Refresh user data from backend
|
||||
await refreshUser();
|
||||
|
||||
// Also check payments to see if any succeeded
|
||||
const { results: payments } = await getPayments();
|
||||
const hasSucceededPayment = payments.some(
|
||||
(p: any) => p.status === 'succeeded' || p.status === 'completed'
|
||||
);
|
||||
|
||||
if (hasSucceededPayment) {
|
||||
toast?.success?.('Payment approved! Your account is now active.');
|
||||
onPaymentSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check payment status:', error);
|
||||
} finally {
|
||||
setCheckingStatus(false);
|
||||
}
|
||||
}, [bankTransferSubmitted, refreshUser, onPaymentSuccess, toast]);
|
||||
|
||||
// Auto-check status every 30 seconds when awaiting approval
|
||||
useEffect(() => {
|
||||
if (!bankTransferSubmitted) return;
|
||||
|
||||
// Check immediately on mount
|
||||
checkPaymentStatus();
|
||||
|
||||
// Then poll every 30 seconds
|
||||
const interval = setInterval(checkPaymentStatus, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [bankTransferSubmitted, checkPaymentStatus]);
|
||||
|
||||
// Load available payment gateways
|
||||
useEffect(() => {
|
||||
const loadGateways = async () => {
|
||||
const isPK = userCountry === 'PK';
|
||||
|
||||
setGatewaysLoading(true);
|
||||
try {
|
||||
const gateways = await getAvailablePaymentGateways();
|
||||
const gateways = await getAvailablePaymentGateways(userCountry);
|
||||
const options: PaymentOption[] = [];
|
||||
|
||||
// Always show Stripe (Credit Card) if available
|
||||
// Add Stripe if available
|
||||
if (gateways.stripe) {
|
||||
options.push({
|
||||
id: 'stripe',
|
||||
@@ -91,28 +162,26 @@ export default function PendingPaymentView({
|
||||
});
|
||||
}
|
||||
|
||||
// For Pakistan: show Bank Transfer
|
||||
// For Global: show PayPal
|
||||
if (isPakistan) {
|
||||
if (gateways.manual) {
|
||||
options.push({
|
||||
id: 'bank_transfer',
|
||||
type: 'manual',
|
||||
name: 'Bank Transfer',
|
||||
description: 'Pay via local bank transfer (PKR)',
|
||||
icon: <Building2Icon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (gateways.paypal) {
|
||||
options.push({
|
||||
id: 'paypal',
|
||||
type: 'paypal',
|
||||
name: 'PayPal',
|
||||
description: 'Pay with your PayPal account',
|
||||
icon: <PayPalIcon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
// Add PayPal if available (Global users only, not PK)
|
||||
if (gateways.paypal) {
|
||||
options.push({
|
||||
id: 'paypal',
|
||||
type: 'paypal',
|
||||
name: 'PayPal',
|
||||
description: 'Pay with your PayPal account',
|
||||
icon: <PayPalIcon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Bank Transfer if available (Pakistan users only)
|
||||
if (gateways.manual) {
|
||||
options.push({
|
||||
id: 'bank_transfer',
|
||||
type: 'manual',
|
||||
name: 'Bank Transfer',
|
||||
description: 'Pay via local bank transfer (PKR equivalent)',
|
||||
icon: <Building2Icon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
|
||||
setPaymentOptions(options);
|
||||
@@ -135,7 +204,7 @@ export default function PendingPaymentView({
|
||||
};
|
||||
|
||||
loadGateways();
|
||||
}, [isPakistan]);
|
||||
}, [userCountry]);
|
||||
|
||||
const handlePayNow = async () => {
|
||||
if (!invoice) {
|
||||
@@ -187,7 +256,8 @@ export default function PendingPaymentView({
|
||||
invoice={invoice}
|
||||
onSuccess={() => {
|
||||
setShowBankTransfer(false);
|
||||
onPaymentSuccess();
|
||||
setBankTransferSubmitted(true);
|
||||
// Don't call onPaymentSuccess immediately - wait for approval
|
||||
}}
|
||||
onCancel={() => setShowBankTransfer(false)}
|
||||
/>
|
||||
@@ -196,6 +266,132 @@ export default function PendingPaymentView({
|
||||
);
|
||||
}
|
||||
|
||||
// If bank transfer was submitted - show awaiting approval state
|
||||
if (bankTransferSubmitted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-amber-50 via-white to-orange-50 dark:from-gray-900 dark:via-gray-900 dark:to-amber-950 py-12 px-4">
|
||||
<div className="max-w-xl mx-auto">
|
||||
{/* Header with Awaiting Badge */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-amber-100 dark:bg-amber-900/30 mb-4 animate-pulse">
|
||||
<Loader2Icon className="w-10 h-10 text-amber-600 dark:text-amber-400 animate-spin" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<Badge variant="soft" tone="warning" size="md">
|
||||
Awaiting Approval
|
||||
</Badge>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Payment Submitted!
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Your bank transfer for <strong>{planName}</strong> is being verified
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status Card */}
|
||||
<Card className="p-6 mb-6 border-2 border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-900/10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-amber-200 dark:bg-amber-800 rounded-full">
|
||||
<Building2Icon className="w-5 h-5 text-amber-700 dark:text-amber-300" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-amber-900 dark:text-amber-100">Bank Transfer</h3>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">Manual verification required</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 py-4 border-t border-b border-amber-200 dark:border-amber-800">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">{planName} Plan</span>
|
||||
<span className="text-gray-900 dark:text-white">${planPrice} USD</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">Amount Transferred (PKR)</span>
|
||||
<span className="font-medium text-amber-700 dark:text-amber-300">PKR {pkrEquivalent.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Info Pointers */}
|
||||
<Card className="p-6 mb-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<CheckCircleIcon className="w-5 h-5 text-brand-500" />
|
||||
What happens next?
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 text-sm font-bold shrink-0 mt-0.5">1</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Verification in Progress</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Our team is reviewing your payment</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 text-sm font-bold shrink-0 mt-0.5">2</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Email Confirmation</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">You'll receive an email once approved</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400 text-sm font-bold shrink-0 mt-0.5">3</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Account Activated</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Your subscription will be activated automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Time Estimate Badge */}
|
||||
<div className="flex items-center justify-center gap-2 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl border border-blue-200 dark:border-blue-800">
|
||||
<AlertCircleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Expected approval time:</strong> Within 24 hours (usually faster)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Check Status Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="brand"
|
||||
size="lg"
|
||||
onClick={checkPaymentStatus}
|
||||
disabled={checkingStatus}
|
||||
className="w-full mt-6"
|
||||
>
|
||||
{checkingStatus ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2Icon className="w-5 h-5 animate-spin" />
|
||||
Checking status...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<RefreshCwIcon className="w-5 h-5" />
|
||||
Check Payment Status
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">
|
||||
Status is checked automatically every 30 seconds
|
||||
</p>
|
||||
|
||||
{/* Disabled Payment Options Notice */}
|
||||
<div className="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl opacity-60">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LockIcon className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Payment options disabled</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||
Other payment methods are disabled while your bank transfer is being verified.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-brand-50 dark:from-gray-900 dark:via-gray-900 dark:to-brand-950 py-12 px-4">
|
||||
<div className="max-w-xl mx-auto">
|
||||
@@ -222,13 +418,24 @@ export default function PendingPaymentView({
|
||||
<div className="space-y-3 py-4 border-t border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">{planName} Plan (Monthly)</span>
|
||||
<span className="text-gray-900 dark:text-white">${planPrice}</span>
|
||||
<span className="text-gray-900 dark:text-white">${planPrice} USD</span>
|
||||
</div>
|
||||
{showPKREquivalent && (
|
||||
<div className="flex justify-between text-sm text-brand-600 dark:text-brand-400 font-medium">
|
||||
<span>Bank Transfer Amount (PKR)</span>
|
||||
<span>PKR {pkrEquivalent.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-4">
|
||||
<span className="text-lg font-semibold text-gray-900 dark:text-white">Total</span>
|
||||
<span className="text-2xl font-bold text-brand-600 dark:text-brand-400">${planPrice}</span>
|
||||
<div className="text-right">
|
||||
<span className="text-2xl font-bold text-brand-600 dark:text-brand-400">${planPrice} USD</span>
|
||||
{showPKREquivalent && (
|
||||
<div className="text-sm font-medium text-brand-500">≈ PKR {pkrEquivalent.toLocaleString()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -320,16 +527,16 @@ export default function PendingPaymentView({
|
||||
Processing...
|
||||
</span>
|
||||
) : selectedGateway === 'manual' ? (
|
||||
'Continue to Bank Transfer'
|
||||
'Continue to Bank Transfer Details'
|
||||
) : (
|
||||
`Pay $${planPrice} Now`
|
||||
`Pay $${planPrice} USD Now`
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Info text */}
|
||||
<p className="mt-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{selectedGateway === 'manual'
|
||||
? 'You will receive bank details to complete your transfer'
|
||||
? 'View bank account details and submit your transfer proof'
|
||||
: 'You will be redirected to complete payment securely'
|
||||
}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user