EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
PayInvoiceModal.jsx
Go to the documentation of this file.
1import { useState, useEffect, useMemo } from 'react';
2import { loadStripe } from '@stripe/stripe-js';
3import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
4import { apiFetch } from '../lib/api';
5import './PayInvoiceModal.css';
6
7// Payment form component
8function PaymentForm({ invoice, onSuccess, onCancel }) {
9 const stripe = useStripe();
10 const elements = useElements();
11 const [processing, setProcessing] = useState(false);
12 const [error, setError] = useState(null);
13
14 const handleSubmit = async (e) => {
15 e.preventDefault();
16
17 if (!stripe || !elements) {
18 setError('Payment form not ready. Please wait...');
19 return;
20 }
21
22 setProcessing(true);
23 setError(null);
24
25 try {
26 // Confirm payment
27 const { error: stripeError, paymentIntent } = await stripe.confirmPayment({
28 elements,
29 confirmParams: {
30 return_url: `${window.location.origin}/invoices`,
31 },
32 redirect: 'if_required',
33 });
34
35 if (stripeError) {
36 setError(stripeError.message);
37 setProcessing(false);
38 return;
39 }
40
41 if (paymentIntent && paymentIntent.status === 'succeeded') {
42 // Confirm payment on backend
43 const token = localStorage.getItem('token');
44 const response = await apiFetch(`/invoices/${invoice.invoice_id}/confirm-payment`, {
45 method: 'POST',
46 headers: {
47 'Authorization': `Bearer ${token}`,
48 'Content-Type': 'application/json',
49 },
50 body: JSON.stringify({
51 paymentIntentId: paymentIntent.id,
52 }),
53 });
54
55 if (!response.ok) {
56 const data = await response.json();
57 throw new Error(data.error || 'Failed to confirm payment');
58 }
59
60 onSuccess();
61 }
62 } catch (err) {
63 setError(err.message || 'Payment failed');
64 setProcessing(false);
65 }
66 };
67
68 return (
69 <form onSubmit={handleSubmit} className="payment-form">
70 <div className="invoice-summary">
71 <h3>Invoice #{invoice.invoice_id}</h3>
72 <p>{invoice.description || 'Payment for invoice'}</p>
73 <div className="amount-display">
74 <span className="label">Amount Due:</span>
75 <span className="amount">${parseFloat(invoice.total || invoice.amount || 0).toFixed(2)}</span>
76 </div>
77 </div>
78
79 <div className="payment-element-container">
80 <PaymentElement />
81 </div>
82
83 {error && <div className="error-message">{error}</div>}
84
85 <div className="modal-actions">
86 <button
87 type="button"
88 onClick={onCancel}
89 className="btn btn-secondary"
90 disabled={processing}
91 >
92 Cancel
93 </button>
94 <button
95 type="submit"
96 className="btn btn-primary"
97 disabled={!stripe || !elements || processing}
98 >
99 {processing ? 'Processing...' : `Pay $${parseFloat(invoice.total || invoice.amount || 0).toFixed(2)}`}
100 </button>
101 </div>
102 </form>
103 );
104}
105
106// Main modal component
107export default function PayInvoiceModal({ invoice, onClose, onSuccess }) {
108 const [clientSecret, setClientSecret] = useState(null);
109 const [publishableKey, setPublishableKey] = useState(null);
110 const [stripeAccount, setStripeAccount] = useState(null);
111 const [loading, setLoading] = useState(true);
112 const [error, setError] = useState(null);
113
114 // Load payment intent when modal opens
115 useEffect(() => {
116 async function createPaymentIntent() {
117 try {
118 const token = localStorage.getItem('token');
119 const response = await apiFetch(`/invoices/${invoice.invoice_id}/create-payment-intent`, {
120 method: 'POST',
121 headers: {
122 'Authorization': `Bearer ${token}`,
123 },
124 });
125
126 if (!response.ok) {
127 const data = await response.json();
128 throw new Error(data.error || 'Failed to initialize payment');
129 }
130
131 const data = await response.json();
132 setPublishableKey(data.publishableKey);
133 setStripeAccount(data.stripeAccount);
134 setClientSecret(data.clientSecret);
135 setLoading(false);
136 } catch (err) {
137 setError(err.message);
138 setLoading(false);
139 }
140 }
141
142 createPaymentIntent();
143 }, [invoice.invoice_id]);
144
145 // Only render Elements when we have the necessary data
146 if (loading) {
147 return (
148 <div className="modal-overlay" onClick={onClose}>
149 <div className="modal-content pay-invoice-modal" onClick={(e) => e.stopPropagation()}>
150 <div className="modal-header">
151 <h2>Pay Invoice</h2>
152 <button className="close-btn" onClick={onClose}>
153 <span className="material-symbols-outlined">close</span>
154 </button>
155 </div>
156 <div className="modal-body">
157 <div className="loading-state">
158 <div className="spinner"></div>
159 <p>Loading payment form...</p>
160 </div>
161 </div>
162 </div>
163 </div>
164 );
165 }
166
167 if (error) {
168 return (
169 <div className="modal-overlay" onClick={onClose}>
170 <div className="modal-content pay-invoice-modal" onClick={(e) => e.stopPropagation()}>
171 <div className="modal-header">
172 <h2>Pay Invoice</h2>
173 <button className="close-btn" onClick={onClose}>
174 <span className="material-symbols-outlined">close</span>
175 </button>
176 </div>
177 <div className="modal-body">
178 <div className="error-state">
179 <span className="material-symbols-outlined">error</span>
180 <p>{error}</p>
181 <button className="btn btn-secondary" onClick={onClose}>Close</button>
182 </div>
183 </div>
184 </div>
185 </div>
186 );
187 }
188
189 // Create stripe promise with connected account (this will be cached)
190 const stripePromise = loadStripe(publishableKey, {
191 stripeAccount: stripeAccount, // Pass connected account ID
192 });
193
194 const options = {
195 clientSecret,
196 appearance: {
197 theme: 'stripe',
198 variables: {
199 colorPrimary: '#3b82f6',
200 },
201 },
202 };
203
204 return (
205 <div className="modal-overlay" onClick={onClose}>
206 <div className="modal-content pay-invoice-modal" onClick={(e) => e.stopPropagation()}>
207 <div className="modal-header">
208 <h2>Pay Invoice</h2>
209 <button className="close-btn" onClick={onClose}>
210 <span className="material-symbols-outlined">close</span>
211 </button>
212 </div>
213
214 <div className="modal-body">
215 {clientSecret && publishableKey && stripeAccount && (
216 <Elements stripe={stripePromise} options={options}>
217 <PaymentForm
218 invoice={invoice}
219 onSuccess={() => {
220 onSuccess();
221 onClose();
222 }}
223 onCancel={onClose}
224 />
225 </Elements>
226 )}
227 </div>
228 </div>
229 </div>
230 );
231}