STripe Paymen and PK payemtns and many othe rbacekd and froentened issues

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 05:51:36 +00:00
parent 87d1662a18
commit 0386d4bf33
24 changed files with 1079 additions and 174 deletions

View File

@@ -1,6 +1,10 @@
/**
* 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
*/
import { useState, useEffect } from 'react';
@@ -14,6 +18,13 @@ 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;
@@ -38,11 +49,21 @@ interface PaymentMethodConfig {
is_enabled: boolean;
}
// Payment method option type
interface PaymentOption {
id: string;
type: string;
name: string;
description: string;
icon: React.ReactNode;
}
interface SignUpFormUnifiedProps {
plans: Plan[];
selectedPlan: Plan | null;
onPlanSelect: (plan: Plan) => void;
plansLoading: boolean;
countryCode?: string; // Optional: 'PK' for Pakistan-specific, empty for global
}
export default function SignUpFormUnified({
@@ -50,6 +71,7 @@ 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);
@@ -61,11 +83,12 @@ export default function SignUpFormUnified({
email: '',
password: '',
accountName: '',
billingCountry: 'US',
billingCountry: countryCode || 'US',
});
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('');
const [paymentMethods, setPaymentMethods] = useState<PaymentMethodConfig[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>('stripe');
const [availablePaymentOptions, setAvailablePaymentOptions] = useState<PaymentOption[]>([]);
const [backendPaymentMethods, setBackendPaymentMethods] = useState<PaymentMethodConfig[]>([]);
const [paymentMethodsLoading, setPaymentMethodsLoading] = useState(false);
const [error, setError] = useState('');
@@ -74,6 +97,9 @@ 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) {
@@ -83,10 +109,10 @@ export default function SignUpFormUnified({
}
}, [selectedPlan]);
// Load payment methods for paid plans
// Load payment methods from backend and determine available options
useEffect(() => {
if (!isPaidPlan) {
setPaymentMethods([]);
setAvailablePaymentOptions([]);
return;
}
@@ -94,8 +120,7 @@ export default function SignUpFormUnified({
setPaymentMethodsLoading(true);
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const country = formData.billingCountry || 'US';
const response = await fetch(`${API_BASE_URL}/v1/billing/payment-configs/payment-methods/?country=${country}`);
const response = await fetch(`${API_BASE_URL}/v1/billing/payment-configs/payment-methods/`);
if (!response.ok) {
throw new Error('Failed to load payment methods');
@@ -113,22 +138,79 @@ export default function SignUpFormUnified({
}
const enabledMethods = methodsList.filter((m: PaymentMethodConfig) => m.is_enabled);
setPaymentMethods(enabledMethods);
setBackendPaymentMethods(enabledMethods);
if (enabledMethods.length > 0 && !selectedPaymentMethod) {
setSelectedPaymentMethod(enabledMethods[0].payment_method);
// 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" />,
});
}
} 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" />,
});
}
}
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);
// Don't set error for free plans or if payment methods fail to load
// Just log it and continue
// 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" />,
}
]);
setSelectedPaymentMethod('stripe');
} finally {
setPaymentMethodsLoading(false);
}
};
loadPaymentMethods();
}, [isPaidPlan, formData.billingCountry]);
}, [isPaidPlan, isPakistanSignup]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
@@ -180,27 +262,33 @@ export default function SignUpFormUnified({
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
});
// CRITICAL: Verify auth state is actually set in Zustand store
// The register function should have already set isAuthenticated=true
const currentAuthState = useAuthStore.getState();
console.log('Post-registration auth state check:', {
isAuthenticated: currentAuthState.isAuthenticated,
hasUser: !!currentAuthState.user,
hasToken: !!currentAuthState.token,
userData: user
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...');
// Extract tokens from user data if available
const tokenData = user?.tokens || {};
const accessToken = user?.access || tokenData.access || localStorage.getItem('access_token');
const refreshToken = user?.refresh || tokenData.refresh || localStorage.getItem('refresh_token');
// Force set the state
useAuthStore.setState({
user: user,
token: accessToken,
@@ -209,7 +297,6 @@ export default function SignUpFormUnified({
loading: false
});
// Wait a bit for state to propagate
await new Promise((resolve) => setTimeout(resolve, 500));
}
@@ -219,30 +306,46 @@ export default function SignUpFormUnified({
throw new Error('Failed to authenticate after registration. Please try logging in manually.');
}
const status = user?.account?.status;
if (status === 'pending_payment') {
navigate('/account/plans', { replace: true });
} else {
navigate('/sites', { replace: true });
// 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');
navigate('/account/plans', { replace: true });
return;
}
// 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.');
}
};
const getPaymentIcon = (method: string) => {
switch (method) {
case 'stripe':
return <CreditCardIcon className="w-5 h-5" />;
case 'bank_transfer':
return <Building2Icon className="w-5 h-5" />;
case 'local_wallet':
return <WalletIcon className="w-5 h-5" />;
default:
return <CreditCardIcon className="w-5 h-5" />;
}
};
const formatNumber = (num: number): string => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
@@ -426,78 +529,72 @@ export default function SignUpFormUnified({
{isPaidPlan && (
<div className="pt-4 border-t border-gray-200 dark:border-gray-700 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>
Country<span className="text-error-500">*</span>
</Label>
<SelectDropdown
options={[
{ value: 'US', label: '🇺🇸 United States' },
{ value: 'GB', label: '🇬🇧 United Kingdom' },
{ value: 'IN', label: '🇮🇳 India' },
{ value: 'PK', label: '🇵🇰 Pakistan' },
{ value: 'CA', label: '🇨🇦 Canada' },
{ value: 'AU', label: '🇦🇺 Australia' },
{ value: 'DE', label: '🇩🇪 Germany' },
{ value: 'FR', label: '🇫🇷 France' },
]}
value={formData.billingCountry}
onChange={(value) => setFormData((prev) => ({ ...prev, billingCountry: value }))}
className="text-base"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Payment methods filtered by country</p>
</div>
<div>
<Label>
Payment Method<span className="text-error-500">*</span>
</Label>
{paymentMethodsLoading ? (
<div className="flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg h-[52px]">
<Loader2Icon className="w-4 h-4 animate-spin text-brand-500" />
</div>
) : paymentMethods.length === 0 ? (
<div className="p-3 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-xs">No payment methods available</p>
</div>
) : (
<SelectDropdown
options={paymentMethods.map(m => ({
value: m.payment_method,
label: m.display_name
}))}
value={selectedPaymentMethod}
onChange={(value) => setSelectedPaymentMethod(value)}
className="text-base"
/>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">How you'd like to pay</p>
</div>
</div>
{/* Payment Method Details - Full Width Below */}
{selectedPaymentMethod && paymentMethods.length > 0 && (
<div className="space-y-2">
{paymentMethods.filter(m => m.payment_method === selectedPaymentMethod).map((method) => (
method.instructions && (
<div
key={method.id}
className="p-4 rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50"
{/* 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'
}`}
>
<div className="flex items-start gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-brand-500 text-white flex-shrink-0">
{getPaymentIcon(method.payment_method)}
{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-1">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm mb-1">{method.display_name}</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-line">{method.instructions}</p>
)}
<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>
</div>
)
))}
</div>
</button>
))}
</div>
)}
</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>
)}