diff --git a/src/components/PaymentMethodsSection.js b/src/components/PaymentMethodsSection.js index 5b67c35..b2bed3f 100644 --- a/src/components/PaymentMethodsSection.js +++ b/src/components/PaymentMethodsSection.js @@ -1,18 +1,15 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { loadStripe } from '@stripe/stripe-js'; import { Elements } from '@stripe/react-stripe-js'; import { Card } from './ui/card'; import { Button } from './ui/button'; import { CreditCard, Plus, Loader2, AlertCircle } from 'lucide-react'; import { toast } from 'sonner'; import api from '../utils/api'; +import useStripeConfig from '../hooks/use-stripe-config'; import PaymentMethodCard from './PaymentMethodCard'; import AddPaymentMethodDialog from './AddPaymentMethodDialog'; import ConfirmationDialog from './ConfirmationDialog'; -// Initialize Stripe with publishable key from environment -const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); - /** * PaymentMethodsSection - Manages user payment methods * @@ -28,6 +25,9 @@ const PaymentMethodsSection = () => { const [actionLoading, setActionLoading] = useState(false); const [error, setError] = useState(null); + // Get Stripe configuration from API + const { stripePromise, loading: stripeLoading, error: stripeError } = useStripeConfig(); + // Dialog states const [addDialogOpen, setAddDialogOpen] = useState(false); const [clientSecret, setClientSecret] = useState(null); diff --git a/src/components/admin/AdminPaymentMethodsPanel.js b/src/components/admin/AdminPaymentMethodsPanel.js index 1376a7a..90a2550 100644 --- a/src/components/admin/AdminPaymentMethodsPanel.js +++ b/src/components/admin/AdminPaymentMethodsPanel.js @@ -1,5 +1,4 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { loadStripe } from '@stripe/stripe-js'; import { Elements } from '@stripe/react-stripe-js'; import { Card } from '../ui/card'; import { Button } from '../ui/button'; @@ -26,13 +25,11 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import api from '../../utils/api'; +import useStripeConfig from '../../hooks/use-stripe-config'; import ConfirmationDialog from '../ConfirmationDialog'; import PasswordConfirmDialog from '../PasswordConfirmDialog'; import AddPaymentMethodDialog from '../AddPaymentMethodDialog'; -// Initialize Stripe with publishable key from environment -const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); - /** * Get icon for payment method type */ @@ -76,6 +73,9 @@ const AdminPaymentMethodsPanel = ({ userId, userName }) => { const [actionLoading, setActionLoading] = useState(false); const [error, setError] = useState(null); + // Get Stripe configuration from API + const { stripePromise, loading: stripeLoading, error: stripeError } = useStripeConfig(); + // Dialog states const [addCardDialogOpen, setAddCardDialogOpen] = useState(false); const [addManualDialogOpen, setAddManualDialogOpen] = useState(false); diff --git a/src/hooks/use-stripe-config.js b/src/hooks/use-stripe-config.js new file mode 100644 index 0000000..a65daae --- /dev/null +++ b/src/hooks/use-stripe-config.js @@ -0,0 +1,91 @@ +import { useState, useEffect, useCallback } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import api from '../utils/api'; + +// Cache the stripe promise to avoid multiple loads +let stripePromiseCache = null; +let cachedPublishableKey = null; + +/** + * Hook to get Stripe configuration from the backend. + * + * Returns the Stripe publishable key and a pre-initialized Stripe promise. + * The publishable key is fetched from the backend API, allowing admins + * to configure it through the admin panel instead of environment variables. + */ +const useStripeConfig = () => { + const [publishableKey, setPublishableKey] = useState(cachedPublishableKey); + const [stripePromise, setStripePromise] = useState(stripePromiseCache); + const [loading, setLoading] = useState(!cachedPublishableKey); + const [error, setError] = useState(null); + const [environment, setEnvironment] = useState(null); + + const fetchConfig = useCallback(async () => { + // If we already have a cached key, use it + if (cachedPublishableKey && stripePromiseCache) { + setPublishableKey(cachedPublishableKey); + setStripePromise(stripePromiseCache); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await api.get('/config/stripe'); + const { publishable_key, environment: env } = response.data; + + // Cache the key and stripe promise + cachedPublishableKey = publishable_key; + stripePromiseCache = loadStripe(publishable_key); + + setPublishableKey(publishable_key); + setStripePromise(stripePromiseCache); + setEnvironment(env); + } catch (err) { + console.error('[useStripeConfig] Failed to fetch Stripe config:', err); + setError(err.response?.data?.detail || 'Failed to load Stripe configuration'); + + // Fallback to environment variable if available + const envKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY; + if (envKey) { + console.warn('[useStripeConfig] Falling back to environment variable'); + cachedPublishableKey = envKey; + stripePromiseCache = loadStripe(envKey); + setPublishableKey(envKey); + setStripePromise(stripePromiseCache); + setEnvironment(envKey.startsWith('pk_live_') ? 'live' : 'test'); + setError(null); + } + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchConfig(); + }, [fetchConfig]); + + // Function to clear cache (useful after admin updates settings) + const clearCache = useCallback(() => { + cachedPublishableKey = null; + stripePromiseCache = null; + setPublishableKey(null); + setStripePromise(null); + fetchConfig(); + }, [fetchConfig]); + + return { + publishableKey, + stripePromise, + loading, + error, + environment, + refetch: fetchConfig, + clearCache, + isConfigured: !!publishableKey, + }; +}; + +export default useStripeConfig; diff --git a/src/pages/admin/AdminSettings.js b/src/pages/admin/AdminSettings.js index b008368..85fedd8 100644 --- a/src/pages/admin/AdminSettings.js +++ b/src/pages/admin/AdminSettings.js @@ -16,11 +16,13 @@ export default function AdminSettings() { // Form state const [formData, setFormData] = useState({ + publishable_key: '', secret_key: '', webhook_secret: '' }); // Show/hide sensitive values + const [showPublishableKey, setShowPublishableKey] = useState(false); const [showSecretKey, setShowSecretKey] = useState(false); const [showWebhookSecret, setShowWebhookSecret] = useState(false); @@ -57,6 +59,7 @@ export default function AdminSettings() { const handleEditClick = () => { setIsEditing(true); setFormData({ + publishable_key: '', secret_key: '', webhook_secret: '' }); @@ -65,17 +68,24 @@ export default function AdminSettings() { const handleCancelEdit = () => { setIsEditing(false); setFormData({ + publishable_key: '', secret_key: '', webhook_secret: '' }); + setShowPublishableKey(false); setShowSecretKey(false); setShowWebhookSecret(false); }; const handleSave = async () => { // Validate inputs - if (!formData.secret_key || !formData.webhook_secret) { - toast.error('Both Secret Key and Webhook Secret are required'); + if (!formData.publishable_key || !formData.secret_key || !formData.webhook_secret) { + toast.error('All three keys are required: Publishable Key, Secret Key, and Webhook Secret'); + return; + } + + if (!formData.publishable_key.startsWith('pk_test_') && !formData.publishable_key.startsWith('pk_live_')) { + toast.error('Invalid Publishable Key format. Must start with pk_test_ or pk_live_'); return; } @@ -89,15 +99,25 @@ export default function AdminSettings() { return; } + // Check environment consistency + const pkIsLive = formData.publishable_key.startsWith('pk_live_'); + const skIsLive = formData.secret_key.startsWith('sk_live_'); + if (pkIsLive !== skIsLive) { + toast.error('Publishable Key and Secret Key must be from the same environment (both test or both live)'); + return; + } + setSaving(true); try { await api.put('/admin/settings/stripe', formData); toast.success('Stripe settings updated successfully'); setIsEditing(false); setFormData({ + publishable_key: '', secret_key: '', webhook_secret: '' }); + setShowPublishableKey(false); setShowSecretKey(false); setShowWebhookSecret(false); // Refresh status @@ -157,6 +177,31 @@ export default function AdminSettings() { {isEditing ? ( /* Edit Mode */
+ Get this from your Stripe Dashboard → Developers → API keys (Publishable key) +
+- Get this from your Stripe Dashboard → Developers → API keys + Get this from your Stripe Dashboard → Developers → API keys (Secret key)
Environment
-Detected from secret key prefix
+Detected from key prefixes
Publishable Key
++ {stripeStatus.publishable_key_prefix}... +
+Secret Key