mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Save signatures to server (#5080)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
fde449e738
commit
651f17f1c6
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { signatureStorageService, type StorageType } from '@app/services/signatureStorageService';
|
||||
|
||||
const STORAGE_KEY = 'stirling:saved-signatures:v1';
|
||||
export const MAX_SAVED_SIGNATURES = 10;
|
||||
|
||||
export type SavedSignatureType = 'canvas' | 'image' | 'text';
|
||||
export type SignatureScope = 'personal' | 'shared' | 'localStorage';
|
||||
|
||||
export type SavedSignaturePayload =
|
||||
| {
|
||||
@@ -16,6 +17,7 @@ export type SavedSignaturePayload =
|
||||
}
|
||||
| {
|
||||
type: 'text';
|
||||
dataUrl: string;
|
||||
signerName: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
@@ -25,6 +27,7 @@ export type SavedSignaturePayload =
|
||||
export type SavedSignature = SavedSignaturePayload & {
|
||||
id: string;
|
||||
label: string;
|
||||
scope: SignatureScope;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
};
|
||||
@@ -35,68 +38,6 @@ export type AddSignatureResult =
|
||||
|
||||
const isSupportedEnvironment = () => typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||
|
||||
const safeParse = (raw: string | null): SavedSignature[] => {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parsed.filter((entry: any): entry is SavedSignature => {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (typeof entry.id !== 'string' || typeof entry.label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (typeof entry.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.type === 'text') {
|
||||
return (
|
||||
typeof entry.signerName === 'string' &&
|
||||
typeof entry.fontFamily === 'string' &&
|
||||
typeof entry.fontSize === 'number' &&
|
||||
typeof entry.textColor === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
return typeof entry.dataUrl === 'string';
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const readFromStorage = (): SavedSignature[] => {
|
||||
if (!isSupportedEnvironment()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return safeParse(window.localStorage.getItem(STORAGE_KEY));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const writeToStorage = (entries: SavedSignature[]) => {
|
||||
if (!isSupportedEnvironment()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
||||
} catch {
|
||||
// Swallow storage errors silently; we still keep state in memory.
|
||||
}
|
||||
};
|
||||
|
||||
const generateId = () => {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
@@ -105,29 +46,61 @@ const generateId = () => {
|
||||
};
|
||||
|
||||
export const useSavedSignatures = () => {
|
||||
const [savedSignatures, setSavedSignatures] = useState<SavedSignature[]>(() => readFromStorage());
|
||||
const [savedSignatures, setSavedSignatures] = useState<SavedSignature[]>([]);
|
||||
const [storageType, setStorageType] = useState<StorageType | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load signatures and detect storage type on mount
|
||||
useEffect(() => {
|
||||
if (!isSupportedEnvironment()) {
|
||||
const loadSignatures = async () => {
|
||||
try {
|
||||
const [signatures, type] = await Promise.all([
|
||||
signatureStorageService.loadSignatures(),
|
||||
signatureStorageService.getStorageType(),
|
||||
]);
|
||||
setSavedSignatures(signatures);
|
||||
setStorageType(type);
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to load signatures:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSignatures();
|
||||
}, []);
|
||||
|
||||
// Attempt migration from localStorage to backend when backend becomes available
|
||||
useEffect(() => {
|
||||
if (storageType === 'backend' && !isLoading) {
|
||||
signatureStorageService.migrateToBackend().then(result => {
|
||||
if (result.migrated > 0) {
|
||||
console.log(`[useSavedSignatures] Migrated ${result.migrated} signatures to backend`);
|
||||
// Reload after migration
|
||||
signatureStorageService.loadSignatures().then(setSavedSignatures);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [storageType, isLoading]);
|
||||
|
||||
// Listen for storage events (for localStorage only)
|
||||
useEffect(() => {
|
||||
if (!isSupportedEnvironment() || storageType !== 'localStorage') {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncFromStorage = () => {
|
||||
setSavedSignatures(readFromStorage());
|
||||
signatureStorageService.loadSignatures().then(setSavedSignatures);
|
||||
};
|
||||
|
||||
window.addEventListener('storage', syncFromStorage);
|
||||
return () => window.removeEventListener('storage', syncFromStorage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
writeToStorage(savedSignatures);
|
||||
}, [savedSignatures]);
|
||||
}, [storageType]);
|
||||
|
||||
const isAtCapacity = savedSignatures.length >= MAX_SAVED_SIGNATURES;
|
||||
|
||||
const addSignature = useCallback(
|
||||
(payload: SavedSignaturePayload, label?: string): AddSignatureResult => {
|
||||
async (payload: SavedSignaturePayload, label?: string, scope?: SignatureScope): Promise<AddSignatureResult> => {
|
||||
if (
|
||||
(payload.type === 'text' && !payload.signerName.trim()) ||
|
||||
((payload.type === 'canvas' || payload.type === 'image') && !payload.dataUrl)
|
||||
@@ -135,62 +108,85 @@ export const useSavedSignatures = () => {
|
||||
return { success: false, reason: 'invalid' };
|
||||
}
|
||||
|
||||
let createdSignature: SavedSignature | null = null;
|
||||
setSavedSignatures(prev => {
|
||||
if (prev.length >= MAX_SAVED_SIGNATURES) {
|
||||
return prev;
|
||||
}
|
||||
if (savedSignatures.length >= MAX_SAVED_SIGNATURES) {
|
||||
return { success: false, reason: 'limit' };
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const nextEntry: SavedSignature = {
|
||||
...payload,
|
||||
id: generateId(),
|
||||
label: (label || 'Signature').trim() || 'Signature',
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
createdSignature = nextEntry;
|
||||
return [nextEntry, ...prev];
|
||||
});
|
||||
const timestamp = Date.now();
|
||||
const newSignature: SavedSignature = {
|
||||
...payload,
|
||||
id: generateId(),
|
||||
label: (label || 'Signature').trim() || 'Signature',
|
||||
scope: scope || (storageType === 'backend' ? 'personal' : 'localStorage'),
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
|
||||
return createdSignature
|
||||
? { success: true, signature: createdSignature }
|
||||
: { success: false, reason: 'limit' };
|
||||
try {
|
||||
await signatureStorageService.saveSignature(newSignature);
|
||||
setSavedSignatures(prev => [newSignature, ...prev]);
|
||||
return { success: true, signature: newSignature };
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to save signature:', error);
|
||||
return { success: false, reason: 'invalid' };
|
||||
}
|
||||
},
|
||||
[]
|
||||
[savedSignatures.length, storageType]
|
||||
);
|
||||
|
||||
const removeSignature = useCallback((id: string) => {
|
||||
setSavedSignatures(prev => prev.filter(entry => entry.id !== id));
|
||||
const removeSignature = useCallback(async (id: string) => {
|
||||
try {
|
||||
await signatureStorageService.deleteSignature(id);
|
||||
setSavedSignatures(prev => prev.filter(entry => entry.id !== id));
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to delete signature:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateSignatureLabel = useCallback((id: string, nextLabel: string) => {
|
||||
setSavedSignatures(prev =>
|
||||
prev.map(entry =>
|
||||
entry.id === id
|
||||
? { ...entry, label: nextLabel.trim() || entry.label || 'Signature', updatedAt: Date.now() }
|
||||
: entry
|
||||
)
|
||||
);
|
||||
const updateSignatureLabel = useCallback(async (id: string, nextLabel: string) => {
|
||||
try {
|
||||
await signatureStorageService.updateSignatureLabel(id, nextLabel);
|
||||
setSavedSignatures(prev =>
|
||||
prev.map(entry =>
|
||||
entry.id === id
|
||||
? { ...entry, label: nextLabel.trim() || entry.label || 'Signature', updatedAt: Date.now() }
|
||||
: entry
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to update signature label:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const replaceSignature = useCallback((id: string, payload: SavedSignaturePayload) => {
|
||||
setSavedSignatures(prev =>
|
||||
prev.map(entry =>
|
||||
entry.id === id
|
||||
? {
|
||||
...entry,
|
||||
...payload,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
const replaceSignature = useCallback(
|
||||
async (id: string, payload: SavedSignaturePayload) => {
|
||||
const existing = savedSignatures.find(s => s.id === id);
|
||||
if (!existing) return;
|
||||
|
||||
const clearSignatures = useCallback(() => {
|
||||
setSavedSignatures([]);
|
||||
}, []);
|
||||
const updated: SavedSignature = {
|
||||
...existing,
|
||||
...payload,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
await signatureStorageService.saveSignature(updated);
|
||||
setSavedSignatures(prev => prev.map(entry => (entry.id === id ? updated : entry)));
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to replace signature:', error);
|
||||
}
|
||||
},
|
||||
[savedSignatures]
|
||||
);
|
||||
|
||||
const clearSignatures = useCallback(async () => {
|
||||
try {
|
||||
await Promise.all(savedSignatures.map(sig => signatureStorageService.deleteSignature(sig.id)));
|
||||
setSavedSignatures([]);
|
||||
} catch (error) {
|
||||
console.error('[useSavedSignatures] Failed to clear signatures:', error);
|
||||
}
|
||||
}, [savedSignatures]);
|
||||
|
||||
const byTypeCounts = useMemo(() => {
|
||||
return savedSignatures.reduce<Record<SavedSignatureType, number>>(
|
||||
@@ -211,6 +207,8 @@ export const useSavedSignatures = () => {
|
||||
replaceSignature,
|
||||
clearSignatures,
|
||||
byTypeCounts,
|
||||
storageType,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user