From a22ec7a7d31e7a1e3929a0fb970f56bc4928deb5 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 9 Jun 2026 11:55:56 +0200 Subject: [PATCH] Enhance signature management functionality Added a new button in `EnvelopeViewer.razor` for creating or modifying signatures, with dynamic styling and tooltips based on the signature state. Enhanced `OpenSignaturePopup` and `OnPopupShownAsync` methods to preload and display existing signatures in the popup and canvas. Introduced new "success" button styles in `envelope-viewer.css` for better visual feedback. Added `loadExistingSignature` function in `receiver-signature.js` to render existing signatures on the canvas and updated the public API to expose this functionality. --- COPILOT_CONTEXT_EN.md | 223 ++++++++++++++++++ .../Pages/EnvelopeViewer.razor | 51 ++++ .../wwwroot/css/envelope-viewer.css | 62 +++++ .../wwwroot/js/receiver-signature.js | 21 +- 4 files changed, 356 insertions(+), 1 deletion(-) diff --git a/COPILOT_CONTEXT_EN.md b/COPILOT_CONTEXT_EN.md index 2399e363..a9a9ea3e 100644 --- a/COPILOT_CONTEXT_EN.md +++ b/COPILOT_CONTEXT_EN.md @@ -935,6 +935,229 @@ canvas { --- +## Signature Caching System — Session 15 + +**Purpose:** Persist receiver signature across page refreshes using distributed cache (Redis/SQL Server). + +### Architecture + +**API Cache Controller (`EnvelopeGenerator.API/Controllers/CacheController.cs`):** +```csharp +[Route("api/[controller]")] +[Authorize(Policy = AuthPolicy.Receiver)] +public class CacheController(IDistributedCache cache, IOptions cacheOptions) : ControllerBase +{ + private const string SignatureCacheKeyPrefix = "signature:91751687-8ae6-4777-bf5f-b8846085e62e:"; + + [HttpPost("SignatureCapture/{envelopeKey}")] + public async Task SaveSignature(string envelopeKey, + [FromBody] SignatureCaptureDto request, CancellationToken cancel) + + [HttpGet("SignatureCapture/{envelopeKey}")] + public async Task GetSignature(string envelopeKey, CancellationToken cancel) + + [HttpDelete("SignatureCapture/{envelopeKey}")] + public async Task DeleteSignature(string envelopeKey, CancellationToken cancel) +} +``` + +**Cache Options (`EnvelopeGenerator.API/Options/CacheOptions.cs`):** +```csharp +public sealed class CacheOptions +{ + public const string SectionName = "Cache"; + + // If null, signatures never expire (until manual delete) + public TimeSpan? SignatureCacheExpiration { get; set; } +} +``` + +**Configuration (appsettings.json):** +```json +{ + "Cache": { + "SignatureCacheExpiration": null // Or "02:00:00" for 2 hours + } +} +``` + +**Blazor Service (`EnvelopeGenerator.ReceiverUI/Services/SignatureCacheService.cs`):** +```csharp +public class SignatureCacheService(HttpClient http, IOptions apiOptions) +{ + public async Task SaveSignatureAsync(string envelopeKey, + SignatureCaptureDto signature, CancellationToken cancel = default) + + public async Task GetSignatureAsync(string envelopeKey, + CancellationToken cancel = default) + + public async Task DeleteSignatureAsync(string envelopeKey, + CancellationToken cancel = default) +} +``` + +### Workflow + +**1. Page Load (First Time):** +```csharp +protected override async Task OnInitializedAsync() { + // Try to load cached signature first + try { + var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey); + if (cachedSignature is not null) { + _capturedSignature = cachedSignature; + _signerFullName = cachedSignature.FullName; + _signerPosition = cachedSignature.Position; + _signaturePlace = cachedSignature.Place; + _signaturePopupVisible = false; // Skip popup + } else { + // No cache - show popup + _signaturePopupVisible = true; + } + } catch (Exception ex) { + logger.LogWarning(ex, "Failed to load cached signature, showing popup"); + _signaturePopupVisible = true; // Fallback to popup + } +} +``` + +**2. Save Signature:** +```csharp +async Task SaveSignatureAsync() { + // ... validation ... + + _capturedSignature = new SignatureCaptureDto { ... }; + _signaturePopupVisible = false; + + // Save to cache (fire-and-forget, ignore errors) + _ = Task.Run(async () => { + try { + await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature); + } catch { + // Ignore cache errors + } + }); +} +``` + +**3. Change Signature (Toolbar Button):** +```csharp +void OpenSignaturePopup() { + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + + // Load current signature into form fields + if (_capturedSignature is not null) { + _signerFullName = _capturedSignature.FullName; + _signerPosition = _capturedSignature.Position; + _signaturePlace = _capturedSignature.Place; + } +} + +async Task OnPopupShownAsync() { + await InitializeActiveSignatureTabAsync(); + + // Load existing signature image to canvas (Draw tab) + if (_capturedSignature is not null && _activeSignatureTab == SignatureTabDraw) { + await Task.Delay(100); + await JSRuntime.InvokeVoidAsync("receiverSignature.loadExistingSignature", + DrawCanvasId, _capturedSignature.DataUrl); + } +} +``` + +**JavaScript Helper:** +```javascript +receiverSignature.loadExistingSignature(canvasId, dataUrl) { + const canvas = document.getElementById(canvasId); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + state.hasSignature = true; + }; + + img.src = dataUrl; +} +``` + +**4. Restart Signing (Reset Button):** +```csharp +void RestartSigning() { + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + // Page reload ? cache cleared ? popup shows +} +``` + +### Toolbar Button Design + +**Change Signature Button:** +```html + +``` + +**CSS:** +```css +.pdf-toolbar__btn--success { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); + border-color: rgba(16, 185, 129, 0.3); + color: #059669; +} + +.pdf-toolbar__btn--success:hover:not(:disabled) { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: white; +} +``` + +**States:** +- **No Signature:** Blue button with pen icon +- **Signature Created:** Green button with checkmark icon +- **Hover:** Filled green gradient with white icon + +### Cache Key Format +``` +signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey} +``` +- Prefix prevents collisions with other cache keys +- GUID ensures uniqueness across application instances +- `envelopeKey` is user-provided identifier (URL parameter) + +### Error Handling Philosophy + +**API Controller:** No validation, no try-catch, no logging +- Throws exceptions directly to caller +- Blazor handles errors with try-catch + +**Blazor Service:** No try-catch +- Throws `HttpRequestException` with status code + body +- Component catches and handles + +**Blazor Component:** Try-catch with fallback +- Graceful degradation: show popup on cache failure +- Fire-and-forget saves: ignore errors + +### Benefits + +1. **UX:** No need to re-enter signature on page refresh +2. **Performance:** Fast cache retrieval (Redis/SQL) +3. **Security:** Per-receiver isolation (cookie-based auth) +4. **Flexibility:** Configurable expiration time +5. **Reliability:** Graceful degradation on cache failure + +--- + + diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor index e5d3d6dc..78b99f3d 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor @@ -219,6 +219,20 @@
@if (_totalSignatures > 0) { +
+ +
+ +
+