billing realted PK bank transfer settigns
This commit is contained in:
@@ -879,6 +879,129 @@ class CreditPackageViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
{'count': paginator.page.paginator.count, 'next': paginator.get_next_link(), 'previous': paginator.get_previous_link(), 'results': results},
|
{'count': paginator.page.paginator.count, 'next': paginator.get_next_link(), 'previous': paginator.get_previous_link(), 'results': results},
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def purchase(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Purchase a credit package - creates an invoice for manual payment.
|
||||||
|
|
||||||
|
For Stripe/PayPal, use the dedicated checkout endpoints.
|
||||||
|
This endpoint is specifically for bank transfer/manual payments.
|
||||||
|
|
||||||
|
Request body:
|
||||||
|
{
|
||||||
|
"payment_method": "bank_transfer" // Required for manual/bank transfer
|
||||||
|
}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
"invoice_id": 123,
|
||||||
|
"invoice_number": "INV-2025-001",
|
||||||
|
"total_amount": "99.00",
|
||||||
|
"currency": "USD",
|
||||||
|
"status": "pending",
|
||||||
|
"message": "Invoice created. Please submit payment confirmation."
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
payment_method = request.data.get('payment_method', 'bank_transfer')
|
||||||
|
|
||||||
|
# Only allow manual/bank_transfer through this endpoint
|
||||||
|
if payment_method not in ['bank_transfer', 'manual', 'local_wallet']:
|
||||||
|
return error_response(
|
||||||
|
error='Use dedicated Stripe/PayPal endpoints for card payments',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
package = self.get_queryset().get(pk=pk)
|
||||||
|
except CreditPackage.DoesNotExist:
|
||||||
|
return error_response(
|
||||||
|
error='Credit package not found',
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the account
|
||||||
|
account = getattr(request, 'account', None)
|
||||||
|
if not account:
|
||||||
|
return error_response(
|
||||||
|
error='Account not found',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if account has an active status (allow purchases only for active accounts)
|
||||||
|
if account.status not in ['active', 'trial']:
|
||||||
|
return error_response(
|
||||||
|
error='Account must be active to purchase credits. Please complete your subscription first.',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for existing pending invoice for this package
|
||||||
|
existing_pending = Invoice.objects.filter(
|
||||||
|
account=account,
|
||||||
|
status='pending',
|
||||||
|
metadata__credit_package_id=package.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_pending:
|
||||||
|
return success_response(
|
||||||
|
data={
|
||||||
|
'invoice_id': existing_pending.id,
|
||||||
|
'invoice_number': existing_pending.invoice_number,
|
||||||
|
'total_amount': str(existing_pending.total),
|
||||||
|
'currency': existing_pending.currency,
|
||||||
|
'status': existing_pending.status,
|
||||||
|
'message': 'You already have a pending invoice for this credit package.',
|
||||||
|
'next_action': 'submit_payment'
|
||||||
|
},
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# Create invoice for the credit package
|
||||||
|
invoice = InvoiceService.create_credit_package_invoice(
|
||||||
|
account=account,
|
||||||
|
credit_package=package
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update invoice payment method
|
||||||
|
invoice.payment_method = payment_method
|
||||||
|
invoice.save()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Credit package invoice created: {invoice.invoice_number} "
|
||||||
|
f"for account {account.id}, package {package.name} ({package.credits} credits)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={
|
||||||
|
'invoice_id': invoice.id,
|
||||||
|
'invoice_number': invoice.invoice_number,
|
||||||
|
'total_amount': str(invoice.total),
|
||||||
|
'currency': invoice.currency,
|
||||||
|
'status': invoice.status,
|
||||||
|
'credit_package': {
|
||||||
|
'id': package.id,
|
||||||
|
'name': package.name,
|
||||||
|
'credits': package.credits,
|
||||||
|
},
|
||||||
|
'message': 'Invoice created successfully. Please submit your bank transfer payment confirmation.',
|
||||||
|
'next_action': 'submit_payment'
|
||||||
|
},
|
||||||
|
request=request,
|
||||||
|
status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create credit package invoice: {str(e)}", exc_info=True)
|
||||||
|
return error_response(
|
||||||
|
error=f'Failed to create invoice: {str(e)}',
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountPaymentMethodViewSet(AccountModelViewSet):
|
class AccountPaymentMethodViewSet(AccountModelViewSet):
|
||||||
|
|||||||
@@ -599,7 +599,7 @@ class CreditPackage(models.Model):
|
|||||||
|
|
||||||
# Display
|
# Display
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
features = models.JSONField(default=list, help_text="Bonus features or highlights")
|
features = models.JSONField(default=list, blank=True, help_text="Bonus features or highlights")
|
||||||
|
|
||||||
# Sort order
|
# Sort order
|
||||||
sort_order = models.IntegerField(default=0, help_text="Display order (lower = first)")
|
sort_order = models.IntegerField(default=0, help_text="Display order (lower = first)")
|
||||||
|
|||||||
@@ -182,16 +182,28 @@ class InvoiceService:
|
|||||||
local_currency = get_currency_for_country(account.billing_country) if account.billing_country else 'USD'
|
local_currency = get_currency_for_country(account.billing_country) if account.billing_country else 'USD'
|
||||||
local_equivalent = convert_usd_to_local(usd_price, account.billing_country) if local_currency != 'USD' else usd_price
|
local_equivalent = convert_usd_to_local(usd_price, account.billing_country) if local_currency != 'USD' else usd_price
|
||||||
|
|
||||||
|
# Get billing email from account
|
||||||
|
billing_email = account.billing_email
|
||||||
|
if not billing_email:
|
||||||
|
owner = account.users.filter(role='owner').first()
|
||||||
|
if owner:
|
||||||
|
billing_email = owner.email
|
||||||
|
|
||||||
invoice = Invoice.objects.create(
|
invoice = Invoice.objects.create(
|
||||||
account=account,
|
account=account,
|
||||||
invoice_number=InvoiceService.generate_invoice_number(account),
|
invoice_number=InvoiceService.generate_invoice_number(account),
|
||||||
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
|
||||||
status='pending',
|
status='pending',
|
||||||
currency=currency,
|
currency=currency,
|
||||||
invoice_date=invoice_date,
|
invoice_date=invoice_date,
|
||||||
due_date=invoice_date + timedelta(days=INVOICE_DUE_DATE_OFFSET),
|
due_date=invoice_date + timedelta(days=INVOICE_DUE_DATE_OFFSET),
|
||||||
metadata={
|
metadata={
|
||||||
|
'billing_snapshot': {
|
||||||
|
'email': billing_email,
|
||||||
|
'name': account.name,
|
||||||
|
'billing_address': account.billing_address or '',
|
||||||
|
},
|
||||||
'credit_package_id': credit_package.id,
|
'credit_package_id': credit_package.id,
|
||||||
|
'credit_package_name': credit_package.name,
|
||||||
'credit_amount': credit_package.credits,
|
'credit_amount': credit_package.credits,
|
||||||
'usd_price': str(credit_package.price), # Store original USD price
|
'usd_price': str(credit_package.price), # Store original USD price
|
||||||
'local_currency': local_currency, # Store local currency code for display
|
'local_currency': local_currency, # Store local currency code for display
|
||||||
@@ -233,15 +245,27 @@ class InvoiceService:
|
|||||||
notes: Invoice notes
|
notes: Invoice notes
|
||||||
due_date: Payment due date
|
due_date: Payment due date
|
||||||
"""
|
"""
|
||||||
|
# Get billing email
|
||||||
|
email = billing_email or account.billing_email
|
||||||
|
if not email:
|
||||||
|
owner = account.users.filter(role='owner').first()
|
||||||
|
if owner:
|
||||||
|
email = owner.email
|
||||||
|
|
||||||
invoice = Invoice.objects.create(
|
invoice = Invoice.objects.create(
|
||||||
account=account,
|
account=account,
|
||||||
invoice_number=InvoiceService.generate_invoice_number(account),
|
invoice_number=InvoiceService.generate_invoice_number(account),
|
||||||
billing_email=billing_email or account.billing_email or account.users.filter(role='owner').first().email,
|
|
||||||
status='draft',
|
status='draft',
|
||||||
currency='USD',
|
currency='USD',
|
||||||
notes=notes,
|
notes=notes,
|
||||||
invoice_date=timezone.now().date(),
|
invoice_date=timezone.now().date(),
|
||||||
due_date=due_date or (timezone.now() + timedelta(days=30))
|
due_date=due_date or (timezone.now() + timedelta(days=30)),
|
||||||
|
metadata={
|
||||||
|
'billing_snapshot': {
|
||||||
|
'email': email,
|
||||||
|
'name': account.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add all line items
|
# Add all line items
|
||||||
|
|||||||
@@ -559,11 +559,10 @@ export default function PlansAndBillingPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to manual/bank transfer flow
|
// For manual/bank transfer - use Stripe to create invoice, then pay via bank transfer
|
||||||
await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
|
// This creates the invoice which user can then pay using bank transfer from billing history
|
||||||
toast?.success?.('Plan upgraded successfully!');
|
toast?.info?.('Bank transfer requires an invoice. Please select Card payment to create an invoice, then use "Pay Now" with bank transfer option from Billing History.');
|
||||||
setShowUpgradeModal(false);
|
setShowUpgradeModal(false);
|
||||||
await loadData();
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
handleError(err, 'Failed to upgrade plan');
|
handleError(err, 'Failed to upgrade plan');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -597,10 +596,36 @@ export default function PlansAndBillingPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to manual/bank transfer flow
|
// For manual/bank transfer - create invoice and show payment modal
|
||||||
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'manual' });
|
const result = await purchaseCreditPackage({
|
||||||
toast?.success?.('Credits purchased successfully!');
|
package_id: packageId,
|
||||||
await loadData();
|
payment_method: 'bank_transfer'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.invoice_id) {
|
||||||
|
// Find the package to get details for the invoice display
|
||||||
|
const pkg = packages.find(p => p.id === packageId);
|
||||||
|
|
||||||
|
// Create invoice object for PayInvoiceModal
|
||||||
|
const invoiceForModal: Invoice = {
|
||||||
|
id: result.invoice_id,
|
||||||
|
invoice_number: result.invoice_number || '',
|
||||||
|
total: result.total_amount || '0',
|
||||||
|
total_amount: result.total_amount || '0',
|
||||||
|
currency: 'USD',
|
||||||
|
status: result.status || 'pending',
|
||||||
|
payment_method: 'bank_transfer',
|
||||||
|
subscription: null, // Credit purchase - no subscription
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the payment modal
|
||||||
|
setSelectedInvoice(invoiceForModal);
|
||||||
|
setShowPayInvoiceModal(true);
|
||||||
|
|
||||||
|
toast?.success?.(result.message || 'Invoice created! Please submit your payment confirmation.');
|
||||||
|
} else {
|
||||||
|
toast?.error?.(result.message || 'Failed to create invoice');
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
handleError(err, 'Failed to purchase credits');
|
handleError(err, 'Failed to purchase credits');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -674,12 +699,22 @@ export default function PlansAndBillingPage() {
|
|||||||
? Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100)
|
? Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Upgrade plans (exclude current and free)
|
// All paid plans for the upgrade panel (includes current plan)
|
||||||
const upgradePlans = plans.filter(p => {
|
const allPaidPlans = plans.filter(p => {
|
||||||
const price = Number(p.price) || 0;
|
const price = Number(p.price) || 0;
|
||||||
return price > 0 && p.id !== effectivePlanId;
|
return price > 0;
|
||||||
}).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
|
}).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
|
||||||
|
|
||||||
|
// Check if user is on the highest plan (by price)
|
||||||
|
const highestPlanPrice = allPaidPlans.length > 0
|
||||||
|
? Math.max(...allPaidPlans.map(p => Number(p.price) || 0))
|
||||||
|
: 0;
|
||||||
|
const currentPlanPrice = Number(currentPlan?.price) || 0;
|
||||||
|
const isOnHighestPlan = currentPlanPrice >= highestPlanPrice && highestPlanPrice > 0;
|
||||||
|
|
||||||
|
// Upgrade plans for modal (exclude current)
|
||||||
|
const upgradePlans = allPaidPlans.filter(p => p.id !== effectivePlanId);
|
||||||
|
|
||||||
// PAYMENT PROCESSING OVERLAY - Beautiful full-page loading with breathing badge
|
// PAYMENT PROCESSING OVERLAY - Beautiful full-page loading with breathing badge
|
||||||
if (paymentProcessing?.active) {
|
if (paymentProcessing?.active) {
|
||||||
const stageConfig = {
|
const stageConfig = {
|
||||||
@@ -860,14 +895,16 @@ export default function PlansAndBillingPage() {
|
|||||||
Manage Billing
|
Manage Billing
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{!isOnHighestPlan && (
|
||||||
variant="primary"
|
<Button
|
||||||
tone="brand"
|
variant="primary"
|
||||||
onClick={() => setShowUpgradeModal(true)}
|
tone="brand"
|
||||||
startIcon={<ArrowUpIcon className="w-4 h-4" />}
|
onClick={() => setShowUpgradeModal(true)}
|
||||||
>
|
startIcon={<ArrowUpIcon className="w-4 h-4" />}
|
||||||
Upgrade
|
>
|
||||||
</Button>
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1043,14 +1080,14 @@ export default function PlansAndBillingPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Payment Method Selector - Clear buttons */}
|
{/* Payment Method Selector - Clear buttons */}
|
||||||
{(availableGateways.stripe || availableGateways.paypal) && (
|
{(availableGateways.stripe || availableGateways.paypal || availableGateways.manual) && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Label className="text-xs text-gray-500 dark:text-gray-400 mb-2 block">Payment Method</Label>
|
<Label className="text-xs text-gray-500 dark:text-gray-400 mb-2 block">Payment Method</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{availableGateways.stripe && (
|
{availableGateways.stripe && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedGateway('stripe')}
|
onClick={() => setSelectedGateway('stripe')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
className={`flex-1 min-w-[140px] flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
||||||
selectedGateway === 'stripe'
|
selectedGateway === 'stripe'
|
||||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
@@ -1063,7 +1100,7 @@ export default function PlansAndBillingPage() {
|
|||||||
{availableGateways.paypal && (
|
{availableGateways.paypal && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedGateway('paypal')}
|
onClick={() => setSelectedGateway('paypal')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
className={`flex-1 min-w-[140px] flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
||||||
selectedGateway === 'paypal'
|
selectedGateway === 'paypal'
|
||||||
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
@@ -1073,6 +1110,19 @@ export default function PlansAndBillingPage() {
|
|||||||
<span className="text-sm font-medium">PayPal</span>
|
<span className="text-sm font-medium">PayPal</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{availableGateways.manual && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedGateway('manual')}
|
||||||
|
className={`flex-1 min-w-[140px] flex items-center justify-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
||||||
|
selectedGateway === 'manual'
|
||||||
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Building2Icon className={`w-5 h-5 ${selectedGateway === 'manual' ? 'text-brand-600' : 'text-gray-500'}`} />
|
||||||
|
<span className="text-sm font-medium">Bank Transfer</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1117,56 +1167,87 @@ export default function PlansAndBillingPage() {
|
|||||||
<ShootingStarIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
<ShootingStarIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white">Upgrade Plan</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Get more features & credits</p>
|
{isOnHighestPlan ? 'Your Plan' : 'Upgrade Plan'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{isOnHighestPlan ? 'You are on the highest plan' : 'Get more features & credits'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{!isOnHighestPlan && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
tone="brand"
|
variant="outline"
|
||||||
onClick={() => setShowUpgradeModal(true)}
|
tone="brand"
|
||||||
>
|
onClick={() => setShowUpgradeModal(true)}
|
||||||
View All
|
>
|
||||||
</Button>
|
View All
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{upgradePlans.slice(0, 3).map((plan) => (
|
{allPaidPlans.map((plan) => {
|
||||||
<div
|
const isCurrentPlan = plan.id === effectivePlanId;
|
||||||
key={plan.id}
|
const currentPriceVal = Number(currentPlan?.price) || 0;
|
||||||
className="p-4 border border-gray-200 dark:border-gray-700 rounded-xl flex items-center justify-between hover:border-purple-300 dark:hover:border-purple-700 transition-colors"
|
const thisPlanPrice = Number(plan.price) || 0;
|
||||||
>
|
const isUpgrade = thisPlanPrice > currentPriceVal;
|
||||||
<div>
|
const isDowngrade = thisPlanPrice < currentPriceVal;
|
||||||
<div className="font-semibold text-gray-900 dark:text-white">{plan.name}</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
return (
|
||||||
{plan.included_credits?.toLocaleString() || 0} credits/mo
|
<div
|
||||||
</div>
|
key={plan.id}
|
||||||
</div>
|
className={`p-4 border rounded-xl flex items-center justify-between transition-colors ${
|
||||||
<div className="text-right">
|
isCurrentPlan
|
||||||
<div className="font-bold text-gray-900 dark:text-white">
|
? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/20'
|
||||||
{formatCurrency(plan.price, 'USD')}/mo
|
: 'border-gray-200 dark:border-gray-700 hover:border-purple-300 dark:hover:border-purple-700'
|
||||||
</div>
|
}`}
|
||||||
{billingCountry === 'PK' && (
|
>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
<div>
|
||||||
≈ PKR {convertUSDToPKR(plan.price).toLocaleString()}/mo
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">{plan.name}</span>
|
||||||
|
{isCurrentPlan && (
|
||||||
|
<Badge variant="soft" tone="brand" size="sm">Current</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
<Button
|
{plan.included_credits?.toLocaleString() || 0} credits/mo
|
||||||
size="sm"
|
</div>
|
||||||
variant="ghost"
|
</div>
|
||||||
tone="brand"
|
<div className="text-right">
|
||||||
onClick={() => handleSelectPlan(plan.id)}
|
<div className="font-bold text-gray-900 dark:text-white">
|
||||||
disabled={planLoadingId === plan.id}
|
{formatCurrency(plan.price, 'USD')}/mo
|
||||||
>
|
</div>
|
||||||
{planLoadingId === plan.id ? <Loader2Icon className="w-4 h-4 animate-spin" /> : 'Select'}
|
{billingCountry === 'PK' && (
|
||||||
</Button>
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
≈ PKR {convertUSDToPKR(plan.price).toLocaleString()}/mo
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCurrentPlan && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
tone={isUpgrade ? 'brand' : 'neutral'}
|
||||||
|
onClick={() => handleSelectPlan(plan.id)}
|
||||||
|
disabled={planLoadingId === plan.id}
|
||||||
|
>
|
||||||
|
{planLoadingId === plan.id ? (
|
||||||
|
<Loader2Icon className="w-4 h-4 animate-spin" />
|
||||||
|
) : isUpgrade ? (
|
||||||
|
'Upgrade'
|
||||||
|
) : (
|
||||||
|
'Downgrade'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
{upgradePlans.length === 0 && (
|
{allPaidPlans.length === 0 && (
|
||||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
<CheckCircleIcon className="w-8 h-8 mx-auto mb-2 text-success-500" />
|
<CheckCircleIcon className="w-8 h-8 mx-auto mb-2 text-success-500" />
|
||||||
<p>You're on the best plan!</p>
|
<p>No plans available</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1360,8 +1441,10 @@ export default function PlansAndBillingPage() {
|
|||||||
|
|
||||||
{/* Upgrade Modal */}
|
{/* Upgrade Modal */}
|
||||||
{showUpgradeModal && (
|
{showUpgradeModal && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 z-99999 flex items-center justify-center overflow-y-auto modal">
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-auto">
|
{/* Backdrop */}
|
||||||
|
<div className="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[32px] dark:bg-gray-900/70" onClick={() => setShowUpgradeModal(false)}></div>
|
||||||
|
<div className="relative bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-auto mx-4">
|
||||||
<div className="sticky top-0 bg-white dark:bg-gray-900 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700 z-10">
|
<div className="sticky top-0 bg-white dark:bg-gray-900 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700 z-10">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Choose Your Plan</h2>
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Choose Your Plan</h2>
|
||||||
@@ -1449,7 +1532,7 @@ export default function PlansAndBillingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Plans Grid */}
|
{/* Plans Grid */}
|
||||||
<div className="p-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-4 max-w-3xl mx-auto">
|
||||||
{upgradePlans.map((plan, index) => {
|
{upgradePlans.map((plan, index) => {
|
||||||
const isPopular = index === 1;
|
const isPopular = index === 1;
|
||||||
const planPrice = Number(plan.price) || 0;
|
const planPrice = Number(plan.price) || 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user