diff --git a/frontend/src/components/billing/PendingPaymentView.tsx b/frontend/src/components/billing/PendingPaymentView.tsx
index c8cebfe2..7e78f2ec 100644
--- a/frontend/src/components/billing/PendingPaymentView.tsx
+++ b/frontend/src/components/billing/PendingPaymentView.tsx
@@ -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 }) => (
);
+// Currency symbol helper
+const getCurrencySymbol = (currency: string): string => {
+ const symbols: Record = {
+ 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('stripe');
const [loading, setLoading] = useState(false);
const [gatewaysLoading, setGatewaysLoading] = useState(true);
const [paymentOptions, setPaymentOptions] = useState([]);
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: ,
- });
- }
- } else {
- if (gateways.paypal) {
- options.push({
- id: 'paypal',
- type: 'paypal',
- name: 'PayPal',
- description: 'Pay with your PayPal account',
- icon: ,
- });
- }
+ // 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: ,
+ });
+ }
+
+ // 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: ,
+ });
}
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 (
+
+
+ {/* Header with Awaiting Badge */}
+
+
+
+
+
+
+ Awaiting Approval
+
+
+
+ Payment Submitted!
+
+
+ Your bank transfer for {planName} is being verified
+
+
+
+ {/* Status Card */}
+
+
+
+
+
+
+
Bank Transfer
+
Manual verification required
+
+
+
+
+
+ {planName} Plan
+ ${planPrice} USD
+
+
+ Amount Transferred (PKR)
+ PKR {pkrEquivalent.toLocaleString()}
+
+
+
+
+ {/* Info Pointers */}
+
+
+
+ What happens next?
+
+
+
+
1
+
+
Verification in Progress
+
Our team is reviewing your payment
+
+
+
+
2
+
+
Email Confirmation
+
You'll receive an email once approved
+
+
+
+
3
+
+
Account Activated
+
Your subscription will be activated automatically
+
+
+
+
+
+ {/* Time Estimate Badge */}
+
+
+
+ Expected approval time: Within 24 hours (usually faster)
+
+
+
+ {/* Check Status Button */}
+
+
+ Status is checked automatically every 30 seconds
+
+
+ {/* Disabled Payment Options Notice */}
+
+
+
+ Payment options disabled
+
+
+ Other payment methods are disabled while your bank transfer is being verified.
+
+
+
+
+ );
+ }
+
return (
@@ -222,13 +418,24 @@ export default function PendingPaymentView({
{planName} Plan (Monthly)
- ${planPrice}
+ ${planPrice} USD
+ {showPKREquivalent && (
+
+ Bank Transfer Amount (PKR)
+ PKR {pkrEquivalent.toLocaleString()}
+
+ )}
Total
- ${planPrice}
+
+ ${planPrice} USD
+ {showPKREquivalent && (
+
≈ PKR {pkrEquivalent.toLocaleString()}
+ )}
+
@@ -320,16 +527,16 @@ export default function PendingPaymentView({
Processing...
) : selectedGateway === 'manual' ? (
- 'Continue to Bank Transfer'
+ 'Continue to Bank Transfer Details'
) : (
- `Pay $${planPrice} Now`
+ `Pay $${planPrice} USD Now`
)}
{/* Info text */}
{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'
}
diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx
index 275f154d..2e88bac9 100644
--- a/frontend/src/layout/AppLayout.tsx
+++ b/frontend/src/layout/AppLayout.tsx
@@ -166,7 +166,10 @@ const LayoutContent: React.FC = () => {
>
{/* Pending Payment Banner - Shows when account status is 'pending_payment' */}
-
+ {/* Hidden on /account/plans since PendingPaymentView handles it there */}
+ {!window.location.pathname.startsWith('/account/plans') && (
+
+ )}
diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx
index bb4cf4e1..848e074e 100644
--- a/frontend/src/pages/account/PlansAndBillingPage.tsx
+++ b/frontend/src/pages/account/PlansAndBillingPage.tsx
@@ -5,6 +5,7 @@
*/
import { useState, useEffect, useRef } from 'react';
+import { createPortal } from 'react-dom';
import { Link } from 'react-router-dom';
import {
CreditCardIcon,
@@ -39,6 +40,7 @@ import PageHeader from '../../components/common/PageHeader';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { usePageLoading } from '../../context/PageLoadingContext';
import { formatCurrency } from '../../utils';
+import { fetchAPI } from '../../services/api';
import {
getCreditBalance,
getCreditPackages,
@@ -69,12 +71,49 @@ import { useAuthStore } from '../../store/authStore';
import PayInvoiceModal from '../../components/billing/PayInvoiceModal';
import PendingPaymentView from '../../components/billing/PendingPaymentView';
+/**
+ * Helper function to determine the effective currency based on billing country and payment method
+ * - PKR for Pakistan users using bank_transfer
+ * - USD for all other cases (Stripe, PayPal, or non-PK countries)
+ */
+const getCurrencyForDisplay = (billingCountry: string, paymentMethod?: string): string => {
+ if (billingCountry === 'PK' && paymentMethod === 'bank_transfer') {
+ return 'PKR';
+ }
+ return 'USD';
+};
+
+/**
+ * Convert USD price to PKR using approximate exchange rate
+ * Backend uses 278 PKR per USD
+ * Rounds to nearest thousand for cleaner display
+ */
+const convertUSDToPKR = (usdAmount: string | number): number => {
+ const amount = typeof usdAmount === 'string' ? parseFloat(usdAmount) : usdAmount;
+ const pkr = amount * 278;
+ return Math.round(pkr / 1000) * 1000; // Round to nearest thousand
+};
+
export default function PlansAndBillingPage() {
const { startLoading, stopLoading } = usePageLoading();
const toast = useToast();
const hasLoaded = useRef(false);
- const { user } = useAuthStore.getState();
+
+ // FIX: Subscribe to user changes from Zustand store (reactive)
+ const user = useAuthStore((state) => state.user);
+ const refreshUser = useAuthStore((state) => state.refreshUser);
+
const isAwsAdmin = user?.account?.slug === 'aws-admin';
+
+ // Track if initial data has been loaded to prevent flash
+ const [initialDataLoaded, setInitialDataLoaded] = useState(false);
+
+ // Payment processing state - shows beautiful loading UI
+ const [paymentProcessing, setPaymentProcessing] = useState<{
+ active: boolean;
+ stage: 'verifying' | 'processing' | 'finalizing' | 'activating';
+ message: string;
+ } | null>(null);
// UI States
const [error, setError] = useState('');
@@ -99,7 +138,7 @@ export default function PlansAndBillingPage() {
const [availableGateways, setAvailableGateways] = useState<{ stripe: boolean; paypal: boolean; manual: boolean }>({
stripe: false,
paypal: false,
- manual: true,
+ manual: false, // FIX: Initialize as false, will be set based on country
});
useEffect(() => {
@@ -109,65 +148,280 @@ export default function PlansAndBillingPage() {
// Handle payment gateway return URLs BEFORE loadData
const params = new URLSearchParams(window.location.search);
const success = params.get('success');
+ const sessionId = params.get('session_id'); // Stripe session ID
const canceled = params.get('canceled');
const purchase = params.get('purchase');
const paypalStatus = params.get('paypal');
- const paypalToken = params.get('token'); // PayPal order ID
+ const paypalToken = params.get('token'); // PayPal token from URL
const planIdParam = params.get('plan_id');
const packageIdParam = params.get('package_id');
- const { refreshUser } = useAuthStore.getState();
+
+ // Don't destructure from getState - use hooks above instead
- // Handle PayPal return - MUST capture the order to complete payment
- // Do this BEFORE loadData to ensure payment is processed first
+ // ============================================================================
+ // PAYMENT RETURN LOGGING - Comprehensive debug output
+ // ============================================================================
+ const LOG_PREFIX = '[PAYMENT-RETURN]';
+
+ console.group(`${LOG_PREFIX} Payment Return Flow Started`);
+ console.log(`${LOG_PREFIX} Full URL:`, window.location.href);
+ console.log(`${LOG_PREFIX} Session ID:`, sessionId);
+
+ // Detect which payment flow we're in
+ const paymentFlow =
+ (paypalStatus === 'success' && paypalToken) ? 'PAYPAL_SUCCESS' :
+ paypalStatus === 'cancel' ? 'PAYPAL_CANCEL' :
+ (success === 'true' && sessionId) ? 'STRIPE_SUCCESS_WITH_SESSION' :
+ success === 'true' ? 'STRIPE_SUCCESS_NO_SESSION' :
+ canceled === 'true' ? 'STRIPE_CANCELED' :
+ purchase === 'success' ? 'CREDIT_PURCHASE_SUCCESS' :
+ purchase === 'canceled' ? 'CREDIT_PURCHASE_CANCELED' :
+ 'NO_PAYMENT_RETURN';
+
+ console.log(`${LOG_PREFIX} ===== DETECTED PAYMENT FLOW =====`);
+ console.log(`${LOG_PREFIX} Flow type:`, paymentFlow);
+ console.groupEnd();
+
+ // Handle PayPal return - Get order_id from localStorage and capture
if (paypalStatus === 'success' && paypalToken) {
- // Import and capture PayPal order
- import('../../services/billing.api').then(({ capturePayPalOrder }) => {
- toast?.info?.('Completing PayPal payment...');
- capturePayPalOrder(paypalToken, {
- plan_id: planIdParam || undefined,
- package_id: packageIdParam || undefined,
- })
- .then(() => {
- toast?.success?.('Payment completed successfully!');
- refreshUser().catch(() => {});
- // Reload the page to get fresh data
- window.history.replaceState({}, '', window.location.pathname);
- window.location.reload();
- })
- .catch((err) => {
- console.error('PayPal capture error:', err);
- toast?.error?.(err?.message || 'Failed to complete PayPal payment');
- window.history.replaceState({}, '', window.location.pathname);
- });
+ console.group(`${LOG_PREFIX} PayPal Success Flow`);
+
+ // FIX: Retrieve order_id from localStorage (stored before redirect)
+ const storedOrderId = localStorage.getItem('paypal_order_id');
+
+ console.log(`${LOG_PREFIX} PayPal token from URL:`, paypalToken);
+ console.log(`${LOG_PREFIX} Stored order_id from localStorage:`, storedOrderId);
+ console.log(`${LOG_PREFIX} plan_id:`, planIdParam);
+ console.log(`${LOG_PREFIX} package_id:`, packageIdParam);
+
+ if (!storedOrderId) {
+ console.error(`${LOG_PREFIX} ❌ CRITICAL: No order_id in localStorage!`);
+ console.log(`${LOG_PREFIX} This means order_id was not saved before redirect to PayPal`);
+ console.groupEnd();
+ toast?.error?.('Payment not captured - order ID missing. Please try again.');
+ window.history.replaceState({}, '', window.location.pathname);
+ loadData(); // Still load data to show current state
+ return;
+ }
+
+ console.log(`${LOG_PREFIX} ✓ Order ID found, proceeding to capture...`);
+
+ // Show payment processing UI for PayPal
+ setPaymentProcessing({
+ active: true,
+ stage: 'processing',
+ message: 'Completing PayPal payment...'
});
+
+ // Clean URL immediately
+ window.history.replaceState({}, '', window.location.pathname);
+
+ // Import and capture PayPal order
+ import('../../services/billing.api').then(async ({ capturePayPalOrder }) => {
+ try {
+ const captureResponse = await capturePayPalOrder(storedOrderId, {
+ plan_id: planIdParam || undefined,
+ package_id: packageIdParam || undefined,
+ });
+
+ console.log(`${LOG_PREFIX} ✓ PayPal capture SUCCESS!`, captureResponse);
+ localStorage.removeItem('paypal_order_id');
+
+ // Update stage
+ setPaymentProcessing({
+ active: true,
+ stage: 'activating',
+ message: 'Activating your subscription...'
+ });
+
+ // Refresh user data - IMPORTANT: wait for this!
+ try {
+ await refreshUser();
+ console.log(`${LOG_PREFIX} ✓ User refreshed`);
+ } catch (refreshErr) {
+ console.error(`${LOG_PREFIX} User refresh failed:`, refreshErr);
+ }
+
+ // Short delay then complete
+ setTimeout(() => {
+ setPaymentProcessing(null);
+ toast?.success?.('Payment completed successfully!');
+ loadData();
+ }, 500);
+
+ } catch (err: any) {
+ console.error(`${LOG_PREFIX} ❌ PayPal capture FAILED:`, err);
+ localStorage.removeItem('paypal_order_id');
+ setPaymentProcessing(null);
+ toast?.error?.(err?.message || 'Failed to complete PayPal payment');
+ loadData();
+ }
+ });
+
+ console.groupEnd();
return; // Don't load data yet, wait for capture to complete
} else if (paypalStatus === 'cancel') {
+ console.log(`${LOG_PREFIX} PayPal payment was cancelled by user`);
+ localStorage.removeItem('paypal_order_id'); // Clear on cancellation
toast?.info?.('PayPal payment was cancelled');
window.history.replaceState({}, '', window.location.pathname);
}
- // Handle Stripe success
- else if (success === 'true') {
+ // Handle Stripe return - Verify payment with backend
+ else if (success === 'true' && sessionId) {
+ console.log(`${LOG_PREFIX} Stripe Success Flow - Session:`, sessionId);
+
+ // Show beautiful processing UI
+ setPaymentProcessing({
+ active: true,
+ stage: 'verifying',
+ message: 'Verifying your payment...'
+ });
+
+ // Clean URL immediately
+ window.history.replaceState({}, '', window.location.pathname);
+
+ fetchAPI(`/v1/billing/stripe/verify-return/?session_id=${sessionId}`)
+ .then(async (data) => {
+ console.log(`${LOG_PREFIX} Verification response:`, data);
+
+ if (data.payment_processed) {
+ // Payment already processed by webhook!
+ setPaymentProcessing({
+ active: true,
+ stage: 'activating',
+ message: 'Activating your subscription...'
+ });
+
+ // Refresh user to get updated account status
+ try {
+ await refreshUser();
+ console.log(`${LOG_PREFIX} User refreshed successfully`);
+ } catch (err) {
+ console.error(`${LOG_PREFIX} User refresh failed:`, err);
+ }
+
+ // Short delay for UX, then show success
+ setTimeout(() => {
+ setPaymentProcessing(null);
+ toast?.success?.('Payment successful! Your account is now active.');
+ loadData();
+ }, 500);
+ } else if (data.should_poll) {
+ // Webhook hasn't fired yet, poll for status
+ setPaymentProcessing({
+ active: true,
+ stage: 'processing',
+ message: 'Processing your payment...'
+ });
+ pollPaymentStatus(sessionId);
+ } else {
+ setPaymentProcessing(null);
+ toast?.warning?.(data.message || 'Payment verification pending');
+ loadData();
+ }
+ })
+ .catch(err => {
+ console.error(`${LOG_PREFIX} Verification failed:`, err);
+ setPaymentProcessing(null);
+ toast?.warning?.('Payment verification pending. Please refresh the page.');
+ loadData();
+ });
+
+ console.groupEnd();
+ return;
+ } else if (success === 'true') {
+ // Stripe return without session_id (old flow fallback)
+ console.log(`${LOG_PREFIX} Stripe success without session_id (legacy flow)`);
toast?.success?.('Subscription activated successfully!');
- // Refresh user to get updated account status (removes pending_payment banner)
refreshUser().catch(() => {});
- // Clean up URL
window.history.replaceState({}, '', window.location.pathname);
} else if (canceled === 'true') {
+ console.log(`${LOG_PREFIX} Stripe payment was cancelled`);
toast?.info?.('Payment was cancelled');
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'success') {
+ console.log(`${LOG_PREFIX} Credit purchase success`);
toast?.success?.('Credits purchased successfully!');
- // Refresh user to get updated credit balance and account status
refreshUser().catch(() => {});
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'canceled') {
+ console.log(`${LOG_PREFIX} Credit purchase cancelled`);
toast?.info?.('Credit purchase was cancelled');
window.history.replaceState({}, '', window.location.pathname);
+ } else {
+ console.log(`${LOG_PREFIX} No payment return parameters detected, loading page normally`);
+ }
+
+ // Helper function to poll payment status with beautiful UI updates
+ async function pollPaymentStatus(sessionId: string, attempts = 0) {
+ const maxAttempts = 15; // Increased to 15 attempts
+ console.log(`${LOG_PREFIX} [POLL] Attempt ${attempts + 1}/${maxAttempts}`);
+
+ // Update processing stage based on attempt count
+ if (attempts === 3) {
+ setPaymentProcessing({
+ active: true,
+ stage: 'finalizing',
+ message: 'Finalizing your payment...'
+ });
+ }
+
+ if (attempts >= maxAttempts) {
+ console.warn(`${LOG_PREFIX} [POLL] Max attempts reached`);
+ setPaymentProcessing(null);
+ toast?.warning?.('Payment is being processed. Please refresh the page in a moment.');
+ loadData();
+ return;
+ }
+
+ // Faster initial polls (800ms), slower later (1.5s)
+ const pollDelay = attempts < 5 ? 800 : 1500;
+
+ setTimeout(async () => {
+ try {
+ const data = await fetchAPI(`/v1/billing/stripe/verify-return/?session_id=${sessionId}`);
+
+ if (data.payment_processed) {
+ console.log(`${LOG_PREFIX} [POLL] Payment processed!`);
+
+ // Show activating stage
+ setPaymentProcessing({
+ active: true,
+ stage: 'activating',
+ message: 'Activating your subscription...'
+ });
+
+ // Refresh user data
+ try {
+ await refreshUser();
+ console.log(`${LOG_PREFIX} [POLL] User refreshed`);
+ } catch (err) {
+ console.error(`${LOG_PREFIX} [POLL] User refresh failed:`, err);
+ }
+
+ // Short delay then complete
+ setTimeout(() => {
+ setPaymentProcessing(null);
+ toast?.success?.('Payment successful! Your account is now active.');
+ loadData();
+ }, 500);
+ } else {
+ // Continue polling
+ pollPaymentStatus(sessionId, attempts + 1);
+ }
+ } catch (pollErr) {
+ console.error(`${LOG_PREFIX} [POLL] Error:`, pollErr);
+ setPaymentProcessing(null);
+ toast?.warning?.('Please refresh page to see updated status.');
+ loadData();
+ }
+ }, pollDelay);
}
// Load data after handling return URLs
loadData();
- }, []);
+ console.groupEnd();
+ }, [refreshUser]);
const handleError = (err: any, fallback: string) => {
const message = err?.message || fallback;
@@ -245,7 +499,9 @@ export default function PlansAndBillingPage() {
// Load available payment gateways and sync with user's payment method
try {
- const gateways = await getAvailablePaymentGateways();
+ // FIX: Pass billing country to filter payment gateways correctly
+ const billingCountry = user?.account?.billing_country || 'US';
+ const gateways = await getAvailablePaymentGateways(billingCountry);
setAvailableGateways(gateways);
// Use user's verified payment method to set gateway
@@ -288,6 +544,7 @@ export default function PlansAndBillingPage() {
}
} finally {
stopLoading();
+ setInitialDataLoaded(true);
}
};
@@ -382,19 +639,33 @@ export default function PlansAndBillingPage() {
const accountPlanId = user?.account?.plan?.id;
const effectivePlanId = currentPlanId || accountPlanId;
const currentPlan = plans.find((p) => p.id === effectivePlanId) || user?.account?.plan;
- const hasActivePlan = Boolean(effectivePlanId);
- const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
+
+ // FIX: hasActivePlan should check account status, not just plan existence
+ const accountStatus = user?.account?.status || '';
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
+ const hasActivePlan = accountStatus === 'active'
+ && effectivePlanId
+ && currentPlan?.slug !== 'free'
+ && !hasPendingInvoice;
+
+ const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
// Detect new user pending payment scenario:
// - account status is 'pending_payment'
// - user has never made a successful payment
- const accountStatus = user?.account?.status || '';
const hasEverPaid = payments.some((p) => p.status === 'succeeded' || p.status === 'completed');
const isNewUserPendingPayment = accountStatus === 'pending_payment' && !hasEverPaid;
const pendingInvoice = invoices.find((inv) => inv.status === 'pending');
const billingCountry = (user?.account as any)?.billing_country || 'US';
+ // FIX: canManageBilling should check if user actually paid via Stripe
+ const userPaymentMethod = (user?.account as any)?.payment_method || '';
+ const hasStripeCustomerId = !!(user?.account as any)?.stripe_customer_id;
+ const canManageBilling = userPaymentMethod === 'stripe' && hasStripeCustomerId && hasActivePlan;
+
+ // Determine effective currency for display based on country and payment method
+ const effectiveCurrency = getCurrencyForDisplay(billingCountry, userPaymentMethod);
+
// Combined check: disable Buy Credits if no active plan OR has pending invoice
const canBuyCredits = hasActivePlan && !hasPendingInvoice;
@@ -409,18 +680,99 @@ export default function PlansAndBillingPage() {
return price > 0 && p.id !== effectivePlanId;
}).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
+ // PAYMENT PROCESSING OVERLAY - Beautiful full-page loading with breathing badge
+ if (paymentProcessing?.active) {
+ const stageConfig = {
+ verifying: { color: 'bg-blue-600', label: 'Verifying Payment' },
+ processing: { color: 'bg-amber-600', label: 'Processing Payment' },
+ finalizing: { color: 'bg-purple-600', label: 'Finalizing' },
+ activating: { color: 'bg-green-600', label: 'Activating Subscription' },
+ };
+ const config = stageConfig[paymentProcessing.stage];
+
+ // Use Modal-style overlay (matches app's default modal design)
+ return createPortal(
+
,
+ document.body
+ );
+ }
+
+ // Show loading spinner until initial data is loaded
+ // This prevents the flash of billing dashboard before PendingPaymentView
+ if (!initialDataLoaded) {
+ return (
+
+
+
+
Loading billing information...
+
+
+ );
+ }
+
// NEW USER PENDING PAYMENT - Show full-page payment view
// This is the simplified flow for users who just signed up with a paid plan
if (isNewUserPendingPayment && pendingInvoice) {
const planName = currentPlan?.name || pendingInvoice.subscription?.plan?.name || 'Selected Plan';
- const planPrice = pendingInvoice.total_amount || pendingInvoice.total || '0';
+ const invoiceCurrency = pendingInvoice.currency || 'USD';
+
+ // Get USD price from plan, PKR price from invoice
+ const planUSDPrice = currentPlan?.price || pendingInvoice.subscription?.plan?.price || '0';
+ const invoicePKRPrice = invoiceCurrency === 'PKR' ? (pendingInvoice.total_amount || pendingInvoice.total || '0') : undefined;
+
+ // Check if user has a pending bank transfer (status = pending_approval with payment_method = bank_transfer)
+ const hasPendingBankTransfer = payments.some(
+ (p) => p.status === 'pending_approval' && (p.payment_method === 'bank_transfer' || p.payment_method === 'manual')
+ );
+
+ // Debug log for payment view
+ console.log('[PlansAndBillingPage] Rendering PendingPaymentView:', {
+ billingCountry,
+ invoiceCurrency,
+ planUSDPrice,
+ invoicePKRPrice,
+ planName,
+ hasPendingBankTransfer
+ });
return (
{
// Refresh user and billing data
const { refreshUser } = useAuthStore.getState();
@@ -482,23 +834,23 @@ export default function PlansAndBillingPage() {
{/* SECTION 1: Current Plan Hero */}
{/* Main Plan Card */}
-
+
-
+
{currentPlan?.name || 'No Plan'}
{hasActivePlan ? 'Active' : 'Inactive'}
-
+
{currentPlan?.description || 'Select a plan to unlock features'}