Phase 3 & Phase 4 - Completed

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 00:57:26 +00:00
parent 4b6a03a898
commit 909ed1cb17
25 changed files with 5549 additions and 215 deletions

View File

@@ -0,0 +1,192 @@
/**
* PaymentGatewaySelector Component
* Allows users to select between Stripe, PayPal, and Manual payment methods
*/
import React, { useEffect, useState } from 'react';
import { CreditCard, Building2, Wallet, Loader2, Check } from 'lucide-react';
import { getAvailablePaymentGateways, PaymentGateway } from '@/services/billing.api';
interface PaymentGatewayOption {
id: PaymentGateway;
name: string;
description: string;
icon: React.ReactNode;
available: boolean;
recommended?: boolean;
}
interface PaymentGatewaySelectorProps {
selectedGateway: PaymentGateway | null;
onSelectGateway: (gateway: PaymentGateway) => void;
showManual?: boolean;
className?: string;
}
export function PaymentGatewaySelector({
selectedGateway,
onSelectGateway,
showManual = true,
className = '',
}: PaymentGatewaySelectorProps) {
const [loading, setLoading] = useState(true);
const [gateways, setGateways] = useState<PaymentGatewayOption[]>([]);
useEffect(() => {
async function loadGateways() {
try {
const available = await getAvailablePaymentGateways();
const options: PaymentGatewayOption[] = [
{
id: 'stripe',
name: 'Credit/Debit Card',
description: 'Pay securely with your credit or debit card via Stripe',
icon: <CreditCard className="h-6 w-6" />,
available: available.stripe,
recommended: available.stripe,
},
{
id: 'paypal',
name: 'PayPal',
description: 'Pay with your PayPal account or PayPal Credit',
icon: (
<svg className="h-6 w-6" 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.797H9.3L7.076 21.337z" />
</svg>
),
available: available.paypal,
},
];
if (showManual) {
options.push({
id: 'manual',
name: 'Bank Transfer',
description: 'Pay via bank transfer with manual confirmation',
icon: <Building2 className="h-6 w-6" />,
available: available.manual,
});
}
setGateways(options);
// Auto-select first available gateway if none selected
if (!selectedGateway) {
const firstAvailable = options.find((g) => g.available);
if (firstAvailable) {
onSelectGateway(firstAvailable.id);
}
}
} catch (error) {
console.error('Failed to load payment gateways:', error);
// Fallback to manual only
setGateways([
{
id: 'manual',
name: 'Bank Transfer',
description: 'Pay via bank transfer with manual confirmation',
icon: <Building2 className="h-6 w-6" />,
available: true,
},
]);
if (!selectedGateway) {
onSelectGateway('manual');
}
} finally {
setLoading(false);
}
}
loadGateways();
}, [selectedGateway, onSelectGateway, showManual]);
if (loading) {
return (
<div className={`flex items-center justify-center py-8 ${className}`}>
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
<span className="ml-2 text-gray-500">Loading payment options...</span>
</div>
);
}
const availableGateways = gateways.filter((g) => g.available);
if (availableGateways.length === 0) {
return (
<div className={`rounded-lg border border-yellow-200 bg-yellow-50 p-4 ${className}`}>
<p className="text-sm text-yellow-800">
No payment methods are currently available. Please contact support.
</p>
</div>
);
}
return (
<div className={`space-y-3 ${className}`}>
<label className="block text-sm font-medium text-gray-700">
Select Payment Method
</label>
<div className="grid gap-3">
{availableGateways.map((gateway) => (
<button
key={gateway.id}
type="button"
onClick={() => onSelectGateway(gateway.id)}
className={`relative flex items-start rounded-lg border-2 p-4 text-left transition-all ${
selectedGateway === gateway.id
? 'border-indigo-600 bg-indigo-50 ring-1 ring-indigo-600'
: 'border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50'
}`}
>
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full ${
selectedGateway === gateway.id
? 'bg-indigo-600 text-white'
: 'bg-gray-100 text-gray-600'
}`}
>
{gateway.icon}
</div>
<div className="ml-4 flex-1">
<div className="flex items-center">
<span
className={`font-medium ${
selectedGateway === gateway.id ? 'text-indigo-900' : 'text-gray-900'
}`}
>
{gateway.name}
</span>
{gateway.recommended && (
<span className="ml-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
Recommended
</span>
)}
</div>
<p
className={`mt-1 text-sm ${
selectedGateway === gateway.id ? 'text-indigo-700' : 'text-gray-500'
}`}
>
{gateway.description}
</p>
</div>
{selectedGateway === gateway.id && (
<div className="absolute right-4 top-4">
<Check className="h-5 w-5 text-indigo-600" />
</div>
)}
</button>
))}
</div>
{selectedGateway === 'manual' && (
<p className="mt-2 text-xs text-gray-500">
After submitting, you'll receive bank details to complete the transfer.
Your account will be activated once we confirm the payment (usually within 1-2 business days).
</p>
)}
</div>
);
}
export default PaymentGatewaySelector;

View File

@@ -8,7 +8,6 @@ import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import {
CreditCardIcon,
BoxIcon as PackageIcon,
TrendingUpIcon,
FileTextIcon,
WalletIcon,
@@ -56,6 +55,12 @@ import {
cancelSubscription,
type Plan,
type Subscription,
// Payment gateway methods
subscribeToPlan,
purchaseCredits,
openStripeBillingPortal,
getAvailablePaymentGateways,
type PaymentGateway,
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
@@ -73,6 +78,7 @@ export default function PlansAndBillingPage() {
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [selectedBillingCycle, setSelectedBillingCycle] = useState<'monthly' | 'annual'>('monthly');
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
// Data States
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
@@ -83,11 +89,37 @@ export default function PlansAndBillingPage() {
const [plans, setPlans] = useState<Plan[]>([]);
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string | undefined>(undefined);
const [availableGateways, setAvailableGateways] = useState<{ stripe: boolean; paypal: boolean; manual: boolean }>({
stripe: false,
paypal: false,
manual: true,
});
useEffect(() => {
if (hasLoaded.current) return;
hasLoaded.current = true;
loadData();
// Handle payment gateway return URLs
const params = new URLSearchParams(window.location.search);
const success = params.get('success');
const canceled = params.get('canceled');
const purchase = params.get('purchase');
if (success === 'true') {
toast?.success?.('Subscription activated successfully!');
// Clean up URL
window.history.replaceState({}, '', window.location.pathname);
} else if (canceled === 'true') {
toast?.info?.('Payment was cancelled');
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'success') {
toast?.success?.('Credits purchased successfully!');
window.history.replaceState({}, '', window.location.pathname);
} else if (purchase === 'canceled') {
toast?.info?.('Credit purchase was cancelled');
window.history.replaceState({}, '', window.location.pathname);
}
}, []);
const handleError = (err: any, fallback: string) => {
@@ -157,6 +189,23 @@ export default function PlansAndBillingPage() {
subs.push({ id: accountPlan.id || 0, plan: accountPlan, status: 'active' } as any);
}
setSubscriptions(subs);
// Load available payment gateways
try {
const gateways = await getAvailablePaymentGateways();
setAvailableGateways(gateways);
// Auto-select first available gateway
if (gateways.stripe) {
setSelectedGateway('stripe');
} else if (gateways.paypal) {
setSelectedGateway('paypal');
} else {
setSelectedGateway('manual');
}
} catch {
// Non-critical - just keep defaults
console.log('Could not load payment gateways, using defaults');
}
} catch (err: any) {
if (err?.status === 429 && allowRetry) {
setError('Request was throttled. Retrying...');
@@ -172,6 +221,15 @@ export default function PlansAndBillingPage() {
const handleSelectPlan = async (planId: number) => {
try {
setPlanLoadingId(planId);
// Use payment gateway integration for Stripe/PayPal
if (selectedGateway === 'stripe' || selectedGateway === 'paypal') {
const { redirect_url } = await subscribeToPlan(planId.toString(), selectedGateway);
window.location.href = redirect_url;
return;
}
// Fall back to manual/bank transfer flow
await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
toast?.success?.('Plan upgraded successfully!');
setShowUpgradeModal(false);
@@ -201,7 +259,16 @@ export default function PlansAndBillingPage() {
const handlePurchaseCredits = async (packageId: number) => {
try {
setPurchaseLoadingId(packageId);
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'stripe' });
// Use payment gateway integration for Stripe/PayPal
if (selectedGateway === 'stripe' || selectedGateway === 'paypal') {
const { redirect_url } = await purchaseCredits(packageId.toString(), selectedGateway);
window.location.href = redirect_url;
return;
}
// Fall back to manual/bank transfer flow
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod as any || 'manual' });
toast?.success?.('Credits purchased successfully!');
await loadData();
} catch (err: any) {
@@ -211,6 +278,15 @@ export default function PlansAndBillingPage() {
}
};
const handleManageSubscription = async () => {
try {
const { portal_url } = await openStripeBillingPortal();
window.location.href = portal_url;
} catch (err: any) {
handleError(err, 'Failed to open billing portal');
}
};
const handleDownloadInvoice = async (invoiceId: number) => {
try {
const blob = await downloadInvoicePDF(invoiceId);
@@ -312,14 +388,26 @@ export default function PlansAndBillingPage() {
{currentPlan?.description || 'Select a plan to unlock features'}
</p>
</div>
<Button
variant="primary"
tone="brand"
onClick={() => setShowUpgradeModal(true)}
startIcon={<ArrowUpIcon className="w-4 h-4" />}
>
Upgrade
</Button>
<div className="flex items-center gap-2">
{availableGateways.stripe && hasActivePlan && (
<Button
variant="outline"
tone="neutral"
onClick={handleManageSubscription}
startIcon={<CreditCardIcon className="w-4 h-4" />}
>
Manage Billing
</Button>
)}
<Button
variant="primary"
tone="brand"
onClick={() => setShowUpgradeModal(true)}
startIcon={<ArrowUpIcon className="w-4 h-4" />}
>
Upgrade
</Button>
</div>
</div>
{/* Quick Stats */}
@@ -467,6 +555,37 @@ export default function PlansAndBillingPage() {
<p className="text-xs text-gray-500 dark:text-gray-400">Top up your credit balance</p>
</div>
</div>
{/* Compact Payment Gateway Selector for Credits */}
{(availableGateways.stripe || availableGateways.paypal) && (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
{availableGateways.stripe && (
<button
onClick={() => setSelectedGateway('stripe')}
className={`p-1.5 rounded-md transition-colors ${
selectedGateway === 'stripe'
? 'bg-white dark:bg-gray-700 shadow-sm'
: 'hover:bg-white/50 dark:hover:bg-gray-700/50'
}`}
title="Pay with Card"
>
<CreditCardIcon className={`w-4 h-4 ${selectedGateway === 'stripe' ? 'text-brand-600' : 'text-gray-500'}`} />
</button>
)}
{availableGateways.paypal && (
<button
onClick={() => setSelectedGateway('paypal')}
className={`p-1.5 rounded-md transition-colors ${
selectedGateway === 'paypal'
? 'bg-white dark:bg-gray-700 shadow-sm'
: 'hover:bg-white/50 dark:hover:bg-gray-700/50'
}`}
title="Pay with PayPal"
>
<WalletIcon className={`w-4 h-4 ${selectedGateway === 'paypal' ? 'text-blue-600' : 'text-gray-500'}`} />
</button>
)}
</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
{packages.slice(0, 4).map((pkg) => (
@@ -701,8 +820,9 @@ export default function PlansAndBillingPage() {
</button>
</div>
{/* Billing Toggle */}
<div className="flex justify-center py-6">
{/* Billing Toggle & Payment Gateway */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 py-6">
{/* Billing Cycle Toggle */}
<div className="bg-gray-100 dark:bg-gray-800 p-1 rounded-lg flex gap-1">
<button
onClick={() => setSelectedBillingCycle('monthly')}
@@ -726,6 +846,51 @@ export default function PlansAndBillingPage() {
<Badge variant="soft" tone="success" size="sm">Save 20%</Badge>
</button>
</div>
{/* Payment Gateway Selector */}
{(availableGateways.stripe || availableGateways.paypal) && (
<div className="bg-gray-100 dark:bg-gray-800 p-1 rounded-lg flex gap-1">
{availableGateways.stripe && (
<button
onClick={() => setSelectedGateway('stripe')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'stripe'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<CreditCardIcon className="w-4 h-4" />
Card
</button>
)}
{availableGateways.paypal && (
<button
onClick={() => setSelectedGateway('paypal')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'paypal'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<WalletIcon className="w-4 h-4" />
PayPal
</button>
)}
{availableGateways.manual && (
<button
onClick={() => setSelectedGateway('manual')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-2 ${
selectedGateway === 'manual'
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
<Building2Icon className="w-4 h-4" />
Bank
</button>
)}
</div>
)}
</div>
{/* Plans Grid */}

View File

@@ -1126,3 +1126,289 @@ export async function getDashboardStats(params?: { site_id?: number; days?: numb
const query = searchParams.toString();
return fetchAPI(`/v1/account/dashboard/stats/${query ? `?${query}` : ''}`);
}
// ============================================================================
// STRIPE INTEGRATION
// ============================================================================
export interface StripeConfig {
publishable_key: string;
is_sandbox: boolean;
}
export interface StripeCheckoutSession {
checkout_url: string;
session_id: string;
}
export interface StripeBillingPortalSession {
portal_url: string;
}
/**
* Get Stripe publishable key for frontend initialization
*/
export async function getStripeConfig(): Promise<StripeConfig> {
return fetchAPI('/v1/billing/stripe/config/');
}
/**
* Create Stripe Checkout session for plan subscription
* Redirects user to Stripe's hosted checkout page
*/
export async function createStripeCheckout(planId: string, options?: {
success_url?: string;
cancel_url?: string;
}): Promise<StripeCheckoutSession> {
return fetchAPI('/v1/billing/stripe/checkout/', {
method: 'POST',
body: JSON.stringify({
plan_id: planId,
...options,
}),
});
}
/**
* Create Stripe Checkout session for credit package purchase
* Redirects user to Stripe's hosted checkout page
*/
export async function createStripeCreditCheckout(packageId: string, options?: {
success_url?: string;
cancel_url?: string;
}): Promise<StripeCheckoutSession> {
return fetchAPI('/v1/billing/stripe/credit-checkout/', {
method: 'POST',
body: JSON.stringify({
package_id: packageId,
...options,
}),
});
}
/**
* Create Stripe Billing Portal session for subscription management
* Allows customers to manage payment methods, view invoices, cancel subscription
*/
export async function openStripeBillingPortal(options?: {
return_url?: string;
}): Promise<StripeBillingPortalSession> {
return fetchAPI('/v1/billing/stripe/billing-portal/', {
method: 'POST',
body: JSON.stringify(options || {}),
});
}
// ============================================================================
// PAYPAL INTEGRATION
// ============================================================================
export interface PayPalConfig {
client_id: string;
is_sandbox: boolean;
currency: string;
}
export interface PayPalOrder {
order_id: string;
status: string;
approval_url: string;
links?: Array<{ rel: string; href: string }>;
credit_package_id?: string;
credit_amount?: number;
plan_id?: string;
plan_name?: string;
}
export interface PayPalCaptureResult {
order_id: string;
status: string;
capture_id: string;
amount: string;
currency: string;
credits_added?: number;
new_balance?: number;
subscription_id?: string;
plan_name?: string;
payment_id?: number;
}
export interface PayPalSubscription {
subscription_id: string;
status: string;
approval_url: string;
links?: Array<{ rel: string; href: string }>;
}
/**
* Get PayPal client ID for frontend initialization
*/
export async function getPayPalConfig(): Promise<PayPalConfig> {
return fetchAPI('/v1/billing/paypal/config/');
}
/**
* Create PayPal order for credit package purchase
* Returns approval URL for PayPal redirect
*/
export async function createPayPalCreditOrder(packageId: string, options?: {
return_url?: string;
cancel_url?: string;
}): Promise<PayPalOrder> {
return fetchAPI('/v1/billing/paypal/create-order/', {
method: 'POST',
body: JSON.stringify({
package_id: packageId,
...options,
}),
});
}
/**
* Create PayPal order for plan subscription (one-time payment model)
* Returns approval URL for PayPal redirect
*/
export async function createPayPalSubscriptionOrder(planId: string, options?: {
return_url?: string;
cancel_url?: string;
}): Promise<PayPalOrder> {
return fetchAPI('/v1/billing/paypal/create-subscription-order/', {
method: 'POST',
body: JSON.stringify({
plan_id: planId,
...options,
}),
});
}
/**
* Capture PayPal order after user approval
* Call this when user returns from PayPal with approved order
*/
export async function capturePayPalOrder(orderId: string, options?: {
package_id?: string;
plan_id?: string;
}): Promise<PayPalCaptureResult> {
return fetchAPI('/v1/billing/paypal/capture-order/', {
method: 'POST',
body: JSON.stringify({
order_id: orderId,
...options,
}),
});
}
/**
* Create PayPal recurring subscription
* Requires plan to have paypal_plan_id configured
*/
export async function createPayPalSubscription(planId: string, options?: {
return_url?: string;
cancel_url?: string;
}): Promise<PayPalSubscription> {
return fetchAPI('/v1/billing/paypal/create-subscription/', {
method: 'POST',
body: JSON.stringify({
plan_id: planId,
...options,
}),
});
}
// ============================================================================
// PAYMENT GATEWAY HELPERS
// ============================================================================
export type PaymentGateway = 'stripe' | 'paypal' | 'manual';
/**
* Helper to check if Stripe is configured
*/
export async function isStripeConfigured(): Promise<boolean> {
try {
const config = await getStripeConfig();
return !!config.publishable_key;
} catch {
return false;
}
}
/**
* Helper to check if PayPal is configured
*/
export async function isPayPalConfigured(): Promise<boolean> {
try {
const config = await getPayPalConfig();
return !!config.client_id;
} catch {
return false;
}
}
/**
* Get available payment gateways
*/
export async function getAvailablePaymentGateways(): Promise<{
stripe: boolean;
paypal: boolean;
manual: boolean;
}> {
const [stripeAvailable, paypalAvailable] = await Promise.all([
isStripeConfigured(),
isPayPalConfigured(),
]);
return {
stripe: stripeAvailable,
paypal: paypalAvailable,
manual: true, // Manual payment is always available
};
}
/**
* Subscribe to plan using preferred payment gateway
*/
export async function subscribeToPlan(
planId: string,
gateway: PaymentGateway,
options?: { return_url?: string; cancel_url?: string }
): Promise<{ redirect_url: string }> {
switch (gateway) {
case 'stripe': {
const session = await createStripeCheckout(planId, options);
return { redirect_url: session.checkout_url };
}
case 'paypal': {
const order = await createPayPalSubscriptionOrder(planId, options);
return { redirect_url: order.approval_url };
}
case 'manual':
throw new Error('Manual payment requires different flow - use submitManualPayment()');
default:
throw new Error(`Unsupported payment gateway: ${gateway}`);
}
}
/**
* Purchase credit package using preferred payment gateway
*/
export async function purchaseCredits(
packageId: string,
gateway: PaymentGateway,
options?: { return_url?: string; cancel_url?: string }
): Promise<{ redirect_url: string }> {
switch (gateway) {
case 'stripe': {
const session = await createStripeCreditCheckout(packageId, options);
return { redirect_url: session.checkout_url };
}
case 'paypal': {
const order = await createPayPalCreditOrder(packageId, options);
return { redirect_url: order.approval_url };
}
case 'manual':
throw new Error('Manual payment requires different flow - use submitManualPayment()');
default:
throw new Error(`Unsupported payment gateway: ${gateway}`);
}
}