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;