Files
Stirling-PDF/frontend/src/proprietary/utils/licenseCheckoutUtils.ts
ConnorYoh 8d9e70c796 Stripe and license payment integration (#4935)
selfhosted stripe payment and license integration

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-20 12:07:37 +00:00

266 lines
7.6 KiB
TypeScript

/**
* Shared utilities for license checkout completion
* Used by both embedded and hosted checkout flows
*/
import licenseService, { LicenseInfo } from '@app/services/licenseService';
/**
* Result of license key polling
*/
export interface LicenseKeyPollResult {
success: boolean;
licenseKey?: string;
error?: string;
timedOut?: boolean;
}
/**
* Configuration for license key polling
*/
export interface PollConfig {
/** Check if component is still mounted (prevents state updates after unmount) */
isMounted?: () => boolean;
/** Callback for status changes during polling */
onStatusChange?: (status: 'polling' | 'ready' | 'timeout') => void;
/** Custom backoff intervals in milliseconds (default: [1000, 2000, 4000, 8000, 16000]) */
backoffMs?: number[];
}
/**
* Poll for license key with exponential backoff
* Consolidates polling logic used by both embedded and hosted checkout
*/
export async function pollLicenseKeyWithBackoff(
installationId: string,
config: PollConfig = {}
): Promise<LicenseKeyPollResult> {
const {
isMounted = () => true,
onStatusChange,
backoffMs = [1000, 2000, 4000, 8000, 16000],
} = config;
let attemptIndex = 0;
onStatusChange?.('polling');
console.log(`Starting license key polling for installation: ${installationId}`);
const poll = async (): Promise<LicenseKeyPollResult> => {
// Check if component is still mounted
if (!isMounted()) {
console.log('Polling cancelled: component unmounted');
return { success: false, error: 'Component unmounted' };
}
const attemptNumber = attemptIndex + 1;
console.log(`Polling attempt ${attemptNumber}/${backoffMs.length}`);
try {
const response = await licenseService.checkLicenseKey(installationId);
// Check mounted after async operation
if (!isMounted()) {
return { success: false, error: 'Component unmounted' };
}
if (response.status === 'ready' && response.license_key) {
console.log('✅ License key ready!');
onStatusChange?.('ready');
return {
success: true,
licenseKey: response.license_key,
};
}
// License not ready yet, continue polling
attemptIndex++;
if (attemptIndex >= backoffMs.length) {
console.warn('⏱️ License polling timeout after all attempts');
onStatusChange?.('timeout');
return {
success: false,
timedOut: true,
error: 'Polling timeout - license key not ready',
};
}
// Wait before next attempt
const nextDelay = backoffMs[attemptIndex];
console.log(`Retrying in ${nextDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, nextDelay));
return poll();
} catch (error) {
console.error(`Polling attempt ${attemptNumber} failed:`, error);
if (!isMounted()) {
return { success: false, error: 'Component unmounted' };
}
attemptIndex++;
if (attemptIndex >= backoffMs.length) {
console.error('Polling failed after all attempts');
onStatusChange?.('timeout');
return {
success: false,
error: error instanceof Error ? error.message : 'Polling failed',
};
}
// Retry with exponential backoff even on error
const nextDelay = backoffMs[attemptIndex];
console.log(`Retrying after error in ${nextDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, nextDelay));
return poll();
}
};
return poll();
}
/**
* Result of license key activation
*/
export interface LicenseActivationResult {
success: boolean;
licenseType?: string;
licenseInfo?: LicenseInfo;
error?: string;
}
/**
* Activate a license key by saving it to the backend and fetching updated info
* Used for NEW subscriptions where we have a new license key to save
*/
export async function activateLicenseKey(
licenseKey: string,
options: {
/** Check if component is still mounted */
isMounted?: () => boolean;
/** Callback when license is activated with updated info */
onActivated?: (licenseInfo: LicenseInfo) => void;
} = {}
): Promise<LicenseActivationResult> {
const { isMounted = () => true, onActivated } = options;
try {
console.log('Activating license key...');
const saveResponse = await licenseService.saveLicenseKey(licenseKey);
if (!isMounted()) {
return { success: false, error: 'Component unmounted' };
}
if (saveResponse.success) {
console.log(`License key activated: ${saveResponse.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: saveResponse.licenseType,
licenseInfo,
};
} catch (infoError) {
console.error('Error fetching license info after activation:', infoError);
// Still return success since save succeeded
return {
success: true,
licenseType: saveResponse.licenseType,
error: 'Failed to fetch updated license info',
};
}
} else {
console.error('Failed to save license key:', saveResponse.error);
return {
success: false,
error: saveResponse.error || 'Failed to save license key',
};
}
} catch (error) {
console.error('Error activating license key:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Activation failed',
};
}
}
/**
* 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',
};
}
}