diff --git a/frontend/src/api/services/subscription.ts b/frontend/src/api/services/subscription.ts index 959308d0..758b4694 100644 --- a/frontend/src/api/services/subscription.ts +++ b/frontend/src/api/services/subscription.ts @@ -402,6 +402,24 @@ export class SubscriptionService { return apiClient.get(`/subscriptions/${tenantId}/invoices`); } + /** + * Update the default payment method for a subscription + */ + async updatePaymentMethod( + tenantId: string, + paymentMethodId: string + ): Promise<{ + success: boolean; + message: string; + payment_method_id: string; + brand: string; + last4: string; + exp_month?: number; + exp_year?: number; + }> { + return apiClient.post(`/subscriptions/${tenantId}/update-payment-method?payment_method_id=${paymentMethodId}`, {}); + } + // ============================================================================ // NEW METHODS - Usage Forecasting & Predictive Analytics // ============================================================================ diff --git a/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx b/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx new file mode 100644 index 00000000..91dfe187 --- /dev/null +++ b/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx @@ -0,0 +1,258 @@ +import React, { useState, useEffect } from 'react'; +import { CreditCard, X, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; +import { Button, Modal, Input } from '../../components/ui'; +import { DialogModal } from '../../components/ui/DialogModal/DialogModal'; +import { subscriptionService } from '../../api'; +import { showToast } from '../../utils/toast'; +import { useTranslation } from 'react-i18next'; + +interface PaymentMethodUpdateModalProps { + isOpen: boolean; + onClose: () => void; + tenantId: string; + currentPaymentMethod?: { + brand?: string; + last4?: string; + exp_month?: number; + exp_year?: number; + }; + onPaymentMethodUpdated: (paymentMethod: { + brand: string; + last4: string; + exp_month?: number; + exp_year?: number; + }) => void; +} + +export const PaymentMethodUpdateModal: React.FC = ({ + isOpen, + onClose, + tenantId, + currentPaymentMethod, + onPaymentMethodUpdated +}) => { + const { t } = useTranslation('subscription'); + const [loading, setLoading] = useState(false); + const [paymentMethodId, setPaymentMethodId] = useState(''); + const [stripe, setStripe] = useState(null); + const [elements, setElements] = useState(null); + const [cardElement, setCardElement] = useState(null); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + // Load Stripe.js dynamically + useEffect(() => { + if (isOpen) { + const loadStripe = async () => { + try { + // Load Stripe.js from CDN + const stripeScript = document.createElement('script'); + stripeScript.src = 'https://js.stripe.com/v3/'; + stripeScript.async = true; + stripeScript.onload = () => { + const stripeInstance = (window as any).Stripe('pk_test_your_publishable_key'); // Replace with actual key + setStripe(stripeInstance); + const elementsInstance = stripeInstance.elements(); + setElements(elementsInstance); + }; + document.body.appendChild(stripeScript); + } catch (err) { + console.error('Failed to load Stripe:', err); + setError('Failed to load payment processor'); + } + }; + + loadStripe(); + } + }, [isOpen]); + + // Create card element when Stripe and elements are loaded + useEffect(() => { + if (elements && !cardElement) { + const card = elements.create('card', { + style: { + base: { + fontSize: '16px', + color: '#32325d', + '::placeholder': { + color: '#aab7c4', + }, + }, + invalid: { + color: '#fa755a', + iconColor: '#fa755a', + }, + }, + }); + + // Mount the card element + const cardElementContainer = document.getElementById('card-element'); + if (cardElementContainer) { + card.mount(cardElementContainer); + setCardElement(card); + } + } + }, [elements, cardElement]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!stripe || !cardElement) { + setError('Payment processor not loaded'); + return; + } + + setLoading(true); + setError(null); + setSuccess(false); + + try { + // Create payment method using Stripe Elements + const { paymentMethod, error: stripeError } = await stripe.createPaymentMethod({ + type: 'card', + card: cardElement, + }); + + if (stripeError) { + setError(stripeError.message || 'Failed to create payment method'); + setLoading(false); + return; + } + + if (!paymentMethod || !paymentMethod.id) { + setError('No payment method created'); + setLoading(false); + return; + } + + // Call backend to update payment method + const result = await subscriptionService.updatePaymentMethod(tenantId, paymentMethod.id); + + if (result.success) { + setSuccess(true); + showToast.success(result.message); + + // Notify parent component about the update + onPaymentMethodUpdated({ + brand: result.brand, + last4: result.last4, + exp_month: result.exp_month, + exp_year: result.exp_year, + }); + + // Close modal after a brief delay to show success message + setTimeout(() => { + onClose(); + }, 2000); + } else { + setError(result.message || 'Failed to update payment method'); + } + + } catch (err) { + console.error('Error updating payment method:', err); + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setError(null); + setSuccess(false); + setPaymentMethodId(''); + onClose(); + }; + + return ( + +
+ {/* Current Payment Method Info */} + {currentPaymentMethod && ( +
+

Método de Pago Actual

+
+ +
+

+ {currentPaymentMethod.brand} terminando en {currentPaymentMethod.last4} +

+ {currentPaymentMethod.exp_month && currentPaymentMethod.exp_year && ( +

+ Expira: {currentPaymentMethod.exp_month}/{currentPaymentMethod.exp_year} +

+ )} +
+
+
+ )} + + {/* Payment Form */} +
+
+ +
+ {/* Stripe card element will be mounted here */} + {!cardElement && !error && ( +
+ + Cargando procesador de pagos... +
+ )} +
+
+ + {error && ( +
+ + {error} +
+ )} + + {success && ( +
+ + Método de pago actualizado correctamente +
+ )} + +
+ + +
+
+ + {/* Security Info */} +
+

+ + Tus datos de pago están protegidos y se procesan de forma segura. +

+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/pages/app/settings/subscription/SubscriptionPage.tsx b/frontend/src/pages/app/settings/subscription/SubscriptionPage.tsx index ff551ae9..bb85ee49 100644 --- a/frontend/src/pages/app/settings/subscription/SubscriptionPage.tsx +++ b/frontend/src/pages/app/settings/subscription/SubscriptionPage.tsx @@ -10,6 +10,7 @@ import { subscriptionService, type UsageSummary, type AvailablePlans } from '../ import { useSubscriptionEvents } from '../../../../contexts/SubscriptionEventsContext'; import { SubscriptionPricingCards } from '../../../../components/subscription/SubscriptionPricingCards'; import { PlanComparisonTable, ROICalculator, UsageMetricCard } from '../../../../components/subscription'; +import { PaymentMethodUpdateModal } from '../../../../components/subscription/PaymentMethodUpdateModal'; import { useSubscription } from '../../../../hooks/useSubscription'; import { trackSubscriptionPageViewed, @@ -41,6 +42,13 @@ const SubscriptionPage: React.FC = () => { // New state for enhanced features const [showComparison, setShowComparison] = useState(false); const [showROI, setShowROI] = useState(false); + const [paymentMethodModalOpen, setPaymentMethodModalOpen] = useState(false); + const [currentPaymentMethod, setCurrentPaymentMethod] = useState<{ + brand?: string; + last4?: string; + exp_month?: number; + exp_year?: number; + } | null>(null); // Use new subscription hook for usage forecast data const { subscription: subscriptionData, usage: forecastUsage, forecast } = useSubscription(); @@ -916,7 +924,7 @@ const SubscriptionPage: React.FC = () => {