Desktop connection SaaS: config, billing, team support (#5768)

Co-authored-by: James Brunton <[email protected]>
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
ConnorYoh
2026-02-25 14:13:07 +00:00
committed by GitHub
co-authored by James Brunton James Brunton
parent 86072ec91a
commit 5c39acecd8
76 changed files with 6231 additions and 119 deletions
@@ -1,10 +1,11 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import apiClient from '@app/services/apiClient';
import { useTranslation } from 'react-i18next';
import { ConvertParameters, defaultParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { createFileFromApiResponse } from '@app/utils/fileResponseUtils';
import { useToolOperation, ToolType, CustomProcessorResult } from '@app/hooks/tools/shared/useToolOperation';
import { getEndpointUrl, isImageFormat, isWebFormat, isOfficeFormat } from '@app/utils/convertUtils';
import { getEndpointUrl, getEndpointName, isImageFormat, isWebFormat, isOfficeFormat } from '@app/utils/convertUtils';
import { useToolCloudStatus } from '@app/hooks/useToolCloudStatus';
// Static function that can be used by both the hook and automation executor
export const shouldProcessFilesSeparately = (
@@ -195,9 +196,21 @@ export const convertOperationConfig = {
defaultParameters,
} as const;
export const useConvertOperation = () => {
export const useConvertOperation = (parameters?: ConvertParameters) => {
const { t } = useTranslation();
// Calculate current endpoint name for cloud detection
const currentEndpointName = useMemo(() => {
if (!parameters?.fromExtension || !parameters?.toExtension) return undefined;
// Map PDF/X to use PDF/A endpoint (same as in convertProcessor)
const actualToExtension = parameters.toExtension === 'pdfx' ? 'pdfa' : parameters.toExtension;
return getEndpointName(parameters.fromExtension, actualToExtension);
}, [parameters?.fromExtension, parameters?.toExtension]);
// Check if current conversion will use cloud
const willUseCloud = useToolCloudStatus(currentEndpointName);
const customConvertProcessor = useCallback(async (
parameters: ConvertParameters,
selectedFiles: File[]
@@ -205,7 +218,7 @@ export const useConvertOperation = () => {
return convertProcessor(parameters, selectedFiles);
}, []);
return useToolOperation<ConvertParameters>({
const operation = useToolOperation<ConvertParameters>({
...convertOperationConfig,
customProcessor: customConvertProcessor, // Use instance-specific processor for translation support
getErrorMessage: (error) => {
@@ -218,4 +231,10 @@ export const useConvertOperation = () => {
return t("convert.errorConversion", "An error occurred while converting the file.");
},
});
// Override willUseCloud with our calculated value
return {
...operation,
willUseCloud,
};
};
@@ -15,6 +15,8 @@ import { createNewStirlingFileStub } from '@app/types/fileContext';
import { ToolOperation } from '@app/types/file';
import { ToolId } from '@app/types/toolId';
import { ensureBackendReady } from '@app/services/backendReadinessGuard';
import { useWillUseCloud } from '@app/hooks/useWillUseCloud';
import { useCreditCheck } from '@app/hooks/useCreditCheck';
// Re-export for backwards compatibility
export type { ProcessingProgress, ResponseHandler };
@@ -147,6 +149,7 @@ export interface ToolOperationHook<TParams = void> {
status: string;
errorMessage: string | null;
progress: ProcessingProgress | null;
willUseCloud?: boolean;
// Actions
executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise<void>;
@@ -183,6 +186,16 @@ export const useToolOperation = <TParams>(
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
const { generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles } = useToolResources();
const { checkCredits } = useCreditCheck(config.operationType);
// Determine endpoint for cloud usage check
const endpointString = config.toolType !== ToolType.custom && config.endpoint
? (typeof config.endpoint === 'function'
? (config.defaultParameters ? config.endpoint(config.defaultParameters) : undefined)
: config.endpoint)
: undefined;
const willUseCloud = useWillUseCloud(endpointString);
// Track last operation for undo functionality
const lastOperationRef = useRef<{
inputFiles: File[];
@@ -217,7 +230,24 @@ export const useToolOperation = <TParams>(
return;
}
const backendReady = await ensureBackendReady();
// Get endpoint (static or dynamic) for backend readiness check
const endpoint = config.customProcessor
? undefined // Custom processors may not have endpoints
: typeof config.endpoint === 'function'
? config.endpoint(params)
: config.endpoint;
// Credit check for cloud operations (desktop SaaS mode only, no-op in web builds)
if (willUseCloud && endpoint) {
const creditError = await checkCredits();
if (creditError !== null) {
actions.setError(creditError);
return;
}
}
// Backend readiness check (will skip for SaaS-routed endpoints)
const backendReady = await ensureBackendReady(endpoint);
if (!backendReady) {
actions.setError(t('backendHealth.offline', 'Embedded backend is offline. Please try again shortly.'));
return;
@@ -507,7 +537,7 @@ export const useToolOperation = <TParams>(
actions.setLoading(false);
actions.setProgress(null);
}
}, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles]);
}, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, willUseCloud, checkCredits]);
const cancelOperation = useCallback(() => {
cancelApiCalls();
@@ -591,6 +621,7 @@ export const useToolOperation = <TParams>(
status: state.status,
errorMessage: state.errorMessage,
progress: state.progress,
willUseCloud,
// Actions
executeOperation,
@@ -0,0 +1,21 @@
/**
* Comprehensive conversion status data
*/
export interface ConversionStatus {
availability: Record<string, boolean>; // Available on local OR SaaS?
cloudStatus: Record<string, boolean>; // Will use cloud?
localOnly: Record<string, boolean>; // Available ONLY locally (not on SaaS)?
}
/**
* Core stub for conversion cloud status checking
* Desktop layer provides the real implementation
* In web builds, always returns empty objects (no cloud routing)
*/
export function useConversionCloudStatus(): ConversionStatus {
return {
availability: {},
cloudStatus: {},
localOnly: {},
}; // Core stub - web builds don't use cloud
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Core stub for credit checking before cloud operations
* Desktop layer shadows this with the real implementation
* In web builds, always allows the operation (no credit system)
*/
export function useCreditCheck(_operationType?: string) {
return {
checkCredits: async (): Promise<string | null> => null, // null = allowed
};
}
@@ -0,0 +1,8 @@
/**
* Core stub for tool cloud status checking
* Desktop layer provides the real implementation
* In web builds, always returns false (no cloud routing)
*/
export function useToolCloudStatus(_endpointName?: string): boolean {
return false; // Core stub - web builds don't use cloud
}
@@ -0,0 +1,8 @@
/**
* Core stub for cloud usage detection
* Desktop layer provides the real implementation
* In web builds, always returns false since there's no cloud routing
*/
export function useWillUseCloud(_endpoint?: string): boolean {
return false; // Core stub - web builds don't use cloud
}