mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-19 02:22:11 +01:00
Hotfix-cant-run-tools-when-no-credits (#5955)
Tested: * Can sign in on saas -> can run local tools with or without credits-> can run saas only tools (if credits) -> can't run saas only tools without credits * Can sign in self-hosted -> can run all tools on remote if available -> can run local when self-hosted unavailable Clouds show on saas tools when connected Tools are disabled when connected to self-hosted but cannot find server. You also get banner #cantwaitforplaywritetests
This commit is contained in:
@@ -15,7 +15,7 @@ import { ToolId } from "@app/types/toolId";
|
||||
import { getToolDisabledReason, getDisabledLabel } from "@app/components/tools/fullscreen/shared";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { CloudBadge } from "@app/components/shared/CloudBadge";
|
||||
import { useToolCloudStatus } from "@app/hooks/useToolCloudStatus";
|
||||
import { useWillUseCloud } from "@app/hooks/useWillUseCloud";
|
||||
|
||||
interface ToolButtonProps {
|
||||
id: ToolId;
|
||||
@@ -41,8 +41,9 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
const fav = isFavorite(id as ToolId);
|
||||
|
||||
// Check if this tool will route to SaaS backend (desktop only)
|
||||
const endpointName = tool.endpoints?.[0];
|
||||
const usesCloud = useToolCloudStatus(endpointName);
|
||||
const rawEndpoint = tool.operationConfig?.endpoint;
|
||||
const endpointString = typeof rawEndpoint === 'string' ? rawEndpoint : undefined;
|
||||
const usesCloud = useWillUseCloud(endpointString);
|
||||
|
||||
const handleClick = (id: ToolId) => {
|
||||
if (isUnavailable) return;
|
||||
|
||||
@@ -194,6 +194,11 @@ export const convertOperationConfig = {
|
||||
customProcessor: convertProcessor, // Can't use callback version here
|
||||
operationType: 'convert',
|
||||
defaultParameters,
|
||||
endpoint: (params: ConvertParameters): string | undefined => {
|
||||
if (!params.fromExtension || !params.toExtension) return undefined;
|
||||
const actualToExtension = params.toExtension === 'pdfx' ? 'pdfa' : params.toExtension;
|
||||
return getEndpointUrl(params.fromExtension, actualToExtension) ?? undefined;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const useConvertOperation = (parameters?: ConvertParameters) => {
|
||||
|
||||
@@ -101,7 +101,13 @@ export interface CustomToolOperationConfig<TParams> extends BaseToolOperationCon
|
||||
toolType: ToolType.custom;
|
||||
|
||||
buildFormData?: undefined;
|
||||
endpoint?: undefined;
|
||||
|
||||
/**
|
||||
* Optional endpoint for routing decisions (credit check, cloud detection).
|
||||
* Not used for the API call itself — customProcessor handles that directly.
|
||||
* Provide a function when the endpoint depends on runtime parameters.
|
||||
*/
|
||||
endpoint?: string | ((params: TParams) => string | undefined);
|
||||
|
||||
/**
|
||||
* Custom processing logic that completely bypasses standard file processing.
|
||||
|
||||
@@ -70,14 +70,15 @@ 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
|
||||
// Determine endpoint for cloud usage check and credit routing.
|
||||
// For function endpoints, use defaultParameters to get a representative static value.
|
||||
const endpointString = config.endpoint
|
||||
? (typeof config.endpoint === 'function'
|
||||
? (config.defaultParameters ? config.endpoint(config.defaultParameters) : undefined)
|
||||
? (config.defaultParameters ? (config.endpoint(config.defaultParameters) ?? undefined) : undefined)
|
||||
: config.endpoint)
|
||||
: undefined;
|
||||
|
||||
const { checkCredits } = useCreditCheck(config.operationType, endpointString);
|
||||
const willUseCloud = useWillUseCloud(endpointString);
|
||||
|
||||
// Track last operation for undo functionality
|
||||
@@ -114,22 +115,24 @@ export const useToolOperation = <TParams>(
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Resolve the runtime endpoint from params (static string or function result).
|
||||
// Custom processors may omit endpoint entirely — result is undefined in that case.
|
||||
const runtimeEndpoint: string | undefined = config.endpoint
|
||||
? (typeof config.endpoint === 'function' ? (config.endpoint(params) ?? undefined) : config.endpoint)
|
||||
: undefined;
|
||||
|
||||
// Credit check — no-op in core builds, real check in desktop/SaaS versions
|
||||
const creditError = await checkCredits();
|
||||
// Credit check — no-op in core builds, real check in desktop/SaaS versions.
|
||||
// Pass runtime endpoint so the check can determine if this routes locally (no credits needed).
|
||||
const creditError = await checkCredits(runtimeEndpoint);
|
||||
if (creditError !== null) {
|
||||
actions.setError(creditError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Backend readiness check (will skip for SaaS-routed endpoints)
|
||||
const backendReady = await ensureBackendReady(endpoint);
|
||||
// Backend readiness check (will skip for SaaS-routed endpoints).
|
||||
// Custom processors without an endpoint skip this — they manage their own backend calls.
|
||||
const endpointForReadyCheck = config.toolType !== ToolType.custom ? runtimeEndpoint : undefined;
|
||||
const backendReady = await ensureBackendReady(endpointForReadyCheck);
|
||||
if (!backendReady) {
|
||||
actions.setError(t('backendHealth.offline', 'Embedded backend is offline. Please try again shortly.'));
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Desktop layer shadows this with the real implementation
|
||||
* In web builds, always allows the operation (no credit system)
|
||||
*/
|
||||
export function useCreditCheck(_operationType?: string) {
|
||||
export function useCreditCheck(_operationType?: string, _endpoint?: string) {
|
||||
return {
|
||||
checkCredits: async (): Promise<string | null> => null, // null = allowed
|
||||
checkCredits: async (_runtimeEndpoint?: string): Promise<string | null> => null, // null = allowed
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,14 +100,18 @@ export function useConversionCloudStatus(): ConversionStatus {
|
||||
pairs.map(async ([fromExt, toExt, endpointName]) => {
|
||||
const key = `${fromExt}-${toExt}`;
|
||||
try {
|
||||
const combined = await endpointAvailabilityService.checkEndpointCombined(
|
||||
// In SaaS mode, everything is available (locally or via cloud routing).
|
||||
// Only check local support to determine willUseCloud — the same approach
|
||||
// used by useMultipleEndpointsEnabled's SaaS enhancement.
|
||||
const availableLocally = await endpointAvailabilityService.isEndpointSupportedLocally(
|
||||
endpointName,
|
||||
tauriBackendService.getBackendUrl()
|
||||
);
|
||||
return { key, isAvailable: combined.isAvailable, willUseCloud: combined.willUseCloud, localOnly: combined.localOnly };
|
||||
return { key, isAvailable: true, willUseCloud: !availableLocally, localOnly: false };
|
||||
} catch (error) {
|
||||
console.error(`[useConversionCloudStatus] Endpoint check failed for ${key}:`, error);
|
||||
return { key, isAvailable: false, willUseCloud: false, localOnly: false };
|
||||
// On error, assume available via cloud (safe default in SaaS mode)
|
||||
return { key, isAvailable: true, willUseCloud: true, localOnly: false };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSaaSBilling } from '@app/contexts/SaasBillingContext';
|
||||
import { useSaaSMode } from '@app/hooks/useSaaSMode';
|
||||
import { getToolCreditCost } from '@app/utils/creditCosts';
|
||||
import { CREDIT_EVENTS } from '@app/constants/creditEvents';
|
||||
import { operationRouter } from '@app/services/operationRouter';
|
||||
import type { ToolId } from '@app/types/toolId';
|
||||
|
||||
/**
|
||||
@@ -15,15 +16,27 @@ import type { ToolId } from '@app/types/toolId';
|
||||
* Returns null when the operation is allowed, or an error message string
|
||||
* when it should be blocked.
|
||||
*/
|
||||
export function useCreditCheck(operationType?: string) {
|
||||
export function useCreditCheck(operationType?: string, endpoint?: string) {
|
||||
const billing = useSaaSBilling();
|
||||
const isSaaSMode = useSaaSMode();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkCredits = useCallback(async (): Promise<string | null> => {
|
||||
const checkCredits = useCallback(async (runtimeEndpoint?: string): Promise<string | null> => {
|
||||
if (!isSaaSMode) return null; // Credits only apply in SaaS mode, not self-hosted
|
||||
if (!billing) return null;
|
||||
|
||||
// If the operation routes to the local backend, no credits are consumed — skip check.
|
||||
// runtimeEndpoint (from params at execution time) takes priority over hook-level endpoint.
|
||||
const ep = runtimeEndpoint ?? endpoint;
|
||||
if (ep) {
|
||||
try {
|
||||
const willUseSaaS = await operationRouter.willRouteToSaaS(ep);
|
||||
if (!willUseSaaS) return null;
|
||||
} catch {
|
||||
// If routing check fails, fall through to credit check as a safe default.
|
||||
}
|
||||
}
|
||||
|
||||
const { creditBalance, loading } = billing;
|
||||
const requiredCredits = getToolCreditCost(operationType as ToolId);
|
||||
|
||||
@@ -44,7 +57,7 @@ export function useCreditCheck(operationType?: string) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [billing, isSaaSMode, operationType, t]);
|
||||
}, [billing, isSaaSMode, operationType, endpoint, t]);
|
||||
|
||||
return { checkCredits };
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { getToolCreditCost } from '@app/utils/creditCosts';
|
||||
import { openPlanSettings } from '@app/utils/appSettings';
|
||||
import type { ToolId } from '@app/types/toolId';
|
||||
|
||||
export function useCreditCheck(operationType?: string) {
|
||||
export function useCreditCheck(operationType?: string, _endpoint?: string) {
|
||||
const { hasSufficientCredits, isPro, creditBalance, refreshCredits } = useCredits();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkCredits = useCallback(async (): Promise<string | null> => {
|
||||
const checkCredits = useCallback(async (_runtimeEndpoint?: string): Promise<string | null> => {
|
||||
const requiredCredits = getToolCreditCost(operationType as ToolId);
|
||||
const creditCheck = hasSufficientCredits(requiredCredits);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user