trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/especificaciones/ET-PAY-005-frontend.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

443 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: "ET-PAY-005"
title: "Componentes React Frontend"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-005"
project: "trading-platform"
version: "1.0.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---
# 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. Referencias
- React Stripe Elements
- Zustand State Management
- Stripe Checkout Best Practices