STripe Paymen and PK payemtns and many othe rbacekd and froentened issues
This commit is contained in:
@@ -15,6 +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";
|
||||
import Payment from "./pages/Payment";
|
||||
import NotFound from "./pages/OtherPage/NotFound";
|
||||
|
||||
@@ -138,6 +139,7 @@ export default function App() {
|
||||
{/* Auth Routes - Public */}
|
||||
<Route path="/signin" element={<SignIn />} />
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
<Route path="/signup/pk" element={<SignUpPK />} />
|
||||
<Route path="/payment" element={<Payment />} />
|
||||
|
||||
{/* Legal Pages - Public */}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -38,6 +38,14 @@ export default function PendingPaymentBanner({ className = '' }: PendingPaymentB
|
||||
const accountStatus = user?.account?.status;
|
||||
const isPendingPayment = accountStatus === 'pending_payment';
|
||||
|
||||
// Clear dismissed state when account is no longer pending payment
|
||||
// This ensures the banner shows again if account reverts to pending
|
||||
useEffect(() => {
|
||||
if (!isPendingPayment) {
|
||||
sessionStorage.removeItem('payment-banner-dismissed');
|
||||
}
|
||||
}, [isPendingPayment]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPendingPayment && !dismissed) {
|
||||
loadPendingInvoice();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import SignUpFormUnified from "../../components/auth/SignUpFormUnified";
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Plan {
|
||||
}
|
||||
|
||||
export default function SignUp() {
|
||||
const navigate = useNavigate();
|
||||
const planSlug = useMemo(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("plan") || "";
|
||||
@@ -27,6 +28,38 @@ 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]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
@@ -68,6 +101,15 @@ 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
|
||||
|
||||
116
frontend/src/pages/AuthPages/SignUpPK.tsx
Normal file
116
frontend/src/pages/AuthPages/SignUpPK.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import { PricingPlan } from '../../components/ui/pricing-table';
|
||||
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
|
||||
import { usePageLoading } from '../../context/PageLoadingContext';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
@@ -105,6 +107,45 @@ export default function Plans() {
|
||||
const toast = useToast();
|
||||
const { startLoading, stopLoading } = usePageLoading();
|
||||
const [plans, setPlans] = useState<Plan[]>([]);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { refreshUser } = useAuthStore();
|
||||
|
||||
// Handle payment success redirect from Stripe
|
||||
useEffect(() => {
|
||||
const success = searchParams.get('success');
|
||||
const sessionId = searchParams.get('session_id');
|
||||
|
||||
if (success === 'true') {
|
||||
// Clear the query params to avoid re-triggering
|
||||
setSearchParams({});
|
||||
|
||||
// Refresh user data to get updated account status
|
||||
refreshUser().then(() => {
|
||||
toast.success('Payment successful! Your subscription is now active.');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to refresh user after payment:', err);
|
||||
// Still show success message since payment was processed
|
||||
toast.success('Payment successful! Please refresh the page to see updates.');
|
||||
});
|
||||
}
|
||||
}, [searchParams, setSearchParams, refreshUser, toast]);
|
||||
|
||||
// Handle PayPal success redirect
|
||||
useEffect(() => {
|
||||
const paypal = searchParams.get('paypal');
|
||||
|
||||
if (paypal === 'success') {
|
||||
setSearchParams({});
|
||||
refreshUser().then(() => {
|
||||
toast.success('PayPal payment successful! Your subscription is now active.');
|
||||
}).catch(() => {
|
||||
toast.success('PayPal payment successful! Please refresh the page to see updates.');
|
||||
});
|
||||
} else if (paypal === 'cancel') {
|
||||
setSearchParams({});
|
||||
toast.info('Payment was cancelled.');
|
||||
}
|
||||
}, [searchParams, setSearchParams, refreshUser, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPlans();
|
||||
|
||||
@@ -36,6 +36,7 @@ import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { usePageLoading } from '../../context/PageLoadingContext';
|
||||
import { formatCurrency } from '../../utils';
|
||||
import {
|
||||
getCreditBalance,
|
||||
getCreditPackages,
|
||||
@@ -711,7 +712,7 @@ export default function PlansAndBillingPage() {
|
||||
<td className="px-6 py-3 text-gray-600 dark:text-gray-400">
|
||||
{new Date(invoice.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-center font-semibold text-gray-900 dark:text-white">${invoice.total_amount}</td>
|
||||
<td className="px-6 py-3 text-center font-semibold text-gray-900 dark:text-white">{formatCurrency(invoice.total_amount, invoice.currency)}</td>
|
||||
<td className="px-6 py-3 text-center">
|
||||
<Badge variant="soft" tone={invoice.status === 'paid' ? 'success' : 'warning'}>
|
||||
{invoice.status}
|
||||
|
||||
@@ -357,8 +357,20 @@ export const useAuthStore = create<AuthState>()(
|
||||
throw new Error('Failed to save login session. Please try again.');
|
||||
}
|
||||
|
||||
// Return user data for success handling
|
||||
return userData;
|
||||
// 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 {
|
||||
...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
|
||||
set({ loading: false });
|
||||
|
||||
68
frontend/src/utils/currency.ts
Normal file
68
frontend/src/utils/currency.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Currency utilities for formatting amounts with proper symbols
|
||||
*/
|
||||
|
||||
// Currency symbols map
|
||||
const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£',
|
||||
INR: '₹',
|
||||
JPY: '¥',
|
||||
CNY: '¥',
|
||||
AUD: 'A$',
|
||||
CAD: 'C$',
|
||||
CHF: 'Fr',
|
||||
SEK: 'kr',
|
||||
NOK: 'kr',
|
||||
DKK: 'kr',
|
||||
PLN: 'zł',
|
||||
BRL: 'R$',
|
||||
ZAR: 'R',
|
||||
AED: 'د.إ',
|
||||
SAR: 'ر.س',
|
||||
PKR: '₨',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get currency symbol for a currency code
|
||||
*/
|
||||
export const getCurrencySymbol = (currencyCode?: string): string => {
|
||||
if (!currencyCode) return '$';
|
||||
const code = currencyCode.toUpperCase();
|
||||
return CURRENCY_SYMBOLS[code] || `${code} `;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format an amount with the proper currency symbol
|
||||
* @param amount - The amount to format (string or number)
|
||||
* @param currency - The currency code (e.g., 'USD', 'PKR')
|
||||
* @param showDecimals - Whether to show decimal places (default: true for most currencies)
|
||||
*/
|
||||
export const formatCurrency = (
|
||||
amount: string | number,
|
||||
currency?: string,
|
||||
showDecimals: boolean = true
|
||||
): string => {
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(numAmount)) return '-';
|
||||
|
||||
const symbol = getCurrencySymbol(currency);
|
||||
|
||||
// For zero-decimal currencies like JPY, don't show decimals
|
||||
const zeroDecimalCurrencies = ['JPY', 'KRW', 'VND'];
|
||||
const shouldShowDecimals = showDecimals && !zeroDecimalCurrencies.includes(currency?.toUpperCase() || '');
|
||||
|
||||
const formatted = shouldShowDecimals
|
||||
? numAmount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: numAmount.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
||||
|
||||
return `${symbol}${formatted}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format price for display (typically USD)
|
||||
*/
|
||||
export const formatPrice = (price: string | number): string => {
|
||||
return formatCurrency(price, 'USD');
|
||||
};
|
||||
@@ -50,3 +50,46 @@ export function formatRelativeDate(dateString: string | Date): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to a standard display format
|
||||
* @param dateString - ISO date string or Date object
|
||||
* @param options - Intl.DateTimeFormat options
|
||||
* @returns Formatted date string (e.g., "Jan 7, 2026")
|
||||
*/
|
||||
export function formatDate(
|
||||
dateString: string | Date | null | undefined,
|
||||
options: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }
|
||||
): string {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
||||
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date and time to a standard display format
|
||||
* @param dateString - ISO date string or Date object
|
||||
* @returns Formatted date and time string (e.g., "Jan 7, 2026, 3:30 PM")
|
||||
*/
|
||||
export function formatDateTime(
|
||||
dateString: string | Date | null | undefined
|
||||
): string {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
||||
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,5 +27,12 @@ export {
|
||||
formatDateTime,
|
||||
} from './date';
|
||||
|
||||
// Currency utilities
|
||||
export {
|
||||
getCurrencySymbol,
|
||||
formatCurrency,
|
||||
formatPrice,
|
||||
} from './currency';
|
||||
|
||||
// Add other global utilities here as needed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user