This commit is contained in:
Connor Yoh
2025-11-16 16:05:08 +00:00
parent 41a974eb0b
commit 97b51cab02
4 changed files with 215 additions and 4 deletions

View File

@@ -16,6 +16,7 @@ interface StripeCheckoutProps {
email: string;
onSuccess?: (sessionId: string) => void;
onError?: (error: string) => void;
onLicenseActivated?: (licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => void;
}
type CheckoutState = {
@@ -32,6 +33,7 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
email,
onSuccess,
onError,
onLicenseActivated,
}) => {
const { t } = useTranslation();
const [state, setState] = useState<CheckoutState>({ status: 'idle' });
@@ -103,6 +105,27 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
if (response.status === 'ready' && response.license_key) {
setLicenseKey(response.license_key);
setPollingStatus('ready');
// Save license key to backend
try {
const saveResponse = await licenseService.saveLicenseKey(response.license_key);
if (saveResponse.success) {
console.log(`License key activated on backend: ${saveResponse.licenseType}`);
// Fetch and pass license info to parent
try {
const licenseInfo = await licenseService.getLicenseInfo();
onLicenseActivated?.(licenseInfo);
} catch (infoError) {
console.error('Error fetching license info:', infoError);
}
} else {
console.error('Failed to save license key to backend:', saveResponse.error);
}
} catch (error) {
console.error('Error saving license key to backend:', error);
}
return;
}

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react';
import { Divider, Loader, Alert, Select, Group, Text, Collapse, Button, TextInput, Stack, Paper } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { usePlans } from '@app/hooks/usePlans';
import { PlanTierGroup } from '@app/services/licenseService';
import licenseService, { PlanTierGroup } from '@app/services/licenseService';
import StripeCheckout from '@app/components/shared/StripeCheckout';
import AvailablePlansSection from './plan/AvailablePlansSection';
import ActivePlanSection from './plan/ActivePlanSection';
@@ -15,6 +15,7 @@ import RestartConfirmationModal from '@app/components/shared/config/RestartConfi
import { useRestartServer } from '@app/components/shared/config/useRestartServer';
import { useAdminSettings } from '@app/hooks/useAdminSettings';
import PendingBadge from '@app/components/shared/config/PendingBadge';
import { convertOperationConfig } from '@app/hooks/tools/convert/useConvertOperation';
interface PremiumSettingsData {
key?: string;
@@ -29,6 +30,8 @@ const AdminPlanSection: React.FC = () => {
const [currency, setCurrency] = useState<string>('gbp');
const [useStaticVersion, setUseStaticVersion] = useState(false);
const [currentLicenseInfo, setCurrentLicenseInfo] = useState<any>(null);
const [licenseInfoLoading, setLicenseInfoLoading] = useState(false);
const [licenseInfoError, setLicenseInfoError] = useState<string | null>(null);
const [showLicenseKey, setShowLicenseKey] = useState(false);
const [email, setEmail] = useState<string>('');
const { plans, currentSubscription, loading, error, refetch } = usePlans(currency);
@@ -51,6 +54,7 @@ const AdminPlanSection: React.FC = () => {
useEffect(() => {
const fetchLicenseInfo = async () => {
try {
console.log('Fetching user and license info for plan section');
const adminData = await userManagementService.getUsers();
// Determine plan name based on config flags
@@ -66,17 +70,32 @@ const AdminPlanSection: React.FC = () => {
maxUsers: adminData.maxAllowedUsers,
grandfathered: adminData.grandfatheredUserCount > 0,
});
// Also fetch license info from new backend endpoint
try {
setLicenseInfoLoading(true);
setLicenseInfoError(null);
const backendLicenseInfo = await licenseService.getLicenseInfo();
setCurrentLicenseInfo(backendLicenseInfo);
setLicenseInfoLoading(false);
} catch (licenseErr: any) {
console.error('Failed to fetch backend license info:', licenseErr);
setLicenseInfoLoading(false);
setLicenseInfoError(licenseErr?.response?.data?.error || licenseErr?.message || 'Unknown error');
// Don't overwrite existing info if backend call fails
}
} catch (err) {
console.error('Failed to fetch license info:', err);
}
};
// Check if Stripe is configured
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
if (!stripeKey || error) {
setUseStaticVersion(true);
fetchLicenseInfo();
}
fetchLicenseInfo();
// Fetch premium settings
fetchPremiumSettings();
@@ -150,6 +169,11 @@ const AdminPlanSection: React.FC = () => {
// Error is already displayed in the StripeCheckout component
}, []);
const handleLicenseActivated = useCallback((licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => {
console.log('License activated:', licenseInfo);
setCurrentLicenseInfo(licenseInfo);
}, []);
const handleCheckoutClose = useCallback(() => {
setCheckoutOpen(false);
setSelectedPlanGroup(null);
@@ -187,6 +211,37 @@ const AdminPlanSection: React.FC = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{/* License Information Display - Always visible */}
<Alert
color={licenseInfoLoading ? "gray" : licenseInfoError ? "red" : currentLicenseInfo?.enabled ? "green" : "blue"}
title={t('admin.plan.licenseInfo', 'License Information')}
>
{licenseInfoLoading ? (
<Group gap="xs">
<Loader size="sm" />
<Text size="sm">{t('admin.plan.loadingLicense', 'Loading license information...')}</Text>
</Group>
) : licenseInfoError ? (
<Text size="sm" c="red">{t('admin.plan.licenseError', 'Failed to load license info')}: {licenseInfoError}</Text>
) : currentLicenseInfo ? (
<Stack gap="xs">
<Text size="sm">
<strong>{t('admin.plan.licenseType', 'License Type')}:</strong> {currentLicenseInfo.licenseType}
</Text>
<Text size="sm">
<strong>{t('admin.plan.status', 'Status')}:</strong> {currentLicenseInfo.enabled ? t('admin.plan.active', 'Active') : t('admin.plan.inactive', 'Inactive')}
</Text>
{currentLicenseInfo.licenseType === 'ENTERPRISE' && currentLicenseInfo.maxUsers > 0 && (
<Text size="sm">
<strong>{t('admin.plan.maxUsers', 'Max Users')}:</strong> {currentLicenseInfo.maxUsers}
</Text>
)}
</Stack>
) : (
<Text size="sm">{t('admin.plan.noLicenseInfo', 'No license information available')}</Text>
)}
</Alert>
{/* Customer Information Section */}
<Paper withBorder p="md" radius="md">
<Stack gap="md">
@@ -240,6 +295,7 @@ const AdminPlanSection: React.FC = () => {
email={email}
onSuccess={handlePaymentSuccess}
onError={handlePaymentError}
onLicenseActivated={handleLicenseActivated}
/>
)}

View File

@@ -429,6 +429,35 @@ const licenseService = {
return data as LicenseKeyResponse;
},
/**
* Save license key to backend
*/
async saveLicenseKey(licenseKey: string): Promise<{success: boolean; licenseType?: string; message?: string; error?: string}> {
try {
const response = await apiClient.post('/api/v1/admin/license-key', {
licenseKey: licenseKey,
});
return response.data;
} catch (error) {
console.error('Error saving license key:', error);
throw error;
}
},
/**
* Get current license information from backend
*/
async getLicenseInfo(): Promise<{licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}> {
try {
const response = await apiClient.get('/api/v1/admin/license-info');
return response.data;
} catch (error) {
console.error('Error fetching license info:', error);
throw error;
}
},
};
export default licenseService;