updates
This commit is contained in:
937
Frontend/src/pages/admin/SettingsPage.tsx
Normal file
937
Frontend/src/pages/admin/SettingsPage.tsx
Normal file
@@ -0,0 +1,937 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Settings,
|
||||
Shield,
|
||||
DollarSign,
|
||||
CreditCard,
|
||||
Save,
|
||||
Info,
|
||||
Globe,
|
||||
SlidersHorizontal,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Lock,
|
||||
Key,
|
||||
Cookie,
|
||||
Coins,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import adminPrivacyService, {
|
||||
CookieIntegrationSettings,
|
||||
CookieIntegrationSettingsResponse,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
} from '../../services/api/adminPrivacyService';
|
||||
import systemSettingsService, {
|
||||
PlatformCurrencyResponse,
|
||||
StripeSettingsResponse,
|
||||
UpdateStripeSettingsRequest,
|
||||
} from '../../services/api/systemSettingsService';
|
||||
import { useCurrency } from '../../contexts/CurrencyContext';
|
||||
import { Loading } from '../../components/common';
|
||||
import { getCurrencySymbol } from '../../utils/format';
|
||||
|
||||
type SettingsTab = 'general' | 'cookie' | 'currency' | 'payment';
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
const { currency, supportedCurrencies, refreshCurrency } = useCurrency();
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
|
||||
|
||||
// Cookie Settings State
|
||||
const [policy, setPolicy] = useState<CookiePolicySettings>({
|
||||
analytics_enabled: true,
|
||||
marketing_enabled: true,
|
||||
preferences_enabled: true,
|
||||
});
|
||||
const [integrations, setIntegrations] = useState<CookieIntegrationSettings>({
|
||||
ga_measurement_id: '',
|
||||
fb_pixel_id: '',
|
||||
});
|
||||
const [policyMeta, setPolicyMeta] = useState<
|
||||
Pick<CookiePolicySettingsResponse, 'updated_at' | 'updated_by'> | null
|
||||
>(null);
|
||||
const [integrationMeta, setIntegrationMeta] = useState<
|
||||
Pick<CookieIntegrationSettingsResponse, 'updated_at' | 'updated_by'> | null
|
||||
>(null);
|
||||
|
||||
// Currency Settings State
|
||||
const [selectedCurrency, setSelectedCurrency] = useState<string>(currency);
|
||||
const [currencyInfo, setCurrencyInfo] = useState<PlatformCurrencyResponse['data'] | null>(null);
|
||||
|
||||
// Stripe Settings State
|
||||
const [stripeSettings, setStripeSettings] = useState<StripeSettingsResponse['data'] | null>(null);
|
||||
const [formData, setFormData] = useState<UpdateStripeSettingsRequest>({
|
||||
stripe_secret_key: '',
|
||||
stripe_publishable_key: '',
|
||||
stripe_webhook_secret: '',
|
||||
});
|
||||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const currencyNames: Record<string, string> = {
|
||||
VND: 'Vietnamese Dong',
|
||||
USD: 'US Dollar',
|
||||
EUR: 'Euro',
|
||||
GBP: 'British Pound',
|
||||
JPY: 'Japanese Yen',
|
||||
CNY: 'Chinese Yuan',
|
||||
KRW: 'South Korean Won',
|
||||
SGD: 'Singapore Dollar',
|
||||
THB: 'Thai Baht',
|
||||
AUD: 'Australian Dollar',
|
||||
CAD: 'Canadian Dollar',
|
||||
};
|
||||
|
||||
const getCurrencyDisplayName = (code: string): string => {
|
||||
const name = currencyNames[code] || code;
|
||||
const symbol = getCurrencySymbol(code);
|
||||
return `${name} (${symbol})`;
|
||||
};
|
||||
|
||||
// Load all settings
|
||||
useEffect(() => {
|
||||
loadAllSettings();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCurrency(currency);
|
||||
}, [currency]);
|
||||
|
||||
const loadAllSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [policyRes, integrationRes, currencyRes, stripeRes] = await Promise.all([
|
||||
adminPrivacyService.getCookiePolicy(),
|
||||
adminPrivacyService.getIntegrations(),
|
||||
systemSettingsService.getPlatformCurrency(),
|
||||
systemSettingsService.getStripeSettings(),
|
||||
]);
|
||||
|
||||
setPolicy(policyRes.data);
|
||||
setPolicyMeta({
|
||||
updated_at: policyRes.updated_at,
|
||||
updated_by: policyRes.updated_by,
|
||||
});
|
||||
setIntegrations(integrationRes.data || {});
|
||||
setIntegrationMeta({
|
||||
updated_at: integrationRes.updated_at,
|
||||
updated_by: integrationRes.updated_by,
|
||||
});
|
||||
|
||||
setCurrencyInfo(currencyRes.data);
|
||||
setSelectedCurrency(currencyRes.data.currency);
|
||||
|
||||
setStripeSettings(stripeRes.data);
|
||||
setFormData({
|
||||
stripe_secret_key: '',
|
||||
stripe_publishable_key: stripeRes.data.stripe_publishable_key || '',
|
||||
stripe_webhook_secret: '',
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to load settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Cookie Settings Handlers
|
||||
const handleToggle = (key: keyof CookiePolicySettings) => {
|
||||
setPolicy((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSaveCookie = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const [policyRes, integrationRes] = await Promise.all([
|
||||
adminPrivacyService.updateCookiePolicy(policy),
|
||||
adminPrivacyService.updateIntegrations(integrations),
|
||||
]);
|
||||
setPolicy(policyRes.data);
|
||||
setPolicyMeta({
|
||||
updated_at: policyRes.updated_at,
|
||||
updated_by: policyRes.updated_by,
|
||||
});
|
||||
setIntegrations(integrationRes.data || {});
|
||||
setIntegrationMeta({
|
||||
updated_at: integrationRes.updated_at,
|
||||
updated_by: integrationRes.updated_by,
|
||||
});
|
||||
toast.success('Cookie policy and integrations updated successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to update cookie settings');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Currency Settings Handlers
|
||||
const handleSaveCurrency = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
await systemSettingsService.updatePlatformCurrency(selectedCurrency);
|
||||
await refreshCurrency();
|
||||
await loadAllSettings();
|
||||
toast.success('Platform currency updated successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to update platform currency');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Stripe Settings Handlers
|
||||
const handleSaveStripe = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const updateData: UpdateStripeSettingsRequest = {};
|
||||
|
||||
if (formData.stripe_secret_key && formData.stripe_secret_key.trim()) {
|
||||
updateData.stripe_secret_key = formData.stripe_secret_key.trim();
|
||||
}
|
||||
|
||||
if (formData.stripe_publishable_key && formData.stripe_publishable_key.trim()) {
|
||||
updateData.stripe_publishable_key = formData.stripe_publishable_key.trim();
|
||||
}
|
||||
|
||||
if (formData.stripe_webhook_secret && formData.stripe_webhook_secret.trim()) {
|
||||
updateData.stripe_webhook_secret = formData.stripe_webhook_secret.trim();
|
||||
}
|
||||
|
||||
await systemSettingsService.updateStripeSettings(updateData);
|
||||
await loadAllSettings();
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
stripe_secret_key: '',
|
||||
stripe_webhook_secret: '',
|
||||
});
|
||||
|
||||
toast.success('Stripe settings updated successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
'Failed to update Stripe settings'
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loading fullScreen={false} text="Loading settings..." />;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general' as SettingsTab, label: 'Overview', icon: Settings },
|
||||
{ id: 'cookie' as SettingsTab, label: 'Privacy & Cookies', icon: Cookie },
|
||||
{ id: 'currency' as SettingsTab, label: 'Currency', icon: Coins },
|
||||
{ id: 'payment' as SettingsTab, label: 'Payment', icon: CreditCard },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-10 animate-fade-in">
|
||||
{/* Luxury Header */}
|
||||
<div className="relative">
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-amber-400/5 via-transparent to-amber-600/5 rounded-3xl blur-3xl"></div>
|
||||
<div className="relative bg-white/80 backdrop-blur-xl rounded-3xl shadow-2xl border border-amber-200/30 p-8 md:p-10">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-8">
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-amber-400 to-amber-600 rounded-2xl blur-lg opacity-50"></div>
|
||||
<div className="relative p-4 rounded-2xl bg-gradient-to-br from-amber-500 via-amber-500 to-amber-600 shadow-xl border border-amber-400/50">
|
||||
<Settings className="w-8 h-8 text-white" />
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-gradient-to-br from-yellow-300 to-amber-500 rounded-full shadow-lg animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold bg-gradient-to-r from-slate-900 via-amber-700 to-slate-900 bg-clip-text text-transparent">
|
||||
Settings Dashboard
|
||||
</h1>
|
||||
<Sparkles className="w-6 h-6 text-amber-500 animate-pulse" />
|
||||
</div>
|
||||
<p className="text-gray-600 text-base md:text-lg max-w-2xl leading-relaxed">
|
||||
Centralized control center for all platform configurations and system settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Premium Tab Navigation */}
|
||||
<div className="mt-10 pt-8 border-t border-gradient-to-r from-transparent via-amber-200/30 to-transparent">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`
|
||||
group relative flex items-center gap-3 px-6 py-3.5 rounded-xl font-semibold text-sm
|
||||
transition-all duration-300 overflow-hidden
|
||||
${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white shadow-xl shadow-amber-500/40 scale-105'
|
||||
: 'bg-white/80 text-gray-700 border border-gray-200/60 hover:border-amber-300/60 hover:bg-gradient-to-r hover:from-amber-50/50 hover:to-amber-50/30 hover:shadow-lg hover:scale-102'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer"></div>
|
||||
)}
|
||||
<Icon className={`w-5 h-5 transition-transform duration-300 ${isActive ? 'text-white' : 'text-gray-600 group-hover:text-amber-600 group-hover:scale-110'}`} />
|
||||
<span className="relative z-10">{tab.label}</span>
|
||||
{isActive && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-yellow-300 via-amber-400 to-yellow-300"></div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* General Overview Tab */}
|
||||
{activeTab === 'general' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
<div
|
||||
onClick={() => setActiveTab('cookie')}
|
||||
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-blue-300/60 overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-blue-400/10 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-400 to-blue-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
||||
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg border border-blue-400/50 group-hover:scale-110 transition-transform">
|
||||
<Shield className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900 text-xl mb-1">Privacy & Cookies</h3>
|
||||
<div className="h-1 w-12 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
Manage cookie preferences, analytics integrations, and privacy controls
|
||||
</p>
|
||||
<div className="pt-5 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500 font-medium">Last updated</span>
|
||||
<span className="font-semibold text-gray-800 bg-gray-50 px-3 py-1 rounded-lg">
|
||||
{policyMeta?.updated_at
|
||||
? new Date(policyMeta.updated_at).toLocaleDateString()
|
||||
: 'Never'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-blue-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => setActiveTab('currency')}
|
||||
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-emerald-300/60 overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-emerald-400/10 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
||||
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-lg border border-emerald-400/50 group-hover:scale-110 transition-transform">
|
||||
<DollarSign className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900 text-xl mb-1">Currency</h3>
|
||||
<div className="h-1 w-12 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
Configure platform-wide currency settings and display preferences
|
||||
</p>
|
||||
<div className="pt-5 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500 font-medium">Current currency</span>
|
||||
<span className="font-bold text-emerald-600 bg-emerald-50 px-3 py-1 rounded-lg text-lg">
|
||||
{currency}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => setActiveTab('payment')}
|
||||
className="group relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-indigo-100/50 p-8 cursor-pointer transition-all duration-300 hover:shadow-2xl hover:scale-105 hover:border-indigo-300/60 overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-indigo-400/10 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-indigo-400 to-indigo-600 rounded-xl blur-md opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
||||
<div className="relative p-3.5 rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 shadow-lg border border-indigo-400/50 group-hover:scale-110 transition-transform">
|
||||
<CreditCard className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900 text-xl mb-1">Payment Gateway</h3>
|
||||
<div className="h-1 w-12 bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
Manage Stripe payment processing credentials and webhook settings
|
||||
</p>
|
||||
<div className="pt-5 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500 font-medium">Status</span>
|
||||
<span className={`font-bold px-3 py-1 rounded-lg ${
|
||||
stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key
|
||||
? 'text-emerald-600 bg-emerald-50'
|
||||
: 'text-gray-400 bg-gray-50'
|
||||
}`}>
|
||||
{stripeSettings?.has_secret_key && stripeSettings?.has_publishable_key ? '✓ Configured' : 'Not configured'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-indigo-50/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cookie & Privacy Tab */}
|
||||
{activeTab === 'cookie' && (
|
||||
<div className="space-y-8">
|
||||
{/* Section Header */}
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
|
||||
<Shield className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Privacy & Cookie Controls</h2>
|
||||
</div>
|
||||
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
||||
Define which cookie categories are allowed in the application. Control user consent preferences and analytics integrations.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveCookie}
|
||||
disabled={saving}
|
||||
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="relative bg-gradient-to-br from-blue-50/80 via-indigo-50/60 to-blue-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-blue-200/50 p-8 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-blue-400/20 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative flex gap-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="p-4 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 shadow-lg border border-blue-400/50">
|
||||
<Info className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 flex-1">
|
||||
<p className="font-bold text-gray-900 text-lg">
|
||||
How these settings affect the guest experience
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Disabling a category here prevents it from being offered to guests as part of the cookie consent flow. For example, if marketing cookies are disabled, the website should not load marketing pixels even if a guest previously opted in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cookie Toggles */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ key: 'analytics_enabled' as keyof CookiePolicySettings, label: 'Analytics Cookies', desc: 'Anonymous traffic and performance measurement', color: 'emerald', icon: SlidersHorizontal },
|
||||
{ key: 'marketing_enabled' as keyof CookiePolicySettings, label: 'Marketing Cookies', desc: 'Personalised offers and remarketing campaigns', color: 'pink', icon: SlidersHorizontal },
|
||||
{ key: 'preferences_enabled' as keyof CookiePolicySettings, label: 'Preference Cookies', desc: 'Remember guest choices like language and currency', color: 'indigo', icon: SlidersHorizontal },
|
||||
].map(({ key, label, desc, color, icon: Icon }) => {
|
||||
const isEnabled = policy[key];
|
||||
const colorClasses = {
|
||||
emerald: {
|
||||
bg: 'from-emerald-500 to-emerald-600',
|
||||
shadow: 'shadow-emerald-500/30',
|
||||
iconBg: 'bg-emerald-50 border-emerald-100',
|
||||
iconColor: 'text-emerald-600',
|
||||
},
|
||||
pink: {
|
||||
bg: 'from-pink-500 to-pink-600',
|
||||
shadow: 'shadow-pink-500/30',
|
||||
iconBg: 'bg-pink-50 border-pink-100',
|
||||
iconColor: 'text-pink-600',
|
||||
},
|
||||
indigo: {
|
||||
bg: 'from-indigo-500 to-indigo-600',
|
||||
shadow: 'shadow-indigo-500/30',
|
||||
iconBg: 'bg-indigo-50 border-indigo-100',
|
||||
iconColor: 'text-indigo-600',
|
||||
},
|
||||
}[color];
|
||||
|
||||
return (
|
||||
<div key={key} className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-7 hover:shadow-2xl transition-all duration-300">
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2.5 rounded-lg ${colorClasses.iconBg} border`}>
|
||||
<Icon className={`w-5 h-5 ${colorClasses.iconColor}`} />
|
||||
</div>
|
||||
<p className="font-bold text-gray-900 text-lg">{label}</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
{desc}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggle(key)}
|
||||
className={`relative inline-flex h-10 w-18 items-center rounded-full transition-all duration-300 shadow-lg ${
|
||||
isEnabled
|
||||
? `bg-gradient-to-r ${colorClasses.bg} ${colorClasses.shadow}`
|
||||
: 'bg-gray-300 shadow-gray-300/20'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-8 w-8 transform rounded-full bg-white shadow-xl transition-all duration-300 ${
|
||||
isEnabled ? 'translate-x-9' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Integration IDs */}
|
||||
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
||||
<div className="p-3 rounded-xl bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
|
||||
<Globe className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<p className="font-bold text-gray-900 text-xl">Third-Party Integrations</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
Configure IDs for supported analytics and marketing platforms. The application will only load these when both the policy and user consent allow it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
||||
Google Analytics 4 Measurement ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={integrations.ga_measurement_id || ''}
|
||||
onChange={(e) =>
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
ga_measurement_id: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="G-XXXXXXXXXX"
|
||||
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
Example: <code className="px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono text-xs">G-ABCDE12345</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
||||
Meta (Facebook) Pixel ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={integrations.fb_pixel_id || ''}
|
||||
onChange={(e) =>
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
fb_pixel_id: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="123456789012345"
|
||||
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
Numeric ID from your Meta Pixel configuration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Currency Tab */}
|
||||
{activeTab === 'currency' && (
|
||||
<div className="space-y-8">
|
||||
{/* Section Header */}
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-gradient-to-br from-emerald-500/10 to-green-500/10 border border-emerald-200/40">
|
||||
<DollarSign className="w-6 h-6 text-emerald-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Platform Currency Settings</h2>
|
||||
</div>
|
||||
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
||||
Set the default currency that will be displayed across all dashboards and pages throughout the platform
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveCurrency}
|
||||
disabled={saving || selectedCurrency === currency}
|
||||
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="relative bg-gradient-to-br from-emerald-50/80 via-green-50/60 to-emerald-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-emerald-200/50 p-8 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-emerald-400/20 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative flex gap-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="p-4 rounded-xl bg-gradient-to-br from-emerald-500 to-green-600 shadow-lg border border-emerald-400/50">
|
||||
<Info className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 flex-1">
|
||||
<p className="font-bold text-gray-900 text-lg">
|
||||
How platform currency works
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
The platform currency you select here will be used to display all prices, amounts, and financial information across the entire application. This includes customer-facing pages, admin dashboards, reports, and booking pages. All users will see prices in the selected currency.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Currency Selection */}
|
||||
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
||||
<div className="p-3 rounded-xl bg-gradient-to-br from-emerald-500/10 to-green-500/10 border border-emerald-200/40">
|
||||
<Globe className="w-6 h-6 text-emerald-600" />
|
||||
</div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<p className="font-bold text-gray-900 text-xl">Select Platform Currency</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
Choose the currency that will be used throughout the platform for displaying all monetary values
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-bold text-gray-900 tracking-wide">
|
||||
Currency
|
||||
</label>
|
||||
<select
|
||||
value={selectedCurrency}
|
||||
onChange={(e) => setSelectedCurrency(e.target.value)}
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-medium"
|
||||
>
|
||||
{supportedCurrencies.map((curr) => (
|
||||
<option key={curr} value={curr}>
|
||||
{curr} - {getCurrencyDisplayName(curr)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-500">Current platform currency:</span>
|
||||
<span className="font-bold text-emerald-600 bg-emerald-50 px-3 py-1 rounded-lg">{currency}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Tab */}
|
||||
{activeTab === 'payment' && (
|
||||
<div className="space-y-8">
|
||||
{/* Section Header */}
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 border border-indigo-200/40">
|
||||
<CreditCard className="w-6 h-6 text-indigo-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Stripe Payment Settings</h2>
|
||||
</div>
|
||||
<p className="text-gray-600 text-base max-w-2xl leading-relaxed">
|
||||
Configure your Stripe account credentials to enable secure card payments. All payments will be processed through your Stripe account.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveStripe}
|
||||
disabled={saving}
|
||||
className="group relative px-8 py-4 bg-gradient-to-r from-amber-500 via-amber-500 to-amber-600 text-white font-semibold rounded-xl shadow-xl shadow-amber-500/30 hover:shadow-2xl hover:shadow-amber-500/40 transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000"></div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<Save className={`w-5 h-5 ${saving ? 'animate-spin' : ''}`} />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="relative bg-gradient-to-br from-indigo-50/80 via-purple-50/60 to-indigo-50/80 backdrop-blur-xl rounded-2xl shadow-xl border border-indigo-200/50 p-8 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-indigo-400/20 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative flex gap-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="p-4 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg border border-indigo-400/50">
|
||||
<Info className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 flex-1">
|
||||
<p className="font-bold text-gray-900 text-lg">
|
||||
How Stripe payments work
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
Stripe handles all card payments securely. You need to provide your Stripe API keys from your Stripe Dashboard. The secret key is used to process payments on the backend, while the publishable key is used in the frontend payment forms. The webhook secret is required to verify webhook events from Stripe.
|
||||
</p>
|
||||
<div className="pt-3 border-t border-indigo-200/50">
|
||||
<p className="text-sm text-gray-600">
|
||||
<strong className="text-gray-900">Note:</strong> Leave fields empty to keep existing values. Only enter new values when you want to update them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stripe Settings Form */}
|
||||
<div className="relative bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-start gap-4 pb-6 border-b border-gray-200/60">
|
||||
<div className="p-3 rounded-xl bg-gradient-to-br from-purple-500/10 to-pink-500/10 border border-purple-200/40">
|
||||
<Key className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div className="space-y-2 flex-1">
|
||||
<p className="font-bold text-gray-900 text-xl">Stripe API Keys</p>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
Get these keys from your{' '}
|
||||
<a
|
||||
href="https://dashboard.stripe.com/test/apikeys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 hover:text-indigo-700 underline font-medium"
|
||||
>
|
||||
Stripe Dashboard
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Secret Key */}
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
||||
<Lock className="w-4 h-4 text-gray-600" />
|
||||
Stripe Secret Key
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
value={formData.stripe_secret_key}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, stripe_secret_key: e.target.value })
|
||||
}
|
||||
placeholder={
|
||||
stripeSettings?.has_secret_key
|
||||
? `Current: ${stripeSettings.stripe_secret_key_masked || '****'}`
|
||||
: 'sk_test_... or sk_live_...'
|
||||
}
|
||||
className="w-full px-4 py-3.5 pr-12 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors p-1"
|
||||
>
|
||||
{showSecretKey ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<p>Used to process payments on the backend. Must start with</p>
|
||||
<code className="px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono">sk_</code>
|
||||
{stripeSettings?.has_secret_key && (
|
||||
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
||||
Currently configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Publishable Key */}
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
||||
<Globe className="w-4 h-4 text-gray-600" />
|
||||
Stripe Publishable Key
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.stripe_publishable_key}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
stripe_publishable_key: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="pk_test_... or pk_live_..."
|
||||
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<p>Used in frontend payment forms. Must start with</p>
|
||||
<code className="px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono">pk_</code>
|
||||
{stripeSettings?.has_publishable_key && (
|
||||
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
||||
Currently configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook Secret */}
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
|
||||
<Lock className="w-4 h-4 text-gray-600" />
|
||||
Stripe Webhook Secret
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showWebhookSecret ? 'text' : 'password'}
|
||||
value={formData.stripe_webhook_secret}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
stripe_webhook_secret: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={
|
||||
stripeSettings?.has_webhook_secret
|
||||
? `Current: ${stripeSettings.stripe_webhook_secret_masked || '****'}`
|
||||
: 'whsec_...'
|
||||
}
|
||||
className="w-full px-4 py-3.5 pr-12 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWebhookSecret(!showWebhookSecret)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors p-1"
|
||||
>
|
||||
{showWebhookSecret ? (
|
||||
<EyeOff className="w-5 h-5" />
|
||||
) : (
|
||||
<Eye className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<p>Used to verify webhook events from Stripe. Must start with</p>
|
||||
<code className="px-2 py-1 bg-gray-100 rounded text-gray-700 font-mono">whsec_</code>
|
||||
{stripeSettings?.has_webhook_secret && (
|
||||
<span className="ml-2 text-emerald-600 font-medium flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full"></span>
|
||||
Currently configured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook URL Info */}
|
||||
<div className="relative mt-8 p-6 bg-gradient-to-br from-yellow-50 to-amber-50/50 border border-yellow-200/60 rounded-xl overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-yellow-400/20 to-transparent rounded-bl-full"></div>
|
||||
<div className="relative space-y-3">
|
||||
<p className="text-sm font-bold text-yellow-900">Webhook Endpoint URL</p>
|
||||
<p className="text-sm text-yellow-800">
|
||||
Configure this URL in your{' '}
|
||||
<a
|
||||
href="https://dashboard.stripe.com/webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-medium hover:text-yellow-900"
|
||||
>
|
||||
Stripe Webhooks Dashboard
|
||||
</a>
|
||||
:
|
||||
</p>
|
||||
<code className="block text-xs bg-yellow-100/80 px-4 py-3 rounded-lg text-yellow-900 break-all font-mono border border-yellow-200/60">
|
||||
{window.location.origin}/api/payments/stripe/webhook
|
||||
</code>
|
||||
<p className="text-xs text-yellow-700 pt-2">
|
||||
Make sure to subscribe to <code className="px-1.5 py-0.5 bg-yellow-100 rounded font-mono">payment_intent.succeeded</code> and{' '}
|
||||
<code className="px-1.5 py-0.5 bg-yellow-100 rounded font-mono">payment_intent.payment_failed</code> events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
Reference in New Issue
Block a user