Update Stripe publishable key storage in Stripe Settings

1. Created src/hooks/use-stripe-config.js - New hook that:
	- Fetches publishable key from /api/config/stripe
	- Returns a pre-initialized stripePromise for use with Stripe Elements
	- Caches the result to avoid multiple API calls
	- Falls back to REACT_APP_STRIPE_PUBLISHABLE_KEY env var if API fails
2. Updated AdminSettings.js - Added publishable key input field in the Stripe settings form
3. Updated AdminPaymentMethodsPanel.js - Uses useStripeConfig hook instead of env variable
4. Updated PaymentMethodsSection.js - Uses useStripeConfig hook instead of env variable
This commit is contained in:
2026-02-02 17:55:00 +07:00
parent 82ef36b439
commit 68fc34d0a5
4 changed files with 162 additions and 12 deletions

View File

@@ -1,18 +1,15 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js'; import { Elements } from '@stripe/react-stripe-js';
import { Card } from './ui/card'; import { Card } from './ui/card';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { CreditCard, Plus, Loader2, AlertCircle } from 'lucide-react'; import { CreditCard, Plus, Loader2, AlertCircle } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import api from '../utils/api'; import api from '../utils/api';
import useStripeConfig from '../hooks/use-stripe-config';
import PaymentMethodCard from './PaymentMethodCard'; import PaymentMethodCard from './PaymentMethodCard';
import AddPaymentMethodDialog from './AddPaymentMethodDialog'; import AddPaymentMethodDialog from './AddPaymentMethodDialog';
import ConfirmationDialog from './ConfirmationDialog'; 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 * PaymentMethodsSection - Manages user payment methods
* *
@@ -28,6 +25,9 @@ const PaymentMethodsSection = () => {
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Get Stripe configuration from API
const { stripePromise, loading: stripeLoading, error: stripeError } = useStripeConfig();
// Dialog states // Dialog states
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const [clientSecret, setClientSecret] = useState(null); const [clientSecret, setClientSecret] = useState(null);

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js'; import { Elements } from '@stripe/react-stripe-js';
import { Card } from '../ui/card'; import { Card } from '../ui/card';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@@ -26,13 +25,11 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import api from '../../utils/api'; import api from '../../utils/api';
import useStripeConfig from '../../hooks/use-stripe-config';
import ConfirmationDialog from '../ConfirmationDialog'; import ConfirmationDialog from '../ConfirmationDialog';
import PasswordConfirmDialog from '../PasswordConfirmDialog'; import PasswordConfirmDialog from '../PasswordConfirmDialog';
import AddPaymentMethodDialog from '../AddPaymentMethodDialog'; 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 * Get icon for payment method type
*/ */
@@ -76,6 +73,9 @@ const AdminPaymentMethodsPanel = ({ userId, userName }) => {
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
// Get Stripe configuration from API
const { stripePromise, loading: stripeLoading, error: stripeError } = useStripeConfig();
// Dialog states // Dialog states
const [addCardDialogOpen, setAddCardDialogOpen] = useState(false); const [addCardDialogOpen, setAddCardDialogOpen] = useState(false);
const [addManualDialogOpen, setAddManualDialogOpen] = useState(false); const [addManualDialogOpen, setAddManualDialogOpen] = useState(false);

View File

@@ -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;

View File

@@ -16,11 +16,13 @@ export default function AdminSettings() {
// Form state // Form state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
publishable_key: '',
secret_key: '', secret_key: '',
webhook_secret: '' webhook_secret: ''
}); });
// Show/hide sensitive values // Show/hide sensitive values
const [showPublishableKey, setShowPublishableKey] = useState(false);
const [showSecretKey, setShowSecretKey] = useState(false); const [showSecretKey, setShowSecretKey] = useState(false);
const [showWebhookSecret, setShowWebhookSecret] = useState(false); const [showWebhookSecret, setShowWebhookSecret] = useState(false);
@@ -57,6 +59,7 @@ export default function AdminSettings() {
const handleEditClick = () => { const handleEditClick = () => {
setIsEditing(true); setIsEditing(true);
setFormData({ setFormData({
publishable_key: '',
secret_key: '', secret_key: '',
webhook_secret: '' webhook_secret: ''
}); });
@@ -65,17 +68,24 @@ export default function AdminSettings() {
const handleCancelEdit = () => { const handleCancelEdit = () => {
setIsEditing(false); setIsEditing(false);
setFormData({ setFormData({
publishable_key: '',
secret_key: '', secret_key: '',
webhook_secret: '' webhook_secret: ''
}); });
setShowPublishableKey(false);
setShowSecretKey(false); setShowSecretKey(false);
setShowWebhookSecret(false); setShowWebhookSecret(false);
}; };
const handleSave = async () => { const handleSave = async () => {
// Validate inputs // Validate inputs
if (!formData.secret_key || !formData.webhook_secret) { if (!formData.publishable_key || !formData.secret_key || !formData.webhook_secret) {
toast.error('Both Secret Key and Webhook Secret are required'); 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; return;
} }
@@ -89,15 +99,25 @@ export default function AdminSettings() {
return; 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); setSaving(true);
try { try {
await api.put('/admin/settings/stripe', formData); await api.put('/admin/settings/stripe', formData);
toast.success('Stripe settings updated successfully'); toast.success('Stripe settings updated successfully');
setIsEditing(false); setIsEditing(false);
setFormData({ setFormData({
publishable_key: '',
secret_key: '', secret_key: '',
webhook_secret: '' webhook_secret: ''
}); });
setShowPublishableKey(false);
setShowSecretKey(false); setShowSecretKey(false);
setShowWebhookSecret(false); setShowWebhookSecret(false);
// Refresh status // Refresh status
@@ -157,6 +177,31 @@ export default function AdminSettings() {
{isEditing ? ( {isEditing ? (
/* Edit Mode */ /* Edit Mode */
<div className="space-y-6"> <div className="space-y-6">
{/* Publishable Key Input */}
<div className="space-y-2">
<Label htmlFor="publishable_key">Stripe Publishable Key</Label>
<div className="relative">
<Input
id="publishable_key"
type={showPublishableKey ? 'text' : 'password'}
value={formData.publishable_key}
onChange={(e) => setFormData({ ...formData, publishable_key: e.target.value })}
placeholder="pk_test_... or pk_live_..."
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPublishableKey(!showPublishableKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{showPublishableKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-gray-500">
Get this from your Stripe Dashboard Developers API keys (Publishable key)
</p>
</div>
{/* Secret Key Input */} {/* Secret Key Input */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="secret_key">Stripe Secret Key</Label> <Label htmlFor="secret_key">Stripe Secret Key</Label>
@@ -178,7 +223,7 @@ export default function AdminSettings() {
</button> </button>
</div> </div>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Get this from your Stripe Dashboard Developers API keys Get this from your Stripe Dashboard Developers API keys (Secret key)
</p> </p>
</div> </div>
@@ -267,7 +312,7 @@ export default function AdminSettings() {
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div> <div>
<p className="font-semibold text-gray-900">Environment</p> <p className="font-semibold text-gray-900">Environment</p>
<p className="text-sm text-gray-600">Detected from secret key prefix</p> <p className="text-sm text-gray-600">Detected from key prefixes</p>
</div> </div>
<span className={`px-3 py-1 rounded-full text-sm font-semibold ${ <span className={`px-3 py-1 rounded-full text-sm font-semibold ${
stripeStatus.environment === 'live' stripeStatus.environment === 'live'
@@ -278,6 +323,20 @@ export default function AdminSettings() {
</span> </span>
</div> </div>
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<p className="font-semibold text-gray-900">Publishable Key</p>
<p className="text-sm text-gray-600 font-mono">
{stripeStatus.publishable_key_prefix}...
</p>
</div>
{stripeStatus.publishable_key_set ? (
<CheckCircle className="h-5 w-5 text-green-600" />
) : (
<AlertCircle className="h-5 w-5 text-amber-600" />
)}
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div> <div>
<p className="font-semibold text-gray-900">Secret Key</p> <p className="font-semibold text-gray-900">Secret Key</p>