--- id: "ET-PAY-005" title: "Componentes React Frontend" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-005" project: "trading-platform" version: "1.1.0" created_date: "2025-12-05" updated_date: "2026-01-25" --- # ET-PAY-005: Componentes React Frontend **Epic:** OQI-005 Pagos y Stripe **Versión:** 1.0 **Fecha:** 2025-12-05 --- ## 1. Arquitectura Frontend ``` Pages Components Store ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ PricingPage │───────►│ PricingCard │ │ │ └──────────────┘ └──────────────┘ │ paymentStore│ │ │ ┌──────────────┐ ┌──────────────┐ └──────────────┘ │CheckoutPage │───────►│ PaymentForm │ ▲ └──────────────┘ └──────────────┘ │ │ ┌──────────────┐ ┌──────────────┐ │ │ BillingPage │───────►│SubscriptionCard──────────────┘ └──────────────┘ │PaymentMethodList └──────────────┘ ``` --- ## 2. Store con Zustand ```typescript // src/stores/paymentStore.ts import { create } from 'zustand'; import { paymentApi } from '../api/payment.api'; interface PaymentState { payments: Payment[]; subscriptions: Subscription[]; paymentMethods: PaymentMethod[]; loading: boolean; fetchPayments: () => Promise; fetchSubscriptions: () => Promise; fetchPaymentMethods: () => Promise; createPaymentIntent: (amount: number, pmId: string) => Promise; createSubscription: (priceId: string, pmId: string) => Promise; cancelSubscription: (id: string, immediate: boolean) => Promise; } export const usePaymentStore = create((set) => ({ payments: [], subscriptions: [], paymentMethods: [], loading: false, fetchPayments: async () => { set({ loading: true }); const response = await paymentApi.getPayments(); set({ payments: response.data.payments, loading: false }); }, fetchSubscriptions: async () => { const response = await subscriptionApi.list(); set({ subscriptions: response.data.subscriptions }); }, fetchPaymentMethods: async () => { const response = await paymentApi.getPaymentMethods(); set({ paymentMethods: response.data.payment_methods }); }, createPaymentIntent: async (amount, pmId) => { const response = await paymentApi.createPaymentIntent({ amount, payment_method_id: pmId, }); return response.data; }, createSubscription: async (priceId, pmId) => { const response = await subscriptionApi.create({ price_id: priceId, payment_method_id: pmId, }); return response.data; }, cancelSubscription: async (id, immediate) => { await subscriptionApi.cancel(id, immediate); // Refresh subscriptions await usePaymentStore.getState().fetchSubscriptions(); }, })); ``` --- ## 3. Pricing Page ```typescript // src/pages/payment/PricingPage.tsx import React, { useState } from 'react'; import { PricingCard } from '../../components/payment/PricingCard'; import { CheckoutModal } from '../../components/payment/CheckoutModal'; const PLANS = [ { id: 'basic', name: 'Basic', price: 29, interval: 'month', stripe_price_id: 'price_basic_monthly', features: ['Feature 1', 'Feature 2', 'Feature 3'], }, { id: 'pro', name: 'Pro', price: 79, interval: 'month', stripe_price_id: 'price_pro_monthly', features: ['All Basic', 'Feature 4', 'Feature 5', 'Priority Support'], popular: true, }, { id: 'enterprise', name: 'Enterprise', price: 199, interval: 'month', stripe_price_id: 'price_enterprise_monthly', features: ['All Pro', 'Custom ML Agents', 'Dedicated Support'], }, ]; export const PricingPage: React.FC = () => { const [selectedPlan, setSelectedPlan] = useState(null); const [showCheckout, setShowCheckout] = useState(false); const handleSelectPlan = (plan: any) => { setSelectedPlan(plan); setShowCheckout(true); }; return (

Choose Your Plan

Start your AI trading journey today

{PLANS.map((plan) => ( handleSelectPlan(plan)} /> ))}
{showCheckout && selectedPlan && ( setShowCheckout(false)} onSuccess={() => { setShowCheckout(false); // Redirect to dashboard }} /> )}
); }; ``` --- ## 4. Pricing Card Component ```typescript // src/components/payment/PricingCard.tsx import React from 'react'; import './PricingCard.css'; interface PricingCardProps { plan: { name: string; price: number; interval: string; features: string[]; popular?: boolean; }; onSelect: () => void; } export const PricingCard: React.FC = ({ plan, onSelect }) => { return (
{plan.popular &&
Most Popular
}

{plan.name}

${plan.price} /{plan.interval}
    {plan.features.map((feature, idx) => (
  • {feature}
  • ))}
); }; ``` --- ## 5. Checkout Modal ```typescript // src/components/payment/CheckoutModal.tsx import React, { useState } from 'react'; import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import { usePaymentStore } from '../../stores/paymentStore'; const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!); interface CheckoutModalProps { plan: any; onClose: () => void; onSuccess: () => void; } const CheckoutForm: React.FC = ({ plan, onClose, onSuccess }) => { const stripe = useStripe(); const elements = useElements(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const { createSubscription } = usePaymentStore(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!stripe || !elements) return; setLoading(true); setError(null); try { const cardElement = elements.getElement(CardElement); if (!cardElement) throw new Error('Card element not found'); const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement, }); if (pmError) throw new Error(pmError.message); const result = await createSubscription(plan.stripe_price_id, paymentMethod!.id); if (result.client_secret) { const { error: confirmError } = await stripe.confirmCardPayment(result.client_secret); if (confirmError) throw new Error(confirmError.message); } onSuccess(); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; return (

