FInal bank, stripe and paypal sandbox completed

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-08 00:12:41 +00:00
parent ad75fa031e
commit 7ad1f6bdff
19 changed files with 2622 additions and 375 deletions

View File

@@ -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>

View File

@@ -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>