From b6d86aa3eb077121ae96e23ef71b563a89e9909f Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 11 Jun 2026 10:25:44 +0200 Subject: [PATCH] Refactor documentation for unified architecture Updated documentation to reflect the transition to a unified Blazor WASM frontend for both Senders and Receivers. Removed references to PSPDFKit and legacy components, emphasizing the use of PDF.js and DevExpress. Key changes include: - Revised purpose and architecture sections. - Updated solution structure to mark `EnvelopeGenerator.Web` as deprecated. - Enhanced coordinate system explanation with conversion formulas. - Documented new `EnvelopeViewer` features and signature workflows. - Added details on signature caching and login flows for Senders and Receivers. - Expanded "Mistakes History" to highlight lessons learned. - Added quick reference for debugging and development consistency. These changes improve clarity, maintainability, and alignment with the current system architecture. --- COPILOT_CONTEXT_EN.md | 1381 +++++++++-------------------------------- 1 file changed, 291 insertions(+), 1090 deletions(-) diff --git a/COPILOT_CONTEXT_EN.md b/COPILOT_CONTEXT_EN.md index a9a9ea3e..9bbb5736 100644 --- a/COPILOT_CONTEXT_EN.md +++ b/COPILOT_CONTEXT_EN.md @@ -1,1169 +1,370 @@ -# EnvelopeGenerator — Copilot Context Notes (English) +# EnvelopeGenerator — AI Context Reference ## Purpose -A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF. +Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs. + +**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed) + +--- + +## Architecture Evolution + +### Old Architecture (Deprecated) +- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit) +- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js) +- **Backend:** `EnvelopeGenerator.API` + +### Current Architecture +- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers** +- **Backend:** `EnvelopeGenerator.API` — **Both Senders & Receivers** +- **Libraries:** DevExpress + PDF.js +- **PSPDFKit:** **REMOVED** --- ## Solution Structure -| Project | Target | Description | +| Project | Target | Purpose | |---|---|---| -| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. | -| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. | -| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. | -| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. | +| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. | +| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. | +| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. | +| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. | | `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. | | `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. | -| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities. NOT used in ReceiverUI flow. | +| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). | | `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. | -| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. | +| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** | --- -## Key Files +## Key Files & Routes -| File | Purpose | +| File | Route/Purpose | |---|---| -| `ReceiverUI/Pages/EnvelopeViewer.razor` | **NEW** PDF.js-based viewer (`/envelope/{key}`). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. | -| `ReceiverUI/Pages/ReportViewer.razor` | **LEGACY** DevExpress-based signing page (`/receiver/{key}`). Still used for signature workflow. Will be deprecated. | -| `ReceiverUI/wwwroot/js/pdf-viewer.js` | **NEW** PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. | -| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, signature pad (draw/type/image). | -| `ReceiverUI/wwwroot/css/envelope-viewer.css` | **NEW** Styles for EnvelopeViewer.razor (external CSS, not inline). | -| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev-mode fake annotations (YARP proxy target). | -| `ReceiverUI/Models/AnnotationDto.cs` | Annotation position model. All properties non-nullable. | -| `ReceiverUI/Services/AnnotationService.cs` | Fetches `List` from API or fake-data. | -| `ReceiverUI/Services/DocumentService.cs` | Fetches PDF bytes from API. | -| `ReceiverUI/Services/AuthService.cs` | Manages receiver session cookie. | -| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation list. | -| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF bytes. | +| `ReceiverUI/Pages/EnvelopeViewer.razor` | `/envelope/{key}` — PDF.js viewer (read-only). | +| `ReceiverUI/Pages/LoginReceiver.razor` | `/login/{EnvelopeKey}` — Access code auth. | +| `ReceiverUI/Pages/LoginSender.razor` | `/login` — Username/password auth. | +| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). | +| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). | +| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. | +| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. | +| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). | +| `API/Controllers/CacheController.cs` | Signature cache endpoints. | --- -## SignatureDto / AnnotationDto — Coordinate System +## Coordinate System — CRITICAL -**Database Storage Format:** INCHES (GdPicture14 native unit) -**Origin:** Top-left corner of page -**Axes:** X increases rightward, Y increases downward - -### Source Evidence (VB.NET Legacy Code) - -```vb -' From: EnvelopeGenerator.Form/frmFieldEditor.vb -' GdPicture14.Annotations.AnnotationStickyNote - -'Breite und Höhe in Inches (4,5*5cm) -Private Const SIGNATURE_WIDTH As Single = 1.77 ' 1.77 inches = 4.5cm -Private Const SIGNATURE_HEIGHT As Single = 1.96 ' 1.96 inches = 5cm - -Sub LoadAnnotation(pElement As Signature, ...) - oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES - oAnnotation.Top = CSng(pElement.Y) - oAnnotation.Width = CSng(pElement.Width) - oAnnotation.Height = CSng(pElement.Height) -End Sub -``` +**Database Format:** INCHES (GdPicture14 native) +**Origin:** Top-left corner +**Axes:** X right, Y down ### Conversion Formulas -``` -Inches ? DevExpress (DX): x_DX = x_inches * 100.0 - y_DX = y_inches * 100.0 +| From INCHES to | Formula | Example | +|---|---|---| +| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX | +| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt | +| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` | -Inches ? PDF Points: x_pt = x_inches * 72.0 - y_pt = x_inches * 72.0 +**A4 Dimensions:** +- Width: 8.27" = 595pt = 827 DX +- Height: 11.69" = 842pt = 1169 DX -Inches ? PDF.js Canvas: normalize to [0,1], then scale to pixels - x_norm = x_inches / pageWidth_inches - y_norm = y_inches / pageHeight_inches - x_px = x_norm * canvasWidth * scale * dpr - y_px = y_norm * canvasHeight * scale * dpr -``` +### Unit Systems -### Unit Comparison Table - -| System | Unit | Origin | Conversion from INCHES | -|--------|------|--------|------------------------| -| **GdPicture14** (Source) | **Inches** | Top-left | Database format (no conversion) | -| DevExpress (LEGACY) | 1/100 inch (DX) | Top-left | `x_DX = x_inches * 100` | -| PDF.js (NEW) | Pixels | Top-left | `normalize ? scale` | -| PDF Points (iText7) | Points (1/72") | **Bottom-left** | `x_pt = x_inches * 72` + Y-flip | -| PSPDFKit (Web) | Points (1/72") | Top-left | `x_pt = x_inches * 72` | - -**A4 Page Dimensions:** -- Width: 8.27 inches = 595 points = 827 DX units -- Height: 11.69 inches = 842 points = 1169 DX units +| System | Unit | Origin | Y-Axis | +|---|---|---|---| +| **Database (GdPicture14)** | Inches | Top-left | Down | +| PDF.js | Pixels | Top-left | Down | +| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) | +| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** | --- -## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer +## EnvelopeViewer — PDF.js Viewer **Route:** `/envelope/{EnvelopeKey}` -**Purpose:** Modern, high-performance PDF viewing without signing functionality. -**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings +**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality +**File:** `ReceiverUI/Pages/EnvelopeViewer.razor` -### Architecture +### Key Features +1. HiDPI/Retina support (4x quality) +2. Configurable quality (`appsettings.json`) +3. Unlimited zoom (50%-300%) +4. Ctrl+Wheel global zoom +5. Resizable thumbnail sidebar (150-400px, localStorage) +6. Responsive (desktop/mobile) -**Blazor Component (`EnvelopeViewer.razor`):** -- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)` -- Converts to base64 data URL: `data:application/pdf;base64,{base64}` -- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks -- Quality settings loaded from `appsettings.json` via `IOptions` -- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence) -- CSS externalized to `envelope-viewer.css` - -**JavaScript (`pdf-viewer.js`):** -```javascript -window.pdfViewer = { - qualityOptions, // Configurable from appsettings.json - setQualityOptions(options), // Dynamic quality update - initialize(canvasId, pdfDataUrl, dotNetRef), - renderPage(num), - renderThumbnail(pageNum, canvasId), - attachWheelEvent(), // Ctrl+Wheel zoom (configurable step) - zoomIn(), zoomOut(), // Configurable step percentage - dispose() -} -``` - -**Options (`PdfViewerOptions.cs`):** -```csharp -public class PdfViewerOptions { - public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5 - public bool ThumbnailEnableHiDPI { get; set; } = true; - public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0 - public bool MainCanvasEnableHiDPI { get; set; } = true; - public double MainCanvasMaxDPR { get; set; } = 2.0; - public bool EnableSmoothZoom { get; set; } = true; - public int ZoomTransitionDuration { get; set; } = 150; // ms - public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0 - public int ThumbnailRenderDelay { get; set; } = 50; // ms - public int ZoomStepPercentage { get; set; } = 5; // 1-50% -} -``` - -### Configuration (appsettings.json) - -**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json` +### Configuration +**File:** `ReceiverUI/wwwroot/appsettings.json` ```json { "PdfViewer": { "ThumbnailBaseScale": 0.75, "ThumbnailEnableHiDPI": true, - "ThumbnailMaxDPR": 2.0, "MainCanvasEnableHiDPI": true, - "MainCanvasMaxDPR": 2.0, - "EnableSmoothZoom": true, - "ZoomTransitionDuration": 150, - "RenderingOpacity": 0.85, - "ThumbnailRenderDelay": 50, "ZoomStepPercentage": 5 } } ``` -**Usage:** Edit file ? F5 (browser refresh) ? new settings applied +### JavaScript API +**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js` -**Presets:** -- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices) -- **Balanced**: Default values (recommended) -- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end) - ---- - -### Features - -1. **HiDPI/Retina Support** ? 4x quality on Retina displays -2. **Configurable Quality** ? All parameters in appsettings.json -3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%) -4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page -5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering -6. **Smooth Transitions** ? Configurable fade effect -7. **Responsive Design** ? Desktop/mobile adaptive layout - ---- - -### Features - -1. **Unlimited Zoom:** - - Canvas size not restricted by `max-width` - - Frame stays fixed, scroll bars appear automatically - - `text-align: center` for small sizes, full scroll for zoomed views - -2. **Global Mouse Wheel Zoom:** - - Event listener on `document.body` (works anywhere on page) - - `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()` - - Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI - - `{ passive: false }` to enable `preventDefault()` - -3. **Render Task Cancellation:** - - Stores `currentRenderTask` to cancel previous render if new one starts - - Catches `RenderingCancelledException` to avoid console errors - - Queue system (`pageNumPending`) for rapid page changes - -4. **Thumbnail Sidebar:** - - Left panel with page previews (sequential rendering, 50ms delay) - - Click to navigate to specific page - - Active page highlighted with gradient border - - No header/title (maximizes thumbnail space) - - Toggle button in toolbar to show/hide - -5. **Resizable Splitter:** - - 4px draggable divider between thumbnails and canvas - - Min width: 150px, Max width: 400px - - Visual feedback: gradient on hover/active - - User preference saved to localStorage (`envelopeViewer_thumbnailWidth`) - - Global mouse events (works anywhere during drag) - - `col-resize` cursor (?) for intuitive UX - -6. **Flex Layout:** - - Thumbnails and canvas in same container (`.pdf-frame`) - - `display: flex, flex-direction: row, align-items: stretch` - - Perfect vertical alignment (same top/bottom position) - - Responsive: column layout on mobile (<768px) - -7. **Responsive Design:** - - Desktop: 90% width, 1200px max - - Mobile: 95% width, adjusted heights, thumbnails collapse to top - - Adaptive padding and font sizes - -### Initialization Flow - -```csharp -OnInitializedAsync(): - 1. Fetch PDF bytes from DocumentService - 2. Convert to base64 data URL - 3. Set _isLoading = false - -OnAfterRenderAsync(firstRender): - 1. Load saved thumbnail width from localStorage - 2. Create DotNetObjectReference - 3. Send PdfViewerOptions to JavaScript - 4. Initialize PDF.js viewer - 5. Attach splitter resize listeners - 6. Render thumbnails sequentially (configurable delay) -``` - -**User Interactions:** -- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage -- Pages: Buttons/Input/Thumbnails ? navigate -- Sidebar: Toggle button ? show/hide thumbnails -- Splitter: Drag ? resize sidebar (150-400px) - -**Cleanup:** -```csharp -DisposeAsync(): - - Dispose PDF.js viewer - - Detach event listeners - - Dispose DotNetObjectReference -``` - ---- - -### Key Differences from ReportViewer - -| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) | -|---------|----------------------|------------------------| -| Technology | PDF.js + Canvas | DevExpress XtraReports | -| Route | `/envelope/{key}` | `/receiver/{key}` | -| Purpose | Read-only viewing | Signature workflow | -| Dependencies | PDF.js CDN | DevExpress NuGet packages | -| Zoom | Unlimited, smooth | Report viewer default | -| Mouse Wheel | Custom Ctrl+Wheel | Browser default | -| File Size | Minimal (JS + CSS) | Heavy (DX libs) | -| Maintenance | Simple, standard web | Complex, vendor-specific | - ---- - -## ReceiverUI Signing Flow (ReportViewer.razor — LEGACY) - -### On Load (`OnInitializedAsync`) -1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized -2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations` -3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode) -4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer` - -### Signature Popup -- Tabs: Draw / Text / Image -- Fields: full name (required), position (optional), place (required) -- Saved to `_capturedSignature` record -- If annotations exist ? popup closes ? JS checkbox overlays installed - -### JS Checkbox Overlay (`receiver-signature.js`) -- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)` -- One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container -- Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY` - - `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169` -- Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)` - -### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`) - -**Real PDF mode (`_basePdfBytes` is set):** -- Calls `StampSignaturesOnPdf` using **iText7** directly on PDF bytes -- Coordinate conversion: `xPt = ann.X * (72/100)`, `yPt = pageHeight - ann.Y * (72/100) - sigHeight` (Y flip: DX top-down ? PDF bottom-up) -- Returns stamped bytes ? loaded into new `XtraReport` with `XRPdfContent` -- Viewer refreshed with `ViewerKey++` - -**Dev/fake mode (`_basePdfBytes` is null):** -- Falls back to `AddSignatureAtAnnotation` (XtraReports DetailBand + BeforePrint counter) -- This is intentionally left as a dev fallback only - -### Export -- `reportViewer.ExportToAsync(ExportFormat.Pdf)` - ---- - -## StampSignaturesOnPdf — iText7 Implementation - -```csharp -// Located in ReportViewer.razor @code section -// Called by SubmitSignaturesAsync when _basePdfBytes is available - -static byte[] StampSignaturesOnPdf( - byte[] sourcePdfBytes, byte[] signatureImageBytes, - string signerFullName, string signerPosition, string signaturePlace, - IReadOnlyList annotations) -{ - // Opens PDF with PdfReader/PdfWriter - // For each annotation: - // pageNum = ann.Page (clamped to totalPages) - // xPt = ann.X * (72f/100f) - // imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip - // PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false) - // Separator line at bottom of image - // Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87)) - // Returns stamped byte[] +```javascript +window.pdfViewer = { + initialize(canvasId, pdfDataUrl, dotNetRef), + renderPage(num), + renderSignatureButtons(signatures, pageNum, dotNetRef), + applySignature(signatureId, dataUrl, fullName, position, place), + zoomIn(), zoomOut(), dispose() } ``` --- -## BuildFreshBaseReport() +## Signature Workflow — EnvelopeViewer + +**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only. + +### Workflow Steps + +1. **Page Load:** + - Check `SignatureCacheService` for cached signature + - If cached ? skip popup, load signature + - If not ? show automatic popup (mandatory) + +2. **Signature Popup (DxPopup):** + - **Cannot close** (no X, no ESC, no outside-click) + - **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload) + - **Required:** Full name, Place + - **Optional:** Position + - **Save ?** Store in `_capturedSignature`, cache via API + +3. **Signature Buttons:** + - Render purple "Unterschreiben" buttons at signature field positions + - Coordinates: INCHES ? POINTS ? Pixels (scaled) + - File: `pdf-viewer.js` ? `renderSignatureButtons()` + +4. **Apply Signature (Click "Unterschreiben"):** + - JS: Remove button, create HTML overlay + - Format: Image + separator + text (Name, Position, Place, Date) + - **NOT stamped on PDF bytes** (visual overlay only) + +5. **Re-rendering:** + - Zoom/Page change ? recalculate button positions + - Session state: `_capturedSignature` (lost on refresh) + +### Data Model +**File:** `ReceiverUI/Models/SignatureCaptureDto.cs` ```csharp -// Real PDF mode: -var report = new XtraReport(); -var detail = new DetailBand(); -report.Bands.Add(detail); -detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true }); -return report; - -// Dev/fake mode: returns pre-built report from ReportStorage +public sealed record SignatureCaptureDto { + public required string DataUrl { get; init; } // base64 PNG + public required string FullName { get; init; } + public string Position { get; init; } = ""; // Optional + public required string Place { get; init; } +} ``` --- -## NuGet Packages in ReceiverUI +## Signature Caching + +**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL) + +### API Endpoints +**Controller:** `API/Controllers/CacheController.cs` + +- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save +- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load +- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete + +**Cache Key Format:** +``` +signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey} +``` + +**Configuration:** `appsettings.json` +```json +{ + "Cache": { + "SignatureCacheExpiration": null // or "02:00:00" for 2h + } +} +``` + +### Service +**File:** `ReceiverUI/Services/SignatureCacheService.cs` + +```csharp +public class SignatureCacheService { + Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature); + Task GetSignatureAsync(string envelopeKey); + Task DeleteSignatureAsync(string envelopeKey); +} +``` + +**Error Handling:** Fire-and-forget saves, graceful degradation on load failure. + +--- + +## Sender Login + +**Route:** `/login` +**File:** `ReceiverUI/Pages/LoginSender.razor` +**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme + +### AuthService Extension +**File:** `ReceiverUI/Services/AuthService.cs` + +```csharp +public enum SenderLoginResult { Success, InvalidCredentials, Error } + +public async Task LoginSenderAsync(string username, string password) { + var response = await http.PostAsJsonAsync( + $"{_api.BaseUrl}/api/auth?cookie=true", + new { username, password }); + + return response.StatusCode switch { + HttpStatusCode.OK => SenderLoginResult.Success, + HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials, + _ => SenderLoginResult.Error + }; +} +``` + +### API Integration +**Endpoint:** `POST /api/auth?cookie=true` + +**Request:** +```json +{ "username": "TekH", "password": "***" } +``` + +**Response:** +- `200 OK` ? Cookie set, redirect to `/` +- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten" +- Other ? Show error: "Serverfehler" + +**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict + +### UI Flow +1. User enters username + password +2. Click "Anmelden" or press Enter +3. Call `AuthService.LoginSenderAsync()` +4. Success ? `Navigation.NavigateTo("/", forceLoad: true)` +5. Error ? Display alert + +--- + +## Receiver Login + +**Route:** `/login/{EnvelopeKey}` +**File:** `ReceiverUI/Pages/LoginReceiver.razor` + +### AuthService Method +```csharp +public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error } + +public async Task LoginEnvelopeReceiverAsync(string key, string accessCode) { + var form = new MultipartFormDataContent(); + form.Add(new StringContent(accessCode), "AccessCode"); + + var response = await http.PostAsync( + $"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form); + + return response.StatusCode switch { + HttpStatusCode.OK => EnvelopeLoginResult.Success, + HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode, + HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound, + _ => EnvelopeLoginResult.Error + }; +} +``` + +**Success:** Redirect to `/envelope/{key}` + +--- + +## NuGet Packages (ReceiverUI) | Package | Version | Purpose | |---|---|---| -| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) | -| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF viewer (not used in EnvelopeViewer) | -| `DevExpress.Drawing.Skia` | 25.2.3 | Drawing backend | -| `itext` | 8.0.5 | PDF stamping (iText7) | -| `SkiaSharp.*` | 3.119.1 | WASM native rendering | +| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) | +| `SkiaSharp.*` | 3.119.1 | WASM rendering | +| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) | -**External CDN (EnvelopeViewer):** -- PDF.js 3.11.174 (via `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`) -- PDF.js Worker (`pdf.worker.min.js`) +**External CDN:** +- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` --- ## Mistakes History — Do NOT Repeat -| Mistake | Why Wrong | Session | -|---|---|---| -| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 | -| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 | -| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 | -| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — | -| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — | -| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 | -| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** | -| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** | -| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** | -| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** | -| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** | -| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** | +| Mistake | Why Wrong | +|---|---| +| Using iText7 in EnvelopeViewer | GPL license issue. Use overlay system instead. | +| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. | +| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. | +| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. | +| Over-designed UI (gradients/badges) | User prefers simple text labels. | +| Ignoring "revert" instructions | Revert HTML structure, not just CSS. | +| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. | +| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. | --- -## DevExpress Article (2023-08-28) — Why It Does NOT Apply +## Development Notes -The article describes **X.509 cryptographic digital signatures** via `PdfDocumentSigner` + `Pkcs7Signer`. -Our use case is **visual/image stamping** at specific page coordinates — different problem, different API. -`XRPdfSignature` in the article requires pre-placed fields in the report designer, not runtime coordinates. +### Deprecated Projects +**DO NOT USE:** +- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI +- PSPDFKit — Removed, use PDF.js + DevExpress instead + +### Legacy Projects (VB.NET) +**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests` + +### Signature Coordinate Evidence +**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET) + +```vb +Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches +Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches + +Sub LoadAnnotation(pElement As Signature, ...) + oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment + oAnnotation.Top = CSng(pElement.Y) +End Sub +``` + +Proves database uses INCHES natively. --- -## Layout Architecture (EnvelopeViewer) +## Quick Reference -### HTML Structure -```html -
-
- -
- -
- @if (_showThumbnails) { -
- -
-
- -
- } -
- -
-
-
-``` +### When working with coordinates: +1. **Database ? UI:** INCHES × 72 = PDF Points +2. **UI ? Display:** Points × scale = Pixels +3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up) -### CSS Flexbox Layout -```css -.pdf-frame { - display: flex; - flex-direction: row; /* Side-by-side */ - align-items: stretch; /* Same height */ - overflow: hidden; -} +### When adding features: +1. Check `Mistakes History` first +2. Prefer simplicity over complexity +3. Use `appsettings.json` for configuration +4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry) +5. **Unified frontend:** ReceiverUI serves both Senders and Receivers -.pdf-thumbnails { - flex-shrink: 0; /* Fixed width */ - width: 260px; /* Dynamic via inline style */ - border-right: none; /* Seamless join with splitter */ -} - -.pdf-splitter { - width: 4px; - cursor: col-resize; - flex-shrink: 0; -} - -.pdf-canvas-wrapper { - flex: 1; /* Fill remaining space */ - overflow: auto; /* Scrollable for zoom */ - padding: 2rem; - text-align: center; -} -``` - -### Resizable Splitter Workflow -``` -1. User hovers splitter ? cursor: col-resize (?) -2. Mouse down ? OnSplitterMouseDown(e) - - _isResizing = true - - Store start position (clientX) and width - - Add 'resizing' class to body (prevent text selection) - - Call pdfViewer.startResize() - -3. Mouse move (global) ? OnSplitterMouseMove(clientX) - - Calculate delta = clientX - startX - - newWidth = startWidth + delta - - Clamp to 150-400px range - - Update _thumbnailWidth - - StateHasChanged() for reactive UI - -4. Mouse up (global) ? OnSplitterMouseUp() - - _isResizing = false - - Remove 'resizing' class - - Save to localStorage("envelopeViewer_thumbnailWidth") -``` - -### Responsive Behavior -- **Desktop (>768px)**: Flex row, side-by-side -- **Mobile (?768px)**: Flex column, thumbnails on top -- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup +### When debugging: +1. **Coordinates:** Always check unit system (inches/points/pixels) +2. **Authentication:** Check cookie name/domain/SameSite +3. **Cache:** Check Redis/SQL connection + key format +4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated) --- -## Signature Buttons in EnvelopeViewer — Interactive Overlay System - -**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database. - -### Architecture - -**Blazor Component (`EnvelopeViewer.razor`):** -```csharp -IReadOnlyList _signatures = []; - -protected override async Task OnInitializedAsync() { - var signatures = await SignatureService.GetAsync(EnvelopeKey); - _signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS -} - -async Task RenderSignatureButtonsAsync() { - await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", - _signatures, _currentPage, _dotNetRef); -} - -[JSInvokable] -public void OnSignatureButtonClick(int signatureId) { - Console.WriteLine($"Signature #{signatureId} signed"); -} -``` - -**JavaScript (`pdf-viewer.js`):** -```javascript -renderSignatureButtons(signatures, currentPageNum, dotNetRef) { - this.clearSignatureButtons(); // Remove old buttons - - const pageSignatures = signatures.filter(sig => sig.page === currentPageNum); - const signatureLayer = document.getElementById('pdf-signature-layer'); - - pageSignatures.forEach(sig => { - // Convert POINTS to display pixels - const xPx = sig.x * this.scale; // sig.x already in PDF POINTS - const yPx = sig.y * this.scale; - - const button = document.createElement('button'); - button.className = 'signature-button'; - button.style.left = `${xPx}px`; - button.style.top = `${yPx}px`; - button.style.width = '150px'; - button.style.height = '60px'; - - // German text + pen icon - button.innerHTML = ` -
Unterschreiben
- ... - `; - - button.addEventListener('click', () => { - this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id); - }); - - signatureLayer.appendChild(button); - this.signatureButtons.push(button); - }); -} - -clearSignatureButtons() { - this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn)); - this.signatureButtons = []; -} -``` - -**HTML Structure:** -```html -
- -
-
-
-``` - -**CSS (`envelope-viewer.css`):** -```css -.pdf-signature-layer { - position: absolute; - left: 0; top: 0; right: 0; bottom: 0; - overflow: visible; - pointer-events: none; /* Pass clicks through to canvas */ - z-index: 20; -} - -.signature-button { - pointer-events: auto; /* Re-enable for buttons */ - position: absolute; - width: 150px; height: 60px; - background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - /* Hover: scale(1.05), shadow, darker gradient */ -} -``` - -### Rendering Triggers - -1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`) -2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()` -3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions) - -### Coordinate Conversion Flow - -``` -Database (INCHES) - ? SignatureService.GetAsync() -SignatureDto.X/Y (INCHES) - ? .Convert(UnitOfLength.Point) -SignatureDto.X/Y (PDF POINTS, × 72) - ? JavaScript -Display Pixels (× this.scale) - ? CSS -button.style.left/top -``` - -**Example Calculation:** -- Database: `X = 1.5 inches, Y = 2.0 inches` -- After conversion: `X = 108 points, Y = 144 points` (× 72) -- At scale 1.5: `X = 162px, Y = 216px` -- Button positioned at `left: 162px, top: 216px` - -### Button Design - -**Visual Spec:** -- **Size:** 150px × 60px -- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker) -- **Text:** "Unterschreiben" (18px, bold, white) -- **Icon:** Pen SVG (24px white) -- **Effects:** - - Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)` - - Active: `scale(0.98)` - - Focus: `2px solid #7e22ce` outline - -**Accessibility:** -- `tabindex="0"` for keyboard navigation -- Focus outline for keyboard users -- Click handler with semantic button element - ---- - -## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14) - -**IMPORTANT: iText7 NOT USED in EnvelopeViewer** -- **Reason:** GPL license incompatibility (requires source code sharing) -- **Alternative:** Client-side signature overlay system (HTML + Canvas API) -- **Export:** Signatures are visual overlays only, NOT stamped on PDF bytes -- **Future:** Consider PSPDFKit or commercial PDF library for actual PDF stamping - -### Signature Data Structure - -**Captured Signature (`SignatureCaptureDto`):** -```csharp -// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs -public sealed record SignatureCaptureDto -{ - public required string DataUrl { get; init; } // base64 PNG: "data:image/png;base64,iVBORw0KG..." - public required string FullName { get; init; } // Required: "Max Mustermann" - public string Position { get; init; } = string.Empty; // Optional: "Geschäftsführer" - public required string Place { get; init; } // Required: "Berlin" -} - -// Usage in components: -SignatureCaptureDto? _capturedSignature; - -// Initialization with object initializer (required properties): -_capturedSignature = new SignatureCaptureDto -{ - DataUrl = signatureDataUrl, - FullName = _signerFullName.Trim(), - Position = _signerPosition.Trim(), - Place = _signaturePlace.Trim() -}; -``` - -**Applied Signature (HTML Overlay):** -```html -
- -
-
- Max Mustermann -
Geschäftsführer -
Berlin, 26.01.2025 -
-
-``` - -### Complete Workflow (Session 14 Update) - -**Step 1: Page Load & Automatic Popup** -```csharp -protected override async Task OnInitializedAsync() { - // ... load PDF and signatures ... - - // Open signature popup automatically - _activeSignatureTab = SignatureTabDraw; - _signaturePopupVisible = true; - _popupValidationMessage = null; -} -``` - -**Features:** -- Opens automatically on page load (no manual trigger needed) -- Cannot be closed manually (no X button, ESC disabled, no outside-click) -- User MUST create signature before viewing PDF - -**Step 2: Signature Creation Popup (DxPopup)** - -**Tabs:** -1. **Zeichnen (Draw):** Canvas-based signature pad (`receiver-signature.js`) - - Touch-friendly: `touch-action: none` - - Line width: 2.5px, black (`#111`) - - Canvas: 560×180px, rounded corners, shadow - -2. **Text:** Type signature with font selection - - Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive - - Real-time preview on canvas - -3. **Bild (Image):** Upload PNG/JPG/WebP - - File input with preview - - Auto-resize to fit canvas - -**Required Fields:** -- ? **Vor- und Nachname** (Full Name) — Red asterisk `*` -- ? **Ort** (Place) — Red asterisk `*` -- ? **Position** — Optional, gray "(optional)" label - -**Validation:** -```csharp -async Task SaveSignatureAsync() { - if (string.IsNullOrWhiteSpace(_signerFullName)) { - _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; - return; - } - if (string.IsNullOrWhiteSpace(_signaturePlace)) { - _popupValidationMessage = "Bitte geben Sie den Ort ein."; - return; - } - var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); - if (string.IsNullOrWhiteSpace(signatureDataUrl)) { - _popupValidationMessage = "Die Unterschrift ist erforderlich."; - return; - } - - // Save to session state - _capturedSignature = new(signatureDataUrl, _signerFullName.Trim(), - _signerPosition.Trim(), _signaturePlace.Trim()); - _signaturePopupVisible = false; -} -``` - -**Design (Modern & Clean):** -- **Tabs:** Purple active state (`#4F46E5`), 3px bottom border -- **Inputs:** 2px solid border (`#e9ecef`), 6px border-radius, consistent padding -- **Canvas:** Light shadow (`0 1px 3px rgba(0,0,0,0.1)`), rounded corners -- **Buttons:** - - **Erneuern:** Outline secondary with refresh icon - - **Speichern:** Purple gradient + checkmark icon + shadow -- **Error:** Red left border (`4px solid #dc3545`), light red background (`#fee`) - -**Step 3: PDF Viewing with Signature Buttons** - -After popup closes: -```csharp -protected override async Task OnAfterRenderAsync(bool firstRender) { - // ... PDF initialization ... - - await RenderSignatureButtonsAsync(); // Render "Unterschreiben" buttons -} -``` - -**Buttons appear at signature field positions:** -- Purple gradient background -- "Unterschreiben" text + pen icon -- Hover: scale(1.05) + darker color -- Positioned using PDF POINTS ? display pixels conversion - -**Step 4: Apply Signature (Click "Unterschreiben")** - -**C# Handler:** -```csharp -[JSInvokable] -public async Task OnSignatureButtonClick(int signatureId) { - if (_capturedSignature == null) return; - - await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature", - signatureId, - _capturedSignature.DataUrl, - _capturedSignature.FullName, - _capturedSignature.Position, - _capturedSignature.Place); -} -``` - -**JavaScript Implementation:** -```javascript -async applySignature(signatureId, signatureDataUrl, fullName, position, place) { - // 1. Find and remove button - const button = this.signatureButtons.find(btn => - btn.getAttribute('data-signature-id') == signatureId); - button.parentNode.removeChild(button); - - // 2. Create signature container (German standard format) - const signatureContainer = document.createElement('div'); - signatureContainer.className = 'applied-signature'; - signatureContainer.style.position = 'absolute'; - signatureContainer.style.left = button.style.left; // Same position as button - signatureContainer.style.top = button.style.top; - signatureContainer.style.width = '230px'; - signatureContainer.style.backgroundColor = '#f8f9fa'; - signatureContainer.style.border = '1px solid #dee2e6'; - signatureContainer.style.borderRadius = '6px'; - signatureContainer.style.padding = '12px'; - - // 3. Add signature image - const img = document.createElement('img'); - img.src = signatureDataUrl; - img.style.width = '100%'; - img.style.maxHeight = '70px'; - img.style.objectFit = 'contain'; - - // 4. Add separator line (German standard) - const separator = document.createElement('div'); - separator.style.borderTop = '1px solid #495057'; - separator.style.marginTop = '6px'; - separator.style.marginBottom = '8px'; - - // 5. Add text information - const today = new Date(); - const dateStr = today.toLocaleDateString('de-DE', { - day: '2-digit', month: '2-digit', year: 'numeric' - }); // "26.01.2025" - - const infoHtml = [ - `${this.escapeHtml(fullName)}`, - position ? this.escapeHtml(position) : null, - `${this.escapeHtml(place)}, ${dateStr}` - ].filter(x => x).join('
'); - - const info = document.createElement('div'); - info.style.fontSize = '9px'; - info.style.color = '#495057'; - info.innerHTML = infoHtml; - - // 6. Assemble and add to layer - signatureContainer.appendChild(img); - signatureContainer.appendChild(separator); - signatureContainer.appendChild(info); - - document.getElementById('pdf-signature-layer').appendChild(signatureContainer); -} -``` - -**German Standard Layout:** -``` -??????????????????????????????? -? [Signature Image] ? ? Base64 PNG, max 70px height -? ? -??????????????????????????????? ? 1px separator (#495057) -? ? -? Max Mustermann (Bold) ? ? Name (font-weight: 600, #212529) -? Geschäftsführer ? ? Position (optional, normal weight) -? Berlin, 26.01.2025 ? ? Place, Date (dd.MM.yyyy) -? ? -??????????????????????????????? -``` - -**Step 5: Persistence & Re-rendering** - -- **Zoom/Page Change:** Applied signatures re-render automatically -- **Session State:** `_capturedSignature` stored in Blazor component -- **Limitation:** Lost on page refresh (no server-side storage) -- **Future:** Export to PDF with actual byte stamping (requires non-GPL library) - -**Security:** -```javascript -escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; // Browser auto-escapes - return div.innerHTML; -} -``` -Protects against XSS attacks from malicious input in Name/Position/Place fields. - ---- - -### Popup Design Specification - -**DxPopup Properties:** -```razor - - CloseOnOutsideClick="false" - ShowCloseButton="false" - CloseOnEscape="false"> -``` - -**Tab Design:** -- **Active Tab:** `border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;` -- **Inactive Tab:** `color: #6c757d;` -- **Tab Bar:** `border-bottom: 2px solid #e9ecef;` - -**Input Styling:** -```css -input, select { - border: 2px solid #e9ecef; - border-radius: 6px; - padding: 0.625rem; -} -``` - -**Canvas Styling:** -```css -canvas { - border: 2px solid #e9ecef; - border-radius: 8px; - background: white; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} -``` - -**Button Styling:** -```css -/* Erneuern (Renew) */ -.btn-outline-secondary { - border-radius: 6px; - padding: 0.625rem 1.25rem; - font-weight: 500; -} - -/* Speichern (Save) */ -.btn-primary { - background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); - border: none; - border-radius: 6px; - padding: 0.625rem 2rem; - font-weight: 600; - box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3); -} -``` - -**Future Enhancement Required:** -- Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion) -- Or use server-side stamping with Azure PDF Services -- Or accept GPL license and open-source the stamping code - ---- - -## 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 - ---- - - - - - - - - - - - +**Last Updated:** Session 17 (Architecture unification documentation)