Subscribe to {plan.name}

${plan.price}/{plan.interval}

{error &&
{error}
}
); }; export const CheckoutModal: React.FC = (props) => { return ( ); }; ``` --- ## 6. Billing Page ```typescript // src/pages/payment/BillingPage.tsx import React, { useEffect } from 'react'; import { usePaymentStore } from '../../stores/paymentStore'; import { SubscriptionCard } from '../../components/payment/SubscriptionCard'; import { PaymentMethodList } from '../../components/payment/PaymentMethodList'; import { InvoiceList } from '../../components/payment/InvoiceList'; export const BillingPage: React.FC = () => { const { subscriptions, paymentMethods, fetchSubscriptions, fetchPaymentMethods, } = usePaymentStore(); useEffect(() => { fetchSubscriptions(); fetchPaymentMethods(); }, []); return (

Billing & Subscriptions

Active Subscriptions

{subscriptions.map((sub) => ( ))}

Payment Methods

Invoices

); }; ``` --- ## 7. Subscription Card ```typescript // src/components/payment/SubscriptionCard.tsx import React from 'react'; import { usePaymentStore } from '../../stores/paymentStore'; interface SubscriptionCardProps { subscription: Subscription; } export const SubscriptionCard: React.FC = ({ subscription }) => { const { cancelSubscription } = usePaymentStore(); const handleCancel = async () => { if (confirm('Are you sure you want to cancel?')) { await cancelSubscription(subscription.id, false); } }; return (

{subscription.plan_name}

{subscription.status}

${subscription.amount} / {subscription.plan_interval}

Next billing: {new Date(subscription.current_period_end).toLocaleDateString()}

{subscription.cancel_at_period_end ? ( Cancels at period end ) : ( )}
); }; ``` --- ## 8. Componentes Avanzados (2026-01-25) Los siguientes componentes fueron agregados para mejorar la integración Stripe y gestión de reembolsos: ### 8.1 StripeElementsWrapper **Ubicación:** `src/components/payments/StripeElementsWrapper.tsx` **LOC:** 220 Foundation para PCI-DSS compliance. Provee: - `StripeElementsWrapper` - Componente wrapper - `withStripeElements` - HOC para envolver componentes - `useStripeAvailable` - Hook para verificar disponibilidad ```typescript import { StripeElementsWrapper, useStripeAvailable } from '@/components/payments'; // Uso como wrapper // Uso del hook const isAvailable = useStripeAvailable(); ``` ### 8.2 InvoicePreview **Ubicación:** `src/components/payments/InvoicePreview.tsx` **LOC:** 350 Vista previa de factura pre-checkout con: - Desglose de items - Descuentos (% o fijo) - Impuestos - Total calculado - Botones de acción ```typescript import { InvoicePreview, InvoicePreviewData } from '@/components/payments'; const data: InvoicePreviewData = { items: [{ id: '1', description: 'Pro Plan', quantity: 1, unitPrice: 79, total: 79 }], subtotal: 79, taxes: [{ name: 'IVA', rate: 16, amount: 12.64 }], total: 91.64 }; ``` ### 8.3 RefundRequestModal **Ubicación:** `src/components/payments/RefundRequestModal.tsx` **LOC:** 480 Modal para solicitar reembolsos: - Verificación de elegibilidad - Razones predefinidas - Reembolso parcial o total - Validación de montos ```typescript import { RefundRequestModal, RefundEligibility } from '@/components/payments'; const eligibility: RefundEligibility = { eligible: true, maxAmount: 79, daysRemaining: 25 }; ``` ### 8.4 RefundList **Ubicación:** `src/components/payments/RefundList.tsx` **LOC:** 450 Lista paginada de reembolsos: - Filtro por estado - Paginación - Badges de estado - Detalles expandibles ```typescript import { RefundList, Refund } from '@/components/payments'; ``` --- ## 9. Resumen de Componentes | Componente | Ubicación | LOC | Estado | |------------|-----------|-----|--------| | PricingCard | components/payments/ | ~50 | Done | | CheckoutModal | components/payments/ | ~100 | Done | | SubscriptionCard | components/payments/ | ~50 | Done | | PaymentMethodList | components/payments/ | ~80 | Done | | InvoiceList | components/payments/ | ~100 | Done | | **StripeElementsWrapper** | components/payments/ | 220 | **Done (2026-01-25)** | | **InvoicePreview** | components/payments/ | 350 | **Done (2026-01-25)** | | **RefundRequestModal** | components/payments/ | 480 | **Done (2026-01-25)** | | **RefundList** | components/payments/ | 450 | **Done (2026-01-25)** | --- ## 10. Referencias - React Stripe Elements - Zustand State Management - Stripe Checkout Best Practices - [TASK-2026-01-25-OQI-005-PAYMENTS-ADVANCED](../../../orchestration/tareas/TASK-2026-01-25-OQI-005-PAYMENTS-ADVANCED/)