/ frontend / src / components / BillingPage.js
BillingPage.js
  1  import React, { useState, useEffect } from 'react';
  2  import { loadStripe } from '@stripe/stripe-js';
  3  import { Elements } from '@stripe/react-stripe-js';
  4  import axios from 'axios';
  5  import { Link, useNavigate } from 'react-router-dom';
  6  
  7  import SubscriptionPage from './SubscriptionPage';
  8  import PaymentMethodManager from './PaymentMethodManager';
  9  
 10  // Replace with your Stripe publishable key
 11  const stripePromise = loadStripe('pk_test_51XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
 12  
 13  const BillingPage = () => {
 14    const [activeTab, setActiveTab] = useState('subscription');
 15    const [subscription, setSubscription] = useState(null);
 16    const [invoices, setInvoices] = useState([]);
 17    const [loading, setLoading] = useState(true);
 18    const [error, setError] = useState(null);
 19    const [usageStats, setUsageStats] = useState(null);
 20    const navigate = useNavigate();
 21  
 22    useEffect(() => {
 23      // Check if user is authenticated
 24      const token = localStorage.getItem('authToken');
 25      if (!token) {
 26        navigate('/login');
 27        return;
 28      }
 29  
 30      // Fetch billing data
 31      fetchBillingData();
 32    }, [navigate]);
 33  
 34    const fetchBillingData = async () => {
 35      try {
 36        setLoading(true);
 37        setError(null);
 38        
 39        const token = localStorage.getItem('authToken');
 40        
 41        // Fetch current subscription
 42        const subscriptionResponse = await axios.get('/api/subscriptions/current', {
 43          headers: {
 44            'Authorization': `Bearer ${token}`
 45          }
 46        });
 47        
 48        if (subscriptionResponse.status === 200) {
 49          setSubscription(subscriptionResponse.data.subscription);
 50        }
 51        
 52        // Fetch invoices
 53        const invoicesResponse = await axios.get('/api/subscriptions/invoices', {
 54          headers: {
 55            'Authorization': `Bearer ${token}`
 56          }
 57        });
 58        
 59        if (invoicesResponse.status === 200) {
 60          setInvoices(invoicesResponse.data.invoices);
 61        }
 62        
 63        // Fetch usage statistics
 64        const usageResponse = await axios.get('/api/auth/usage-stats', {
 65          headers: {
 66            'Authorization': `Bearer ${token}`
 67          }
 68        });
 69        
 70        if (usageResponse.status === 200) {
 71          setUsageStats(usageResponse.data);
 72        }
 73        
 74      } catch (err) {
 75        console.error('Error fetching billing data:', err);
 76        setError('Unable to load billing information. Please try again later.');
 77      } finally {
 78        setLoading(false);
 79      }
 80    };
 81    
 82    const handleCancelSubscription = async () => {
 83      if (!subscription) return;
 84      
 85      if (!confirm('Are you sure you want to cancel your subscription? Your plan will remain active until the end of the current billing period.')) {
 86        return;
 87      }
 88      
 89      try {
 90        setLoading(true);
 91        
 92        const token = localStorage.getItem('authToken');
 93        const response = await axios.post('/api/subscriptions/cancel', {
 94          subscriptionId: subscription.id
 95        }, {
 96          headers: {
 97            'Authorization': `Bearer ${token}`
 98          }
 99        });
100        
101        if (response.status === 200) {
102          // Refresh subscription data
103          setSubscription(response.data.subscription);
104          alert('Your subscription has been canceled and will end on ' + new Date(response.data.subscription.current_period_end).toLocaleDateString());
105        }
106        
107      } catch (err) {
108        console.error('Error canceling subscription:', err);
109        setError('Unable to cancel subscription. Please try again later.');
110      } finally {
111        setLoading(false);
112      }
113    };
114    
115    const handleReactivateSubscription = async () => {
116      if (!subscription) return;
117      
118      try {
119        setLoading(true);
120        
121        const token = localStorage.getItem('authToken');
122        const response = await axios.post('/api/subscriptions/reactivate', {
123          subscriptionId: subscription.id
124        }, {
125          headers: {
126            'Authorization': `Bearer ${token}`
127          }
128        });
129        
130        if (response.status === 200) {
131          // Refresh subscription data
132          setSubscription(response.data.subscription);
133          alert('Your subscription has been reactivated!');
134        }
135        
136      } catch (err) {
137        console.error('Error reactivating subscription:', err);
138        setError('Unable to reactivate subscription. Please try again later.');
139      } finally {
140        setLoading(false);
141      }
142    };
143  
144    const renderTabContent = () => {
145      switch (activeTab) {
146        case 'subscription':
147          return (
148            <div>
149              {subscription ? (
150                <div className="bg-white rounded-lg shadow overflow-hidden">
151                  <div className="p-6">
152                    <h3 className="text-lg font-medium text-gray-900">Current Subscription</h3>
153                    <div className="mt-2 text-sm text-gray-600">
154                      <div className="flex justify-between mb-2">
155                        <span>Plan:</span>
156                        <span className="font-semibold">{subscription.planName}</span>
157                      </div>
158                      <div className="flex justify-between mb-2">
159                        <span>Status:</span>
160                        <span className={`font-semibold ${subscription.status === 'active' ? 'text-green-600' : 'text-yellow-600'}`}>
161                          {subscription.status.charAt(0).toUpperCase() + subscription.status.slice(1)}
162                        </span>
163                      </div>
164                      <div className="flex justify-between mb-2">
165                        <span>Current period ends:</span>
166                        <span className="font-semibold">{new Date(subscription.current_period_end).toLocaleDateString()}</span>
167                      </div>
168                      {subscription.cancel_at_period_end && (
169                        <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-4">
170                          <div className="flex">
171                            <div className="flex-shrink-0">
172                              <svg className="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
173                                <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
174                              </svg>
175                            </div>
176                            <div className="ml-3">
177                              <p className="text-sm text-yellow-700">
178                                Your subscription will be canceled on {new Date(subscription.current_period_end).toLocaleDateString()}.
179                                <button
180                                  onClick={handleReactivateSubscription}
181                                  className="ml-2 font-medium text-yellow-700 underline"
182                                >
183                                  Reactivate
184                                </button>
185                              </p>
186                            </div>
187                          </div>
188                        </div>
189                      )}
190                    </div>
191                    <div className="mt-6 flex justify-between">
192                      <button
193                        onClick={() => setActiveTab('change')}
194                        className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
195                      >
196                        Change Plan
197                      </button>
198                      {!subscription.cancel_at_period_end && (
199                        <button
200                          onClick={handleCancelSubscription}
201                          className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
202                        >
203                          Cancel Subscription
204                        </button>
205                      )}
206                    </div>
207                  </div>
208                </div>
209              ) : (
210                <div className="bg-white rounded-lg shadow overflow-hidden">
211                  <div className="p-6">
212                    <h3 className="text-lg font-medium text-gray-900">No Active Subscription</h3>
213                    <p className="mt-2 text-sm text-gray-600">
214                      You don't have an active subscription yet. Choose a plan to get started.
215                    </p>
216                    <div className="mt-6">
217                      <button
218                        onClick={() => setActiveTab('change')}
219                        className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
220                      >
221                        Choose a Plan
222                      </button>
223                    </div>
224                  </div>
225                </div>
226              )}
227              
228              {/* Usage Statistics */}
229              {usageStats && (
230                <div className="mt-8 bg-white rounded-lg shadow overflow-hidden">
231                  <div className="p-6">
232                    <h3 className="text-lg font-medium text-gray-900 mb-4">Current Usage</h3>
233                    <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
234                      <div className="bg-blue-50 p-4 rounded-lg">
235                        <div className="text-sm text-gray-500">API Requests Today</div>
236                        <div className="mt-1 flex justify-between items-baseline">
237                          <div className="text-2xl font-semibold text-gray-900">{usageStats.daily_requests || 0}</div>
238                          <div className="text-sm text-gray-500">/ {usageStats.daily_limit || '∞'}</div>
239                        </div>
240                        <div className="mt-3 w-full bg-gray-200 rounded-full h-2">
241                          <div 
242                            className="bg-blue-600 h-2 rounded-full" 
243                            style={{ 
244                              width: `${usageStats.daily_limit ? Math.min(100, (usageStats.daily_requests / usageStats.daily_limit) * 100) : 0}%` 
245                            }}
246                          ></div>
247                        </div>
248                      </div>
249                      
250                      <div className="bg-purple-50 p-4 rounded-lg">
251                        <div className="text-sm text-gray-500">API Requests This Month</div>
252                        <div className="mt-1 flex justify-between items-baseline">
253                          <div className="text-2xl font-semibold text-gray-900">{usageStats.monthly_requests || 0}</div>
254                          <div className="text-sm text-gray-500">/ {usageStats.monthly_limit || '∞'}</div>
255                        </div>
256                        <div className="mt-3 w-full bg-gray-200 rounded-full h-2">
257                          <div 
258                            className="bg-purple-600 h-2 rounded-full" 
259                            style={{ 
260                              width: `${usageStats.monthly_limit ? Math.min(100, (usageStats.monthly_requests / usageStats.monthly_limit) * 100) : 0}%` 
261                            }}
262                          ></div>
263                        </div>
264                      </div>
265                      
266                      <div className="bg-green-50 p-4 rounded-lg">
267                        <div className="text-sm text-gray-500">Total Tokens</div>
268                        <div className="mt-1 text-2xl font-semibold text-gray-900">
269                          {usageStats.total_tokens ? 
270                            usageStats.total_tokens.toLocaleString() : 0}
271                        </div>
272                        <div className="mt-2 text-sm text-gray-500">
273                          Input: {usageStats.total_tokens_input ? 
274                            usageStats.total_tokens_input.toLocaleString() : 0}
275                        </div>
276                        <div className="text-sm text-gray-500">
277                          Output: {usageStats.total_tokens_output ? 
278                            usageStats.total_tokens_output.toLocaleString() : 0}
279                        </div>
280                      </div>
281                    </div>
282                  </div>
283                </div>
284              )}
285            </div>
286          );
287          
288        case 'payment':
289          return (
290            <Elements stripe={stripePromise}>
291              <PaymentMethodManager />
292            </Elements>
293          );
294          
295        case 'invoices':
296          return (
297            <div className="bg-white rounded-lg shadow overflow-hidden">
298              <div className="px-6 py-5 border-b border-gray-200">
299                <h3 className="text-lg font-medium text-gray-900">Billing History</h3>
300              </div>
301              <div className="bg-white">
302                {invoices.length === 0 ? (
303                  <div className="text-center py-8">
304                    <p className="text-gray-500">No invoices found.</p>
305                  </div>
306                ) : (
307                  <ul className="divide-y divide-gray-200">
308                    {invoices.map((invoice) => (
309                      <li key={invoice.id} className="px-6 py-4">
310                        <div className="flex items-center justify-between">
311                          <div>
312                            <p className="text-sm font-medium text-gray-900">
313                              Invoice {invoice.number}
314                            </p>
315                            <p className="text-sm text-gray-500">
316                              {new Date(invoice.date).toLocaleDateString()}
317                            </p>
318                          </div>
319                          <div className="flex items-center">
320                            <span className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
321                              invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
322                              invoice.status === 'open' ? 'bg-blue-100 text-blue-800' :
323                              'bg-gray-100 text-gray-800'
324                            }`}>
325                              {invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
326                            </span>
327                            <span className="ml-4 text-sm font-medium text-gray-900">
328                              {invoice.amount.toLocaleString(undefined, {
329                                style: 'currency',
330                                currency: invoice.currency.toUpperCase()
331                              })}
332                            </span>
333                            {invoice.pdf && (
334                              <a
335                                href={invoice.pdf}
336                                target="_blank"
337                                rel="noopener noreferrer"
338                                className="ml-4 text-sm font-medium text-blue-600 hover:text-blue-500"
339                              >
340                                PDF
341                              </a>
342                            )}
343                          </div>
344                        </div>
345                      </li>
346                    ))}
347                  </ul>
348                )}
349              </div>
350            </div>
351          );
352          
353        case 'change':
354          return <SubscriptionPage currentSubscription={subscription} onSubscriptionChange={fetchBillingData} />;
355          
356        default:
357          return null;
358      }
359    };
360    
361    if (loading && !subscription && !invoices && !usageStats) {
362      return (
363        <div className="min-h-screen flex items-center justify-center">
364          <div className="text-center">
365            <svg className="animate-spin h-12 w-12 mx-auto text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
366              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
367              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
368            </svg>
369            <p className="mt-3 text-gray-600">Loading billing information...</p>
370          </div>
371        </div>
372      );
373    }
374  
375    return (
376      <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
377        <div className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
378          <h2 className="text-3xl font-bold leading-tight text-gray-900">
379            Billing & Subscription
380          </h2>
381          <div className="mt-3 sm:mt-0">
382            <Link
383              to="/dashboard"
384              className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
385            >
386              Back to Dashboard
387            </Link>
388          </div>
389        </div>
390        
391        {error && (
392          <div className="mt-6 bg-red-50 border-l-4 border-red-400 p-4">
393            <div className="flex">
394              <div className="flex-shrink-0">
395                <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
396                  <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
397                </svg>
398              </div>
399              <div className="ml-3">
400                <p className="text-sm text-red-700">
401                  {error}
402                </p>
403              </div>
404            </div>
405          </div>
406        )}
407        
408        <div className="mt-6">
409          <div className="sm:hidden">
410            <select
411              id="tabs"
412              name="tabs"
413              className="mt-4 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
414              value={activeTab}
415              onChange={(e) => setActiveTab(e.target.value)}
416            >
417              <option value="subscription">Subscription</option>
418              <option value="payment">Payment Methods</option>
419              <option value="invoices">Billing History</option>
420              {activeTab === 'change' && <option value="change">Change Plan</option>}
421            </select>
422          </div>
423          <div className="hidden sm:block">
424            <nav className="flex space-x-4" aria-label="Tabs">
425              <button
426                onClick={() => setActiveTab('subscription')}
427                className={`px-3 py-2 font-medium text-sm rounded-md ${
428                  activeTab === 'subscription'
429                    ? 'bg-blue-100 text-blue-700'
430                    : 'text-gray-500 hover:text-gray-700'
431                }`}
432              >
433                Subscription
434              </button>
435              <button
436                onClick={() => setActiveTab('payment')}
437                className={`px-3 py-2 font-medium text-sm rounded-md ${
438                  activeTab === 'payment'
439                    ? 'bg-blue-100 text-blue-700'
440                    : 'text-gray-500 hover:text-gray-700'
441                }`}
442              >
443                Payment Methods
444              </button>
445              <button
446                onClick={() => setActiveTab('invoices')}
447                className={`px-3 py-2 font-medium text-sm rounded-md ${
448                  activeTab === 'invoices'
449                    ? 'bg-blue-100 text-blue-700'
450                    : 'text-gray-500 hover:text-gray-700'
451                }`}
452              >
453                Billing History
454              </button>
455              {activeTab === 'change' && (
456                <button
457                  onClick={() => setActiveTab('change')}
458                  className="bg-blue-100 text-blue-700 px-3 py-2 font-medium text-sm rounded-md"
459                >
460                  Change Plan
461                </button>
462              )}
463            </nav>
464          </div>
465        </div>
466        
467        <div className="mt-6">
468          {renderTabContent()}
469        </div>
470      </div>
471    );
472  };
473  
474  export default BillingPage;