Phase 3 & Phase 4 - Completed
This commit is contained in:
192
frontend/src/components/billing/PaymentGatewaySelector.tsx
Normal file
192
frontend/src/components/billing/PaymentGatewaySelector.tsx
Normal 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;
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user