Files
Stirling-PDF/frontend/src/saas/services/signatureStorageService.ts
T
James BruntonandGitHub fa8c52b2be Add SaaS frontend code (#5879)
# Description of Changes
Adds the code for the SaaS frontend as proprietary code to the OSS repo.
This version of the code is adapted from 22/1/2026, which was the last
SaaS version based on the 'V2' design. This will move us closer to being
able to have the OSS products understand whether the user has a SaaS
account, and provide the correct UI in those cases.
2026-03-11 11:53:54 +00:00

148 lines
4.2 KiB
TypeScript

import type { SavedSignature } from '@app/hooks/tools/sign/useSavedSignatures';
export type StorageType = 'backend' | 'localStorage';
interface SignatureStorageCapabilities {
supportsBackend: boolean;
storageType: StorageType;
}
/**
* SaaS-specific signature storage service that always uses localStorage.
*
* In SaaS mode, the proprietary backend signature API is not available
* (requires Spring Security JWT, not Supabase JWT), so we skip detection
* and force localStorage-only mode to avoid unnecessary 401/403 errors.
*/
class SignatureStorageService {
private capabilities: SignatureStorageCapabilities | null = null;
private blobUrls: Set<string> = new Set();
private readonly STORAGE_KEY = 'stirling:saved-signatures:v1';
/**
* Detect capabilities - in SaaS mode, always returns localStorage
*/
async detectCapabilities(): Promise<SignatureStorageCapabilities> {
if (this.capabilities) {
return this.capabilities;
}
// SaaS mode always uses localStorage (no backend signature API available)
console.log('[SignatureStorage] SaaS mode - using localStorage (backend not available)');
this.capabilities = {
supportsBackend: false,
storageType: 'localStorage',
};
return this.capabilities;
}
/**
* Get current storage type
*/
async getStorageType(): Promise<StorageType> {
const capabilities = await this.detectCapabilities();
return capabilities.storageType;
}
/**
* Load all signatures
*/
async loadSignatures(): Promise<SavedSignature[]> {
// Clean up old blob URLs before loading new ones
this.cleanup();
// Always use localStorage in SaaS mode
return this._loadFromLocalStorage();
}
/**
* Save a signature
*/
async saveSignature(signature: SavedSignature): Promise<void> {
// Force scope to localStorage for SaaS mode
signature.scope = 'localStorage';
this._saveToLocalStorage(signature);
}
/**
* Delete a signature
*/
async deleteSignature(id: string): Promise<void> {
this._deleteFromLocalStorage(id);
}
/**
* Update signature label
*/
async updateSignatureLabel(id: string, label: string): Promise<void> {
this._updateLabelInLocalStorage(id, label);
}
// LocalStorage methods
private _loadFromLocalStorage(): SavedSignature[] {
try {
const raw = localStorage.getItem(this.STORAGE_KEY);
if (!raw) return [];
const signatures = JSON.parse(raw);
// Ensure all localStorage signatures have the correct scope
return signatures.map((sig: SavedSignature) => ({
...sig,
scope: 'localStorage' as const,
}));
} catch {
return [];
}
}
private _saveToLocalStorage(signature: SavedSignature): void {
const signatures = this._loadFromLocalStorage();
const index = signatures.findIndex(s => s.id === signature.id);
if (index >= 0) {
signatures[index] = signature;
} else {
signatures.unshift(signature);
}
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(signatures));
}
private _deleteFromLocalStorage(id: string): void {
const signatures = this._loadFromLocalStorage();
const filtered = signatures.filter(s => s.id !== id);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filtered));
}
private _updateLabelInLocalStorage(id: string, label: string): void {
const signatures = this._loadFromLocalStorage();
const signature = signatures.find(s => s.id === id);
if (signature) {
signature.label = label;
signature.updatedAt = Date.now();
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(signatures));
}
}
/**
* Migrate signatures from localStorage to backend
* In SaaS mode, this is a no-op since we don't support backend storage
*/
async migrateToBackend(): Promise<{ migrated: number; failed: number }> {
console.log('[SignatureStorage] Migration not supported in SaaS mode');
return { migrated: 0, failed: 0 };
}
/**
* Clean up blob URLs to prevent memory leaks
*/
cleanup(): void {
this.blobUrls.forEach(url => {
URL.revokeObjectURL(url);
});
this.blobUrls.clear();
}
}
export const signatureStorageService = new SignatureStorageService();