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:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
91
src/hooks/use-stripe-config.js
Normal file
91
src/hooks/use-stripe-config.js
Normal 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user