mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
public resync license
http license translations
This commit is contained in:
parent
920391f38a
commit
afa71c002b
@ -118,6 +118,11 @@ public class LicenseKeyChecker {
|
||||
synchronizeLicenseSettings();
|
||||
}
|
||||
|
||||
public void resyncLicense() {
|
||||
evaluateLicense();
|
||||
synchronizeLicenseSettings();
|
||||
}
|
||||
|
||||
public License getPremiumLicenseEnabledResult() {
|
||||
return premiumEnabledResult;
|
||||
}
|
||||
|
||||
@ -143,6 +143,66 @@ public class AdminLicenseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resync the current license with Keygen. This endpoint re-validates the existing license key
|
||||
* and updates the max users setting. Used after subscription upgrades to sync the new license
|
||||
* limits.
|
||||
*
|
||||
* @return Response with updated license information
|
||||
*/
|
||||
@PostMapping("/license/resync")
|
||||
@Operation(
|
||||
summary = "Resync license with Keygen",
|
||||
description =
|
||||
"Re-validates the existing license key with Keygen and updates local settings."
|
||||
+ " Used after subscription upgrades.")
|
||||
public ResponseEntity<Map<String, Object>> resyncLicense() {
|
||||
try {
|
||||
if (licenseKeyChecker == null) {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("success", false, "error", "License checker not available"));
|
||||
}
|
||||
|
||||
String currentKey = applicationProperties.getPremium().getKey();
|
||||
if (currentKey == null || currentKey.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "error", "No license key configured"));
|
||||
}
|
||||
|
||||
log.info("Resyncing license with Keygen");
|
||||
|
||||
// Re-validate license and sync settings
|
||||
licenseKeyChecker.resyncLicense();
|
||||
|
||||
// Get updated license status
|
||||
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
|
||||
ApplicationProperties.Premium premium = applicationProperties.getPremium();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("licenseType", license.name());
|
||||
response.put("enabled", premium.isEnabled());
|
||||
response.put("maxUsers", premium.getMaxUsers());
|
||||
response.put("message", "License resynced successfully");
|
||||
|
||||
log.info(
|
||||
"License resynced: type={}, maxUsers={}",
|
||||
license.name(),
|
||||
premium.getMaxUsers());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to resync license", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(
|
||||
Map.of(
|
||||
"success",
|
||||
false,
|
||||
"error",
|
||||
"Failed to resync license: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current license key status, including license type, enabled status,
|
||||
* and max users.
|
||||
|
||||
@ -5156,7 +5156,32 @@
|
||||
"success": "Payment Successful!",
|
||||
"successMessage": "Your subscription has been activated successfully. You will receive a confirmation email shortly.",
|
||||
"autoClose": "This window will close automatically...",
|
||||
"error": "Payment Error"
|
||||
"error": "Payment Error",
|
||||
"upgradeSuccess": "Payment successful! Your subscription has been upgraded. The license has been updated on your server. You will receive a confirmation email shortly.",
|
||||
"paymentSuccess": "Payment successful! Retrieving your license key...",
|
||||
"licenseActivated": "License activated! Your license key has been saved. A confirmation email has been sent to your registered email address.",
|
||||
"licenseDelayed": "Payment successful! Your license is being generated. You will receive an email with your license key shortly. If you don't receive it within 10 minutes, please contact support.",
|
||||
"licensePollingError": "Payment successful but we couldn't retrieve your license key automatically. Please check your email or contact support with your payment confirmation.",
|
||||
"licenseRetrievalError": "Payment successful but license retrieval failed. You will receive your license key via email. Please contact support if you don't receive it within 10 minutes.",
|
||||
"syncError": "Payment successful but license sync failed. Your license will be updated shortly. Please contact support if issues persist.",
|
||||
"licenseSaveError": "Failed to save license key. Please contact support with your license key to complete activation.",
|
||||
"paymentCanceled": "Payment was canceled. No charges were made.",
|
||||
"syncingLicense": "Syncing your upgraded license...",
|
||||
"generatingLicense": "Generating your license key...",
|
||||
"upgradeComplete": "Upgrade Complete",
|
||||
"upgradeCompleteMessage": "Your subscription has been upgraded successfully. Your existing license key has been updated.",
|
||||
"stripeNotConfigured": "Stripe Not Configured",
|
||||
"stripeNotConfiguredMessage": "Stripe payment integration is not configured. Please contact your administrator.",
|
||||
"monthly": "Monthly",
|
||||
"yearly": "Yearly",
|
||||
"billingPeriod": "Billing Period",
|
||||
"enterpriseNote": "Seats can be adjusted in checkout (1-1000).",
|
||||
"installationId": "Installation ID",
|
||||
"licenseKey": "Your License Key",
|
||||
"licenseInstructions": "Enter this key in Settings → Admin Plan → License Key section",
|
||||
"canCloseWindow": "You can now close this window.",
|
||||
"licenseKeyProcessing": "License Key Processing",
|
||||
"licenseDelayedMessage": "Your license key is being generated. Please check your email shortly or contact support."
|
||||
},
|
||||
"firstLogin": {
|
||||
"title": "First Time Login",
|
||||
|
||||
@ -5,7 +5,7 @@ import { loadStripe } from '@stripe/stripe-js';
|
||||
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
|
||||
import licenseService, { PlanTierGroup } from '@app/services/licenseService';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey, resyncExistingLicense } from '@app/utils/licenseCheckoutUtils';
|
||||
|
||||
// Validate Stripe key (static validation, no dynamic imports)
|
||||
const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
@ -168,11 +168,11 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
|
||||
// Check if this is an upgrade (existing license key) or new plan
|
||||
if (currentLicenseKey) {
|
||||
// UPGRADE FLOW: Force license re-verification by saving existing key
|
||||
console.log('Upgrade detected - syncing existing license key');
|
||||
// UPGRADE FLOW: Resync existing license with Keygen
|
||||
console.log('Upgrade detected - resyncing existing license with Keygen');
|
||||
setPollingStatus('polling');
|
||||
|
||||
const activation = await activateLicenseKey(currentLicenseKey, {
|
||||
const activation = await resyncExistingLicense({
|
||||
isMounted: () => true, // Modal is open, no need to check
|
||||
onActivated: onLicenseActivated,
|
||||
});
|
||||
|
||||
@ -15,7 +15,6 @@ import { useAdminSettings } from '@app/hooks/useAdminSettings';
|
||||
import PendingBadge from '@app/components/shared/config/PendingBadge';
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||
import { ManageBillingButton } from '@app/components/shared/ManageBillingButton';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey } from '@app/utils/licenseCheckoutUtils';
|
||||
|
||||
interface PremiumSettingsData {
|
||||
key?: string;
|
||||
@ -62,107 +61,6 @@ const AdminPlanSection: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle return from hosted Stripe checkout
|
||||
const handleCheckoutReturn = async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paymentStatus = urlParams.get('payment_status');
|
||||
const sessionId = urlParams.get('session_id');
|
||||
|
||||
if (paymentStatus === 'success' && sessionId) {
|
||||
console.log('Payment successful via hosted checkout:', sessionId);
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
// Check if this is an upgrade or new subscription
|
||||
if (currentLicenseInfo?.licenseKey) {
|
||||
// UPGRADE: Sync existing license key
|
||||
console.log('Upgrade detected - syncing existing license');
|
||||
|
||||
const activation = await activateLicenseKey(currentLicenseInfo.licenseKey, {
|
||||
onActivated: fetchLicenseInfo,
|
||||
});
|
||||
|
||||
if (activation.success) {
|
||||
alert({
|
||||
message: t('payment.upgradeSuccess', 'Your subscription has been upgraded successfully!'),
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to sync license after upgrade:', activation.error);
|
||||
alert({
|
||||
message: t('payment.syncError', 'Payment successful but license sync failed. Please contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// NEW SUBSCRIPTION: Poll for license key
|
||||
console.log('New subscription - polling for license key');
|
||||
alert({
|
||||
message: t('payment.paymentSuccess', 'Payment successful! Retrieving your license key...'),
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
try {
|
||||
const installationId = await licenseService.getInstallationId();
|
||||
console.log('Polling for license key with installation ID:', installationId);
|
||||
|
||||
// Use shared polling utility
|
||||
const result = await pollLicenseKeyWithBackoff(installationId);
|
||||
|
||||
if (result.success && result.licenseKey) {
|
||||
// Activate the license key
|
||||
const activation = await activateLicenseKey(result.licenseKey, {
|
||||
onActivated: fetchLicenseInfo,
|
||||
});
|
||||
|
||||
if (activation.success) {
|
||||
console.log(`License key activated: ${activation.licenseType}`);
|
||||
alert({
|
||||
message: t('payment.licenseActivated', 'License key activated successfully!'),
|
||||
color: 'green',
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to save license key:', activation.error);
|
||||
alert({
|
||||
message: t('payment.licenseSaveError', 'Failed to save license key. Please contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} else if (result.timedOut) {
|
||||
console.warn('License key polling timed out');
|
||||
alert({
|
||||
message: t('payment.licenseDelayed', 'License key is being generated. Please check back shortly or contact support.'),
|
||||
color: 'yellow',
|
||||
});
|
||||
} else {
|
||||
console.error('License key polling failed:', result.error);
|
||||
alert({
|
||||
message: t('payment.licensePollingError', 'Failed to retrieve license key. Please check your email or contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to poll for license key:', error);
|
||||
alert({
|
||||
message: t('payment.licenseRetrievalError', 'Failed to retrieve license key. Please check your email or contact support.'),
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (paymentStatus === 'canceled') {
|
||||
console.log('Payment canceled by user');
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
alert({
|
||||
message: t('payment.paymentCanceled', 'Payment was canceled.'),
|
||||
color: 'yellow',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Check if Stripe is configured
|
||||
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
if (!stripeKey || error) {
|
||||
@ -170,9 +68,6 @@ const AdminPlanSection: React.FC = () => {
|
||||
}
|
||||
fetchLicenseInfo();
|
||||
|
||||
// Handle checkout return after license info is loaded
|
||||
handleCheckoutReturn();
|
||||
|
||||
// Fetch premium settings
|
||||
fetchPremiumSettings();
|
||||
}, [error, config]);
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePlans } from '@app/hooks/usePlans';
|
||||
import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService';
|
||||
import StripeCheckout from '@app/components/shared/StripeCheckout';
|
||||
import { userManagementService } from '@app/services/userManagementService';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { pollLicenseKeyWithBackoff, activateLicenseKey, resyncExistingLicense } from '@app/utils/licenseCheckoutUtils';
|
||||
|
||||
export interface CheckoutOptions {
|
||||
minimumSeats?: number; // Override calculated seats for enterprise
|
||||
@ -32,6 +35,7 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
||||
children,
|
||||
defaultCurrency = 'gbp'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedPlanGroup, setSelectedPlanGroup] = useState<PlanTierGroup | null>(null);
|
||||
@ -42,6 +46,117 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
||||
// Load plans with current currency
|
||||
const { plans, refetch: refetchPlans } = usePlans(currentCurrency);
|
||||
|
||||
// Handle return from hosted Stripe checkout
|
||||
useEffect(() => {
|
||||
const handleCheckoutReturn = async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const paymentStatus = urlParams.get('payment_status');
|
||||
const sessionId = urlParams.get('session_id');
|
||||
|
||||
if (paymentStatus === 'success' && sessionId) {
|
||||
console.log('Payment successful via hosted checkout:', sessionId);
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
// Fetch current license info to determine upgrade vs new
|
||||
let licenseInfo: LicenseInfo | null = null;
|
||||
try {
|
||||
licenseInfo = await licenseService.getLicenseInfo();
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch license info:', err);
|
||||
}
|
||||
|
||||
// Check if this is an upgrade or new subscription
|
||||
if (licenseInfo?.licenseKey) {
|
||||
// UPGRADE: Resync existing license with Keygen
|
||||
console.log('Upgrade detected - resyncing existing license');
|
||||
|
||||
const activation = await resyncExistingLicense();
|
||||
|
||||
if (activation.success) {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('payment.upgradeSuccess'),
|
||||
});
|
||||
refetchPlans(); // Refresh plans to show updated subscription
|
||||
} else {
|
||||
console.error('Failed to sync license after upgrade:', activation.error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('payment.syncError'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// NEW SUBSCRIPTION: Poll for license key
|
||||
console.log('New subscription - polling for license key');
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('payment.paymentSuccess'),
|
||||
});
|
||||
|
||||
try {
|
||||
const installationId = await licenseService.getInstallationId();
|
||||
console.log('Polling for license key with installation ID:', installationId);
|
||||
|
||||
// Use shared polling utility
|
||||
const result = await pollLicenseKeyWithBackoff(installationId);
|
||||
|
||||
if (result.success && result.licenseKey) {
|
||||
// Activate the license key
|
||||
const activation = await activateLicenseKey(result.licenseKey);
|
||||
|
||||
if (activation.success) {
|
||||
console.log(`License key activated: ${activation.licenseType}`);
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('payment.licenseActivated'),
|
||||
});
|
||||
refetchPlans(); // Refresh plans to show updated subscription
|
||||
} else {
|
||||
console.error('Failed to save license key:', activation.error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('payment.licenseSaveError'),
|
||||
});
|
||||
}
|
||||
} else if (result.timedOut) {
|
||||
console.warn('License key polling timed out');
|
||||
alert({
|
||||
alertType: 'warning',
|
||||
title: t('payment.licenseDelayed'),
|
||||
});
|
||||
} else {
|
||||
console.error('License key polling failed:', result.error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('payment.licensePollingError'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to poll for license key:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('payment.licenseRetrievalError'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (paymentStatus === 'canceled') {
|
||||
console.log('Payment canceled by user');
|
||||
|
||||
// Clear URL parameters
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
alert({
|
||||
alertType: 'warning',
|
||||
title: t('payment.paymentCanceled'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleCheckoutReturn();
|
||||
}, [t, refetchPlans]);
|
||||
|
||||
const openCheckout = useCallback(
|
||||
async (tier: 'server' | 'enterprise', options: CheckoutOptions = {}) => {
|
||||
try {
|
||||
|
||||
@ -470,6 +470,20 @@ const licenseService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resync the current license with Keygen
|
||||
* Re-validates the existing license key and updates local settings
|
||||
*/
|
||||
async resyncLicense(): Promise<LicenseSaveResponse> {
|
||||
try {
|
||||
const response = await apiClient.post('/api/v1/admin/license/resync');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error resyncing license:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -134,7 +134,7 @@ export interface LicenseActivationResult {
|
||||
|
||||
/**
|
||||
* Activate a license key by saving it to the backend and fetching updated info
|
||||
* Consolidates activation logic used by both embedded and hosted checkout
|
||||
* Used for NEW subscriptions where we have a new license key to save
|
||||
*/
|
||||
export async function activateLicenseKey(
|
||||
licenseKey: string,
|
||||
@ -197,3 +197,69 @@ export async function activateLicenseKey(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resync existing license with Keygen
|
||||
* Used for UPGRADES where we already have a license key configured
|
||||
* Calls the dedicated resync endpoint instead of re-saving the same key
|
||||
*/
|
||||
export async function resyncExistingLicense(
|
||||
options: {
|
||||
/** Check if component is still mounted */
|
||||
isMounted?: () => boolean;
|
||||
/** Callback when license is resynced with updated info */
|
||||
onActivated?: (licenseInfo: LicenseInfo) => void;
|
||||
} = {}
|
||||
): Promise<LicenseActivationResult> {
|
||||
const { isMounted = () => true, onActivated } = options;
|
||||
|
||||
try {
|
||||
console.log('Resyncing existing license with Keygen...');
|
||||
const resyncResponse = await licenseService.resyncLicense();
|
||||
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
if (resyncResponse.success) {
|
||||
console.log(`License resynced: ${resyncResponse.licenseType}`);
|
||||
|
||||
// Fetch updated license info
|
||||
try {
|
||||
const licenseInfo = await licenseService.getLicenseInfo();
|
||||
|
||||
if (!isMounted()) {
|
||||
return { success: false, error: 'Component unmounted' };
|
||||
}
|
||||
|
||||
onActivated?.(licenseInfo);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
licenseType: resyncResponse.licenseType,
|
||||
licenseInfo,
|
||||
};
|
||||
} catch (infoError) {
|
||||
console.error('Error fetching license info after resync:', infoError);
|
||||
// Still return success since resync succeeded
|
||||
return {
|
||||
success: true,
|
||||
licenseType: resyncResponse.licenseType,
|
||||
error: 'Failed to fetch updated license info',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to resync license:', resyncResponse.error);
|
||||
return {
|
||||
success: false,
|
||||
error: resyncResponse.error || 'Failed to resync license',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resyncing license:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Resync failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
export function isSecureContext(): boolean {
|
||||
// Allow localhost for development (works with both HTTP and HTTPS)
|
||||
if (typeof window !== 'undefined') {
|
||||
const hostname = window.location.hostname;
|
||||
// const hostname = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
// Localhost is considered secure for development
|
||||
|
||||
Loading…
Reference in New Issue
Block a user