---
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<void>;
  fetchSubscriptions: () => Promise<void>;
  fetchPaymentMethods: () => Promise<void>;
  createPaymentIntent: (amount: number, pmId: string) => Promise<any>;
  createSubscription: (priceId: string, pmId: string) => Promise<any>;
  cancelSubscription: (id: string, immediate: boolean) => Promise<void>;
}

export const usePaymentStore = create<PaymentState>((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<any>(null);
  const [showCheckout, setShowCheckout] = useState(false);

  const handleSelectPlan = (plan: any) => {
    setSelectedPlan(plan);
    setShowCheckout(true);
  };

  return (
    <div className="pricing-page">
      <header>
        <h1>Choose Your Plan</h1>
        <p>Start your AI trading journey today</p>
      </header>

      <div className="pricing-grid">
        {PLANS.map((plan) => (
          <PricingCard
            key={plan.id}
            plan={plan}
            onSelect={() => handleSelectPlan(plan)}
          />
        ))}
      </div>

      {showCheckout && selectedPlan && (
        <CheckoutModal
          plan={selectedPlan}
          onClose={() => setShowCheckout(false)}
          onSuccess={() => {
            setShowCheckout(false);
            // Redirect to dashboard
          }}
        />
      )}
    </div>
  );
};
```

---

## 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<PricingCardProps> = ({ plan, onSelect }) => {
  return (
    <div className={`pricing-card ${plan.popular ? 'popular' : ''}`}>
      {plan.popular && <div className="badge">Most Popular</div>}

      <h3>{plan.name}</h3>

      <div className="price">
        <span className="amount">${plan.price}</span>
        <span className="interval">/{plan.interval}</span>
      </div>

      <ul className="features">
        {plan.features.map((feature, idx) => (
          <li key={idx}>
            <span className="checkmark">✓</span> {feature}
          </li>
        ))}
      </ul>

      <button className="select-button" onClick={onSelect}>
        Get Started
      </button>
    </div>
  );
};
```

---

## 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<CheckoutModalProps> = ({ plan, onClose, onSuccess }) => {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(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 (
    <div className="checkout-modal">
      <div className="modal-content">
        <button className="close-button" onClick={onClose}>×</button>

        <h2>Subscribe to {plan.name}</h2>
        <p className="price">${plan.price}/{plan.interval}</p>

        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label>Card Details</label>
            <CardElement />
          </div>

          {error && <div className="error">{error}</div>}

          <button type="submit" disabled={!stripe || loading}>
            {loading ? 'Processing...' : 'Subscribe Now'}
          </button>
        </form>
      </div>
    </div>
  );
};

export const CheckoutModal: React.FC<CheckoutModalProps> = (props) => {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutForm {...props} />
    </Elements>
  );
};
```

---

## 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 (
    <div className="billing-page">
      <h1>Billing & Subscriptions</h1>

      <section className="subscriptions-section">
        <h2>Active Subscriptions</h2>
        {subscriptions.map((sub) => (
          <SubscriptionCard key={sub.id} subscription={sub} />
        ))}
      </section>

      <section className="payment-methods-section">
        <h2>Payment Methods</h2>
        <PaymentMethodList methods={paymentMethods} />
      </section>

      <section className="invoices-section">
        <h2>Invoices</h2>
        <InvoiceList />
      </section>
    </div>
  );
};
```

---

## 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<SubscriptionCardProps> = ({ subscription }) => {
  const { cancelSubscription } = usePaymentStore();

  const handleCancel = async () => {
    if (confirm('Are you sure you want to cancel?')) {
      await cancelSubscription(subscription.id, false);
    }
  };

  return (
    <div className="subscription-card">
      <div className="plan-info">
        <h3>{subscription.plan_name}</h3>
        <span className="status">{subscription.status}</span>
      </div>

      <div className="billing-info">
        <p>
          <strong>${subscription.amount}</strong> / {subscription.plan_interval}
        </p>
        <p className="next-billing">
          Next billing: {new Date(subscription.current_period_end).toLocaleDateString()}
        </p>
      </div>

      <div className="actions">
        {subscription.cancel_at_period_end ? (
          <span className="canceling">Cancels at period end</span>
        ) : (
          <button onClick={handleCancel}>Cancel Subscription</button>
        )}
      </div>
    </div>
  );
};
```

---

## 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
<StripeElementsWrapper config={{ publicKey: 'pk_test_...' }}>
  <PaymentForm />
</StripeElementsWrapper>

// 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
};

<InvoicePreview data={data} onConfirm={handleConfirm} />
```

### 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
};

<RefundRequestModal
  isOpen={true}
  transactionId="tx_123"
  transactionAmount={79}
  eligibility={eligibility}
  onSubmit={handleRefundRequest}
/>
```

### 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';

<RefundList
  refunds={refunds}
  totalCount={100}
  currentPage={1}
  pageSize={10}
  onPageChange={handlePageChange}
  onStatusFilter={handleStatusFilter}
/>
```

---

## 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/)
