payment gateways and plans billing and signup pages refactored
This commit is contained in:
@@ -15,7 +15,7 @@ import SuspenseLoader from "./components/common/SuspenseLoader";
|
||||
// Auth pages - loaded immediately (needed for login)
|
||||
import SignIn from "./pages/AuthPages/SignIn";
|
||||
import SignUp from "./pages/AuthPages/SignUp";
|
||||
import SignUpPK from "./pages/AuthPages/SignUpPK";
|
||||
// NOTE: SignUpPK removed - country selection now via dropdown in main signup form
|
||||
import Payment from "./pages/Payment";
|
||||
import NotFound from "./pages/OtherPage/NotFound";
|
||||
|
||||
@@ -139,7 +139,9 @@ export default function App() {
|
||||
{/* Auth Routes - Public */}
|
||||
<Route path="/signin" element={<SignIn />} />
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
<Route path="/signup/pk" element={<SignUpPK />} />
|
||||
{/* NOTE: /signup/pk removed - country selection now via dropdown in signup form */}
|
||||
{/* Redirect old PK route to main signup */}
|
||||
<Route path="/signup/pk" element={<SignUp />} />
|
||||
<Route path="/payment" element={<Payment />} />
|
||||
|
||||
{/* Legal Pages - Public */}
|
||||
|
||||
@@ -2,29 +2,24 @@
|
||||
* Unified Signup Form with Integrated Pricing Selection
|
||||
* Combines free and paid signup flows in one modern interface
|
||||
*
|
||||
* Payment Methods:
|
||||
* - Most countries: Credit/Debit Card (Stripe) + PayPal
|
||||
* - Pakistan (PK): Credit/Debit Card (Stripe) + Bank Transfer
|
||||
* Payment Flow (Simplified):
|
||||
* 1. User selects plan and fills in details
|
||||
* 2. User selects country from dropdown
|
||||
* 3. On submit: account created, redirected to /account/plans for payment
|
||||
*
|
||||
* NO payment method selection at signup - this happens on /account/plans
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon, CreditCardIcon, Building2Icon, WalletIcon, CheckIcon, Loader2Icon, CheckCircleIcon } from '../../icons';
|
||||
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon, CheckIcon, Loader2Icon, CheckCircleIcon, GlobeIcon } from '../../icons';
|
||||
import Label from '../form/Label';
|
||||
import Input from '../form/input/InputField';
|
||||
import Checkbox from '../form/input/Checkbox';
|
||||
import Button from '../ui/button/Button';
|
||||
import SelectDropdown from '../form/SelectDropdown';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
// 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 Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -40,22 +35,9 @@ interface Plan {
|
||||
features: string[];
|
||||
}
|
||||
|
||||
interface PaymentMethodConfig {
|
||||
id: number;
|
||||
payment_method: string;
|
||||
display_name: string;
|
||||
instructions: string | null;
|
||||
country_code: string;
|
||||
is_enabled: boolean;
|
||||
}
|
||||
|
||||
// Payment method option type
|
||||
interface PaymentOption {
|
||||
id: string;
|
||||
type: string;
|
||||
interface Country {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SignUpFormUnifiedProps {
|
||||
@@ -63,7 +45,6 @@ interface SignUpFormUnifiedProps {
|
||||
selectedPlan: Plan | null;
|
||||
onPlanSelect: (plan: Plan) => void;
|
||||
plansLoading: boolean;
|
||||
countryCode?: string; // Optional: 'PK' for Pakistan-specific, empty for global
|
||||
}
|
||||
|
||||
export default function SignUpFormUnified({
|
||||
@@ -71,7 +52,6 @@ export default function SignUpFormUnified({
|
||||
selectedPlan,
|
||||
onPlanSelect,
|
||||
plansLoading,
|
||||
countryCode = '', // Default to global (empty = show Credit Card + PayPal)
|
||||
}: SignUpFormUnifiedProps) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
@@ -83,13 +63,12 @@ export default function SignUpFormUnified({
|
||||
email: '',
|
||||
password: '',
|
||||
accountName: '',
|
||||
billingCountry: countryCode || 'US',
|
||||
billingCountry: 'US',
|
||||
});
|
||||
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('stripe');
|
||||
const [availablePaymentOptions, setAvailablePaymentOptions] = useState<PaymentOption[]>([]);
|
||||
const [backendPaymentMethods, setBackendPaymentMethods] = useState<PaymentMethodConfig[]>([]);
|
||||
const [paymentMethodsLoading, setPaymentMethodsLoading] = useState(false);
|
||||
// Countries for dropdown
|
||||
const [countries, setCountries] = useState<Country[]>([]);
|
||||
const [countriesLoading, setCountriesLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -97,9 +76,6 @@ export default function SignUpFormUnified({
|
||||
|
||||
const isPaidPlan = selectedPlan && parseFloat(String(selectedPlan.price || 0)) > 0;
|
||||
|
||||
// Determine if this is a Pakistan-specific signup
|
||||
const isPakistanSignup = countryCode === 'PK';
|
||||
|
||||
// Update URL when plan changes
|
||||
useEffect(() => {
|
||||
if (selectedPlan) {
|
||||
@@ -109,108 +85,61 @@ export default function SignUpFormUnified({
|
||||
}
|
||||
}, [selectedPlan]);
|
||||
|
||||
// Load payment methods from backend and determine available options
|
||||
// Load countries from backend and detect user's country
|
||||
useEffect(() => {
|
||||
if (!isPaidPlan) {
|
||||
setAvailablePaymentOptions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadPaymentMethods = async () => {
|
||||
setPaymentMethodsLoading(true);
|
||||
const loadCountriesAndDetect = async () => {
|
||||
setCountriesLoading(true);
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||
const response = await fetch(`${API_BASE_URL}/v1/billing/payment-configs/payment-methods/`);
|
||||
const response = await fetch(`${API_BASE_URL}/v1/auth/countries/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load payment methods');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
let methodsList: PaymentMethodConfig[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
methodsList = data;
|
||||
} else if (data.success && data.data) {
|
||||
methodsList = Array.isArray(data.data) ? data.data : data.data.results || [];
|
||||
} else if (data.results) {
|
||||
methodsList = data.results;
|
||||
}
|
||||
|
||||
const enabledMethods = methodsList.filter((m: PaymentMethodConfig) => m.is_enabled);
|
||||
setBackendPaymentMethods(enabledMethods);
|
||||
|
||||
// Build payment options based on signup type (PK vs Global)
|
||||
const options: PaymentOption[] = [];
|
||||
|
||||
// Always show Credit/Debit Card (Stripe) if enabled
|
||||
const stripeEnabled = enabledMethods.some(m => m.payment_method === 'stripe');
|
||||
if (stripeEnabled) {
|
||||
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 signup (/signup/pk): show Bank Transfer
|
||||
// For Global signup (/signup): show PayPal
|
||||
if (isPakistanSignup) {
|
||||
// Pakistan: show Bank Transfer as 2nd option
|
||||
const bankTransferEnabled = enabledMethods.some(
|
||||
m => m.payment_method === 'bank_transfer' && (!m.country_code || m.country_code === 'PK')
|
||||
);
|
||||
if (bankTransferEnabled) {
|
||||
options.push({
|
||||
id: 'bank_transfer',
|
||||
type: 'bank_transfer',
|
||||
name: 'Bank Transfer',
|
||||
description: 'Pay via bank transfer (PKR)',
|
||||
icon: <Building2Icon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCountries(data.countries || []);
|
||||
} else {
|
||||
// Global: show PayPal as 2nd option
|
||||
const paypalEnabled = enabledMethods.some(m => m.payment_method === 'paypal');
|
||||
if (paypalEnabled) {
|
||||
options.push({
|
||||
id: 'paypal',
|
||||
type: 'paypal',
|
||||
name: 'PayPal',
|
||||
description: 'Pay with your PayPal account',
|
||||
icon: <PayPalIcon className="w-6 h-6" />,
|
||||
});
|
||||
}
|
||||
// Fallback countries if backend fails
|
||||
setCountries([
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'PK', name: 'Pakistan' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
]);
|
||||
}
|
||||
|
||||
setAvailablePaymentOptions(options);
|
||||
|
||||
// Set default payment method
|
||||
if (options.length > 0 && !options.find(o => o.type === selectedPaymentMethod)) {
|
||||
setSelectedPaymentMethod(options[0].type);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load payment methods:', err);
|
||||
// Fallback to default options
|
||||
setAvailablePaymentOptions([
|
||||
{
|
||||
id: 'stripe',
|
||||
type: 'stripe',
|
||||
name: 'Credit/Debit Card',
|
||||
description: 'Pay securely with Visa, Mastercard, or other cards',
|
||||
icon: <CreditCardIcon className="w-6 h-6" />,
|
||||
// Try to detect user's country for default selection
|
||||
try {
|
||||
const geoResponse = await fetch('https://ipapi.co/country_code/', {
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
if (geoResponse.ok) {
|
||||
const countryCode = await geoResponse.text();
|
||||
if (countryCode && countryCode.length === 2) {
|
||||
setFormData(prev => ({ ...prev, billingCountry: countryCode.trim() }));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently fail - keep default US
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load countries:', err);
|
||||
// Fallback countries
|
||||
setCountries([
|
||||
{ code: 'US', name: 'United States' },
|
||||
{ code: 'GB', name: 'United Kingdom' },
|
||||
{ code: 'CA', name: 'Canada' },
|
||||
{ code: 'AU', name: 'Australia' },
|
||||
{ code: 'PK', name: 'Pakistan' },
|
||||
{ code: 'IN', name: 'India' },
|
||||
]);
|
||||
setSelectedPaymentMethod('stripe');
|
||||
} finally {
|
||||
setPaymentMethodsLoading(false);
|
||||
setCountriesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPaymentMethods();
|
||||
}, [isPaidPlan, isPakistanSignup]);
|
||||
loadCountriesAndDetect();
|
||||
}, []);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -236,11 +165,6 @@ export default function SignUpFormUnified({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPaidPlan && !selectedPaymentMethod) {
|
||||
setError('Please select a payment method');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const username = formData.email.split('@')[0];
|
||||
|
||||
@@ -252,35 +176,20 @@ export default function SignUpFormUnified({
|
||||
last_name: formData.lastName,
|
||||
account_name: formData.accountName,
|
||||
plan_slug: selectedPlan.slug,
|
||||
billing_country: formData.billingCountry,
|
||||
};
|
||||
|
||||
if (isPaidPlan) {
|
||||
registerPayload.payment_method = selectedPaymentMethod;
|
||||
registerPayload.billing_email = formData.email;
|
||||
registerPayload.billing_country = formData.billingCountry;
|
||||
}
|
||||
|
||||
const user = (await register(registerPayload)) as any;
|
||||
|
||||
// Log full registration response for debugging
|
||||
console.log('Registration response:', {
|
||||
user: user,
|
||||
checkoutUrl: user?.checkout_url,
|
||||
selectedPaymentMethod: selectedPaymentMethod,
|
||||
accountStatus: user?.account?.status
|
||||
accountStatus: user?.account?.status,
|
||||
planSlug: selectedPlan.slug,
|
||||
});
|
||||
|
||||
// CRITICAL: Verify auth state is actually set in Zustand store
|
||||
// Verify auth state is actually set in Zustand store
|
||||
const currentAuthState = useAuthStore.getState();
|
||||
|
||||
console.log('Post-registration auth state check:', {
|
||||
isAuthenticated: currentAuthState.isAuthenticated,
|
||||
hasUser: !!currentAuthState.user,
|
||||
hasToken: !!currentAuthState.token,
|
||||
userData: user,
|
||||
checkoutUrl: user?.checkout_url
|
||||
});
|
||||
|
||||
// If for some reason state wasn't set, force set it again
|
||||
if (!currentAuthState.isAuthenticated || !currentAuthState.user || !currentAuthState.token) {
|
||||
console.error('Auth state not properly set after registration, forcing update...');
|
||||
@@ -306,41 +215,16 @@ export default function SignUpFormUnified({
|
||||
throw new Error('Failed to authenticate after registration. Please try logging in manually.');
|
||||
}
|
||||
|
||||
// Handle payment gateway redirects
|
||||
const checkoutUrl = user?.checkout_url;
|
||||
|
||||
console.log('Payment redirect decision:', {
|
||||
checkoutUrl,
|
||||
selectedPaymentMethod,
|
||||
isPaidPlan,
|
||||
fullUserResponse: user,
|
||||
});
|
||||
|
||||
// For Stripe or PayPal with checkout URL - redirect to payment gateway
|
||||
if (checkoutUrl && (selectedPaymentMethod === 'stripe' || selectedPaymentMethod === 'paypal')) {
|
||||
console.log(`Redirecting to ${selectedPaymentMethod} checkout:`, checkoutUrl);
|
||||
window.location.href = checkoutUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
// For bank_transfer ONLY - go to plans page to show payment instructions
|
||||
// This is the expected flow for bank transfer
|
||||
if (selectedPaymentMethod === 'bank_transfer') {
|
||||
console.log('Bank transfer selected, redirecting to plans page for payment confirmation');
|
||||
// Simplified navigation:
|
||||
// - Paid plans: Go to /account/plans to select payment method and complete payment
|
||||
// - Free plans: Go to sites page
|
||||
if (isPaidPlan) {
|
||||
console.log('Paid plan selected, redirecting to /account/plans for payment');
|
||||
navigate('/account/plans', { replace: true });
|
||||
return;
|
||||
} else {
|
||||
console.log('Free plan selected, redirecting to /sites');
|
||||
navigate('/sites', { replace: true });
|
||||
}
|
||||
|
||||
// If Stripe/PayPal but no checkout URL (error case) - still go to plans page
|
||||
// User can retry payment from there
|
||||
if (isPaidPlan && !checkoutUrl) {
|
||||
console.warn('Paid plan selected but no checkout URL received - going to plans page');
|
||||
navigate('/account/plans', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// For free plans - go to sites page
|
||||
navigate('/sites', { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Registration failed. Please try again.');
|
||||
}
|
||||
@@ -527,77 +411,35 @@ export default function SignUpFormUnified({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPaidPlan && (
|
||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700 space-y-4">
|
||||
{/* Payment Method Selection - Card Style */}
|
||||
<div>
|
||||
<Label className="mb-3">
|
||||
Select Payment Method<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
{paymentMethodsLoading ? (
|
||||
<div className="flex items-center justify-center p-6 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<Loader2Icon className="w-5 h-5 animate-spin text-brand-500 mr-2" />
|
||||
<span className="text-sm text-gray-500">Loading payment options...</span>
|
||||
</div>
|
||||
) : availablePaymentOptions.length === 0 ? (
|
||||
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg text-warning-800 dark:bg-warning-900/20 dark:border-warning-800 dark:text-warning-200">
|
||||
<p className="text-sm">No payment methods available for your region</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{availablePaymentOptions.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedPaymentMethod(option.type)}
|
||||
className={`relative p-4 rounded-xl border-2 text-left transition-all ${
|
||||
selectedPaymentMethod === 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'
|
||||
}`}
|
||||
>
|
||||
{selectedPaymentMethod === option.type && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="w-5 h-5 bg-brand-500 rounded-full flex items-center justify-center">
|
||||
<CheckIcon className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-lg ${
|
||||
selectedPaymentMethod === 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 className="flex-1 min-w-0">
|
||||
<h4 className={`font-semibold text-sm ${
|
||||
selectedPaymentMethod === option.type
|
||||
? 'text-brand-700 dark:text-brand-400'
|
||||
: 'text-gray-900 dark:text-white'
|
||||
}`}>
|
||||
{option.name}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Country Selection */}
|
||||
<div>
|
||||
<Label className="flex items-center gap-1.5">
|
||||
<GlobeIcon className="w-4 h-4 text-gray-500" />
|
||||
Country<span className="text-error-500">*</span>
|
||||
</Label>
|
||||
{countriesLoading ? (
|
||||
<div className="flex items-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Loader2Icon className="w-4 h-4 animate-spin text-brand-500 mr-2" />
|
||||
<span className="text-sm text-gray-500">Loading countries...</span>
|
||||
</div>
|
||||
|
||||
{/* Pakistan signup notice */}
|
||||
{isPakistanSignup && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<span>🇵🇰</span> Pakistan - Bank transfer available
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<select
|
||||
name="billingCountry"
|
||||
value={formData.billingCountry}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 text-sm text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-800 dark:text-white dark:border-gray-700 focus:ring-2 focus:ring-brand-500 focus:border-transparent appearance-none cursor-pointer"
|
||||
>
|
||||
{countries.map((country) => (
|
||||
<option key={country.code} value={country.code}>
|
||||
{country.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Your country determines available payment methods
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 pt-2">
|
||||
<Checkbox className="w-5 h-5 mt-0.5" checked={isChecked} onChange={setIsChecked} />
|
||||
@@ -620,7 +462,7 @@ export default function SignUpFormUnified({
|
||||
Creating your account...
|
||||
</span>
|
||||
) : isPaidPlan ? (
|
||||
'Create Account & Continue to Payment'
|
||||
'Create Account'
|
||||
) : (
|
||||
'Start Free Trial'
|
||||
)}
|
||||
|
||||
355
frontend/src/components/billing/BankTransferForm.tsx
Normal file
355
frontend/src/components/billing/BankTransferForm.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* BankTransferForm - Component for submitting bank transfer proof
|
||||
*
|
||||
* Used when user selects bank transfer payment method (primarily for Pakistan)
|
||||
* Shows bank details to transfer to and form to submit payment proof
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Building2Icon,
|
||||
CopyIcon,
|
||||
CheckCircleIcon,
|
||||
Loader2Icon,
|
||||
AlertCircleIcon,
|
||||
FileTextIcon,
|
||||
} from '../../icons';
|
||||
import { Card } from '../ui/card';
|
||||
import Button from '../ui/button/Button';
|
||||
import Label from '../form/Label';
|
||||
import Input from '../form/input/InputField';
|
||||
import TextArea from '../form/input/TextArea';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
import {
|
||||
Invoice,
|
||||
submitManualPayment,
|
||||
getAvailablePaymentMethods,
|
||||
} from '../../services/billing.api';
|
||||
|
||||
interface BankDetails {
|
||||
bank_name: string;
|
||||
account_title: string;
|
||||
account_number: string;
|
||||
iban?: string;
|
||||
swift_code?: string;
|
||||
}
|
||||
|
||||
interface BankTransferFormProps {
|
||||
invoice: Invoice;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function BankTransferForm({
|
||||
invoice,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}: BankTransferFormProps) {
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [bankDetailsLoading, setBankDetailsLoading] = useState(true);
|
||||
const [bankDetails, setBankDetails] = useState<BankDetails | null>(null);
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
reference: '',
|
||||
notes: '',
|
||||
});
|
||||
|
||||
// Load bank details from backend
|
||||
useEffect(() => {
|
||||
const loadBankDetails = async () => {
|
||||
setBankDetailsLoading(true);
|
||||
try {
|
||||
const { results } = await getAvailablePaymentMethods();
|
||||
// Find bank_transfer method config
|
||||
const bankMethod = results.find(
|
||||
(m) => m.type === 'bank_transfer' && m.is_enabled
|
||||
);
|
||||
|
||||
// Cast to any to access extended bank_details properties
|
||||
// Backend may return additional fields not in the TypeScript type
|
||||
const details = bankMethod?.bank_details as any;
|
||||
|
||||
if (details) {
|
||||
setBankDetails({
|
||||
bank_name: details.bank_name || 'Bank ABC',
|
||||
account_title: details.account_title || details.account_name || 'IGNY8',
|
||||
account_number: details.account_number || '',
|
||||
iban: details.iban,
|
||||
swift_code: details.swift_code,
|
||||
});
|
||||
} else {
|
||||
// Fallback hardcoded details - should be replaced with backend config
|
||||
setBankDetails({
|
||||
bank_name: 'MCB Bank Limited',
|
||||
account_title: 'IGNY8 Technologies',
|
||||
account_number: '0000123456789',
|
||||
iban: 'PK00MUCB0000000123456789',
|
||||
swift_code: 'MUCBPKKAXXX',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load bank details:', error);
|
||||
// Use fallback
|
||||
setBankDetails({
|
||||
bank_name: 'MCB Bank Limited',
|
||||
account_title: 'IGNY8 Technologies',
|
||||
account_number: '0000123456789',
|
||||
iban: 'PK00MUCB0000000123456789',
|
||||
swift_code: 'MUCBPKKAXXX',
|
||||
});
|
||||
} finally {
|
||||
setBankDetailsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadBankDetails();
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = async (value: string, field: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopiedField(field);
|
||||
toast?.success?.(`${field} copied to clipboard`);
|
||||
setTimeout(() => setCopiedField(null), 2000);
|
||||
} catch {
|
||||
toast?.error?.('Failed to copy');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.reference.trim()) {
|
||||
toast?.error?.('Please enter your transaction reference');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await submitManualPayment({
|
||||
invoice_id: invoice.id,
|
||||
payment_method: 'bank_transfer',
|
||||
amount: invoice.total_amount || invoice.total || '0',
|
||||
currency: invoice.currency,
|
||||
reference: formData.reference.trim(),
|
||||
notes: formData.notes.trim(),
|
||||
});
|
||||
|
||||
toast?.success?.('Payment submitted successfully! We will verify it within 24 hours.');
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
console.error('Payment submission failed:', error);
|
||||
toast?.error?.(error.message || 'Failed to submit payment');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (bankDetailsLoading) {
|
||||
return (
|
||||
<Card className="p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2Icon className="w-6 h-6 animate-spin text-brand-500 mr-2" />
|
||||
<span className="text-gray-500">Loading bank details...</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!bankDetails) {
|
||||
return (
|
||||
<Card className="p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 p-4 bg-error-50 border border-error-200 rounded-lg dark:bg-error-900/20 dark:border-error-800">
|
||||
<AlertCircleIcon className="w-5 h-5 text-error-600" />
|
||||
<span className="text-error-800 dark:text-error-200">
|
||||
Bank details not available. Please contact support.
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-brand-100 dark:bg-brand-900/30">
|
||||
<Building2Icon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Bank Transfer
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Transfer to the account below and submit your reference
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bank Details */}
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl mb-6 space-y-3">
|
||||
<h3 className="font-medium text-gray-900 dark:text-white mb-3">Transfer Details</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="flex justify-between items-center p-2 bg-white dark:bg-gray-800 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Bank</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{bankDetails.bank_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center p-2 bg-white dark:bg-gray-800 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Account Title</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{bankDetails.account_title}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(bankDetails.account_title, 'Account title')}
|
||||
className="p-1.5 text-gray-400 hover:text-brand-500 transition-colors"
|
||||
>
|
||||
{copiedField === 'Account title' ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center p-2 bg-white dark:bg-gray-800 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Account Number</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white font-mono">{bankDetails.account_number}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(bankDetails.account_number, 'Account number')}
|
||||
className="p-1.5 text-gray-400 hover:text-brand-500 transition-colors"
|
||||
>
|
||||
{copiedField === 'Account number' ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{bankDetails.iban && (
|
||||
<div className="flex justify-between items-center p-2 bg-white dark:bg-gray-800 rounded-lg">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">IBAN</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white font-mono text-sm">{bankDetails.iban}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(bankDetails.iban!, 'IBAN')}
|
||||
className="p-1.5 text-gray-400 hover:text-brand-500 transition-colors"
|
||||
>
|
||||
{copiedField === 'IBAN' ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reference to include */}
|
||||
<div className="mt-4 p-3 bg-brand-50 dark:bg-brand-900/20 rounded-lg border border-brand-200 dark:border-brand-800">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-xs text-brand-600 dark:text-brand-400 font-medium">Payment Reference (Include in transfer)</p>
|
||||
<p className="font-mono font-semibold text-brand-700 dark:text-brand-300">{invoice.invoice_number}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(invoice.invoice_number, 'Reference')}
|
||||
className="p-1.5 text-brand-400 hover:text-brand-600 transition-colors"
|
||||
>
|
||||
{copiedField === 'Reference' ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount */}
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Label>
|
||||
Transaction Reference <span className="text-error-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Enter your bank transaction reference/ID"
|
||||
value={formData.reference}
|
||||
onChange={(e) => setFormData({ ...formData, reference: e.target.value })}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Enter the transaction ID or reference from your bank receipt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Notes (Optional)</Label>
|
||||
<TextArea
|
||||
placeholder="Any additional information about your transfer"
|
||||
value={formData.notes}
|
||||
onChange={(value) => setFormData({ ...formData, notes: value })}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
disabled={loading || !formData.reference.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<Loader2Icon className="w-4 h-4 animate-spin mr-2" />
|
||||
Submitting...
|
||||
</span>
|
||||
) : (
|
||||
'Submit Payment Proof'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Info notice */}
|
||||
<div className="mt-4 flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<FileTextIcon className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
After submitting, our team will verify your payment within 24 hours.
|
||||
You'll receive an email confirmation once approved.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
339
frontend/src/components/billing/PendingPaymentView.tsx
Normal file
339
frontend/src/components/billing/PendingPaymentView.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||
|
||||
@@ -19,7 +19,6 @@ interface Plan {
|
||||
}
|
||||
|
||||
export default function SignUp() {
|
||||
const navigate = useNavigate();
|
||||
const planSlug = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("plan") || "";
|
||||
@@ -28,38 +27,10 @@ export default function SignUp() {
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [plansLoading, setPlansLoading] = useState(true);
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
||||
const [geoChecked, setGeoChecked] = useState(false);
|
||||
|
||||
// Check geo location and redirect PK users to /signup/pk
|
||||
// Using free public API: https://api.country.is (no signup required, CORS enabled)
|
||||
useEffect(() => {
|
||||
const checkGeoAndRedirect = async () => {
|
||||
try {
|
||||
// Free public geo API - no signup required
|
||||
const response = await fetch('https://api.country.is/', {
|
||||
signal: AbortSignal.timeout(3000), // 3 second timeout
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const countryCode = data?.country;
|
||||
|
||||
if (countryCode === 'PK') {
|
||||
// Preserve query params when redirecting
|
||||
const queryString = window.location.search;
|
||||
navigate(`/signup/pk${queryString}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently fail - continue with global signup
|
||||
console.log('Geo detection failed, using global signup');
|
||||
}
|
||||
setGeoChecked(true);
|
||||
};
|
||||
|
||||
checkGeoAndRedirect();
|
||||
}, [navigate]);
|
||||
// NOTE: Geo detection removed per payment system refactor
|
||||
// Country is now selected via dropdown in the signup form
|
||||
// Payment method selection happens on /account/plans after registration
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
@@ -101,15 +72,6 @@ export default function SignUp() {
|
||||
fetchPlans();
|
||||
}, [planSlug]);
|
||||
|
||||
// Don't render until geo check is complete (prevents flash)
|
||||
if (!geoChecked) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* SignUpPK - Pakistan-specific signup page
|
||||
* Shows Credit/Debit Card + Bank Transfer payment options
|
||||
*
|
||||
* Route: /signup/pk
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
price: string | number;
|
||||
billing_cycle: string;
|
||||
is_active: boolean;
|
||||
max_users: number;
|
||||
max_sites: number;
|
||||
max_keywords: number;
|
||||
max_ahrefs_queries: number;
|
||||
included_credits: number;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export default function SignUpPK() {
|
||||
const planSlug = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("plan") || "";
|
||||
}, []);
|
||||
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [plansLoading, setPlansLoading] = useState(true);
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
setPlansLoading(true);
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || "https://api.igny8.com/api";
|
||||
const res = await fetch(`${API_BASE_URL}/v1/auth/plans/`);
|
||||
const data = await res.json();
|
||||
const allPlans = data?.results || [];
|
||||
|
||||
// Show all active plans (including free plan)
|
||||
const publicPlans = allPlans
|
||||
.filter((p: Plan) => p.is_active)
|
||||
.sort((a: Plan, b: Plan) => {
|
||||
const priceA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price || 0));
|
||||
const priceB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price || 0));
|
||||
return priceA - priceB;
|
||||
});
|
||||
|
||||
setPlans(publicPlans);
|
||||
|
||||
// Auto-select plan from URL or default to first plan
|
||||
if (planSlug) {
|
||||
const plan = publicPlans.find((p: Plan) => p.slug === planSlug);
|
||||
if (plan) {
|
||||
setSelectedPlan(plan);
|
||||
} else {
|
||||
setSelectedPlan(publicPlans[0] || null);
|
||||
}
|
||||
} else {
|
||||
setSelectedPlan(publicPlans[0] || null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load plans:', e);
|
||||
} finally {
|
||||
setPlansLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, [planSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="Sign Up (Pakistan) - IGNY8"
|
||||
description="Create your IGNY8 account and start building topical authority with AI-powered content"
|
||||
/>
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left Side - Signup Form with Pakistan-specific payment options */}
|
||||
<SignUpFormUnified
|
||||
plans={plans}
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={setSelectedPlan}
|
||||
plansLoading={plansLoading}
|
||||
countryCode="PK"
|
||||
/>
|
||||
|
||||
{/* Right Side - Pricing Plans */}
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-brand-50 to-purple-50 dark:from-gray-900 dark:to-gray-800 p-8 xl:p-12 items-start justify-center relative">
|
||||
{/* Logo - Top Right */}
|
||||
<Link to="/" className="absolute top-6 right-6">
|
||||
<img
|
||||
src="/images/logo/IGNY8_LIGHT_LOGO.png"
|
||||
alt="IGNY8"
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="w-full max-w-2xl mt-20">
|
||||
{/* Pricing Plans Component Will Load Here */}
|
||||
<div id="signup-pricing-plans" className="w-full">
|
||||
{/* Plans will be rendered by SignUpFormUnified */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
} from '../../services/billing.api';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import PayInvoiceModal from '../../components/billing/PayInvoiceModal';
|
||||
import PendingPaymentView from '../../components/billing/PendingPaymentView';
|
||||
|
||||
export default function PlansAndBillingPage() {
|
||||
const { startLoading, stopLoading } = usePageLoading();
|
||||
@@ -385,6 +386,15 @@ export default function PlansAndBillingPage() {
|
||||
const hasPendingPayment = payments.some((p) => p.status === 'pending_approval');
|
||||
const hasPendingInvoice = invoices.some((inv) => inv.status === 'pending');
|
||||
|
||||
// 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';
|
||||
|
||||
// Combined check: disable Buy Credits if no active plan OR has pending invoice
|
||||
const canBuyCredits = hasActivePlan && !hasPendingInvoice;
|
||||
|
||||
@@ -399,6 +409,29 @@ export default function PlansAndBillingPage() {
|
||||
return price > 0 && p.id !== effectivePlanId;
|
||||
}).sort((a, b) => (Number(a.price) || 0) - (Number(b.price) || 0));
|
||||
|
||||
// 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';
|
||||
|
||||
return (
|
||||
<PendingPaymentView
|
||||
invoice={pendingInvoice}
|
||||
userCountry={billingCountry}
|
||||
planName={planName}
|
||||
planPrice={planPrice}
|
||||
onPaymentSuccess={() => {
|
||||
// Refresh user and billing data
|
||||
const { refreshUser } = useAuthStore.getState();
|
||||
refreshUser().catch(() => {});
|
||||
loadData();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// EXISTING USER - Show normal billing dashboard
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Plans & Billing" description="Manage your subscription and payments" />
|
||||
|
||||
@@ -357,19 +357,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error('Failed to save login session. Please try again.');
|
||||
}
|
||||
|
||||
// Return full response data for success handling (includes checkout_url for payment redirects)
|
||||
console.log('Extracting checkout_url:', {
|
||||
'responseData.checkout_url': responseData.checkout_url,
|
||||
'data.checkout_url': data.checkout_url,
|
||||
'data.data?.checkout_url': data.data?.checkout_url,
|
||||
responseData: responseData,
|
||||
});
|
||||
|
||||
// Return full response data for success handling
|
||||
// Note: checkout_url is no longer returned by backend (payment flow moved to /account/plans)
|
||||
return {
|
||||
...userData,
|
||||
checkout_url: responseData.checkout_url || data.checkout_url || data.data?.checkout_url,
|
||||
checkout_session_id: responseData.checkout_session_id || data.checkout_session_id || data.data?.checkout_session_id,
|
||||
paypal_order_id: responseData.paypal_order_id || data.paypal_order_id || data.data?.paypal_order_id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
// ALWAYS reset loading on error - critical to prevent stuck state
|
||||
|
||||
Reference in New Issue
Block a user