Files
igny8/frontend/src/components/billing/PendingPaymentView.tsx
2026-01-07 13:02:53 +00:00

340 lines
12 KiB
TypeScript

/**
* PendingPaymentView - Full-page view for new users with pending payment
*
* This is shown to users who:
* - Just signed up with a paid plan
* - Have account.status === 'pending_payment'
* - Have NOT made any successful payments yet
*
* Payment methods are shown based on user's country:
* - Global: Stripe (Credit/Debit Card) + PayPal
* - Pakistan (PK): Stripe (Credit/Debit Card) + Bank Transfer
*/
import { useState, useEffect } from 'react';
import {
CreditCardIcon,
Building2Icon,
CheckCircleIcon,
AlertCircleIcon,
Loader2Icon,
ArrowLeftIcon,
LockIcon,
} 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 BankTransferForm from './BankTransferForm';
import {
Invoice,
getAvailablePaymentGateways,
subscribeToPlan,
type PaymentGateway,
} from '../../services/billing.api';
// PayPal icon component
const PayPalIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"/>
</svg>
);
interface PaymentOption {
id: string;
type: PaymentGateway;
name: string;
description: string;
icon: React.ReactNode;
}
interface PendingPaymentViewProps {
invoice: Invoice | null;
userCountry: string;
planName: string;
planPrice: string;
onPaymentSuccess: () => void;
}
export default function PendingPaymentView({
invoice,
userCountry,
planName,
planPrice,
onPaymentSuccess,
}: PendingPaymentViewProps) {
const toast = useToast();
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);
const isPakistan = userCountry === 'PK';
// Load available payment gateways
useEffect(() => {
const loadGateways = async () => {
setGatewaysLoading(true);
try {
const gateways = await getAvailablePaymentGateways();
const options: PaymentOption[] = [];
// Always show Stripe (Credit Card) if available
if (gateways.stripe) {
options.push({
id: 'stripe',
type: 'stripe',
name: 'Credit/Debit Card',
description: 'Pay securely with Visa, Mastercard, or other cards',
icon: <CreditCardIcon className="w-6 h-6" />,
});
}
// 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" />,
});
}
}
setPaymentOptions(options);
if (options.length > 0) {
setSelectedGateway(options[0].type);
}
} catch (error) {
console.error('Failed to load payment gateways:', error);
// Fallback to Stripe
setPaymentOptions([{
id: 'stripe',
type: 'stripe',
name: 'Credit/Debit Card',
description: 'Pay securely with Visa, Mastercard, or other cards',
icon: <CreditCardIcon className="w-6 h-6" />,
}]);
} finally {
setGatewaysLoading(false);
}
};
loadGateways();
}, [isPakistan]);
const handlePayNow = async () => {
if (!invoice) {
toast?.error?.('No invoice found');
return;
}
// For bank transfer, show the bank transfer form
if (selectedGateway === 'manual') {
setShowBankTransfer(true);
return;
}
setLoading(true);
try {
// Get plan ID from invoice subscription
const planId = invoice.subscription?.plan?.id;
if (!planId) {
throw new Error('Plan information not found');
}
// Create checkout session
const { redirect_url } = await subscribeToPlan(planId.toString(), selectedGateway);
// Redirect to payment gateway
window.location.href = redirect_url;
} catch (error: any) {
console.error('Payment initiation failed:', error);
toast?.error?.(error.message || 'Failed to start payment process');
} finally {
setLoading(false);
}
};
// If showing bank transfer form
if (showBankTransfer && invoice) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
<div className="max-w-2xl mx-auto">
<button
onClick={() => setShowBankTransfer(false)}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6"
>
<ArrowLeftIcon className="w-4 h-4" />
Back to payment options
</button>
<BankTransferForm
invoice={invoice}
onSuccess={() => {
setShowBankTransfer(false);
onPaymentSuccess();
}}
onCancel={() => setShowBankTransfer(false)}
/>
</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">
{/* Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-brand-100 dark:bg-brand-900/30 mb-4">
<CheckCircleIcon className="w-8 h-8 text-brand-600 dark:text-brand-400" />
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Account Created!
</h1>
<p className="text-gray-600 dark:text-gray-400">
Complete your payment to activate your <strong>{planName}</strong> plan
</p>
</div>
{/* Plan Summary Card */}
<Card className="p-6 mb-6 border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Order Summary</h2>
<Badge variant="outline" tone="brand">{planName}</Badge>
</div>
<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>
</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>
</Card>
{/* Payment Method Selection */}
<Card className="p-6 mb-6 border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Select Payment Method
</h2>
{gatewaysLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2Icon className="w-6 h-6 animate-spin text-brand-500 mr-2" />
<span className="text-gray-500">Loading payment options...</span>
</div>
) : paymentOptions.length === 0 ? (
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
<div className="flex items-center gap-2">
<AlertCircleIcon className="w-5 h-5 text-warning-600" />
<span className="text-warning-800 dark:text-warning-200">No payment methods available</span>
</div>
</div>
) : (
<div className="space-y-3">
{paymentOptions.map((option) => (
<button
key={option.id}
type="button"
onClick={() => setSelectedGateway(option.type)}
className={`relative w-full p-4 rounded-xl border-2 text-left transition-all ${
selectedGateway === option.type
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 bg-white dark:bg-gray-800'
}`}
>
{selectedGateway === option.type && (
<div className="absolute top-3 right-3">
<div className="w-5 h-5 bg-brand-500 rounded-full flex items-center justify-center">
<CheckCircleIcon className="w-3 h-3 text-white" />
</div>
</div>
)}
<div className="flex items-center gap-4">
<div className={`flex items-center justify-center w-12 h-12 rounded-lg ${
selectedGateway === option.type
? 'bg-brand-500 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
}`}>
{option.icon}
</div>
<div>
<h3 className={`font-semibold ${
selectedGateway === option.type
? 'text-brand-700 dark:text-brand-400'
: 'text-gray-900 dark:text-white'
}`}>
{option.name}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{option.description}
</p>
</div>
</div>
</button>
))}
</div>
)}
{/* Security Badge */}
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<LockIcon className="w-4 h-4 text-green-600" />
<span className="text-xs text-gray-500 dark:text-gray-400">
Your payment information is secure and encrypted
</span>
</div>
</Card>
{/* Pay Now Button */}
<Button
variant="primary"
tone="brand"
size="lg"
onClick={handlePayNow}
disabled={loading || gatewaysLoading || paymentOptions.length === 0}
className="w-full"
>
{loading ? (
<span className="flex items-center justify-center">
<Loader2Icon className="w-5 h-5 animate-spin mr-2" />
Processing...
</span>
) : selectedGateway === 'manual' ? (
'Continue to Bank Transfer'
) : (
`Pay $${planPrice} 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'
: 'You will be redirected to complete payment securely'
}
</p>
</div>
</div>
);
}