diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/AnnotationDto.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/AnnotationDto.cs
new file mode 100644
index 00000000..2a27a2ea
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/AnnotationDto.cs
@@ -0,0 +1,32 @@
+namespace EnvelopeGenerator.WebUI.Client.Models;
+
+///
+/// Represents a pre-assigned signature annotation position on a specific page.
+///
+/// Coordinate unit (X, Y): Inches (GdPicture14 native unit),
+/// origin at the top-left corner of the page, both axes increase downward/rightward.
+///
+/// Conversion to DevExpress: Multiply by 100 (DX uses 1/100 inch).
+/// Convert: xDX = xInches * 100.0
+///
+/// Conversion to PDF Points: Multiply by 72 (1 inch = 72 points).
+/// Convert: xPt = xInches * 72.0
+///
+/// Y-axis for PDF (bottom-left origin): Flip required for iText7.
+/// Convert: yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0
+///
+[Obsolete("Use SignatureDto with SignatureService.")]
+public record AnnotationDto
+{
+ /// Unique identifier of the annotation.
+ public long Id { get; init; }
+
+ /// 1-based page number within the document.
+ public int Page { get; init; }
+
+ /// Horizontal position in INCHES from the left edge of the page.
+ public double X { get; init; }
+
+ /// Vertical position in INCHES from the top edge of the page.
+ public double Y { get; init; }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/SenderAppType.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/SenderAppType.cs
new file mode 100644
index 00000000..f8d0a2b9
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/SenderAppType.cs
@@ -0,0 +1,8 @@
+namespace EnvelopeGenerator.WebUI.Client.Models.Constants
+{
+ public enum SenderAppType
+ {
+ LegacyFormApp = 0,
+ ReceiverUIBlazorApp = 1
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/UnitOfLength.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/UnitOfLength.cs
new file mode 100644
index 00000000..ada84932
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/Constants/UnitOfLength.cs
@@ -0,0 +1,65 @@
+namespace EnvelopeGenerator.WebUI.Client.Models.Constants;
+
+///
+/// Represents the unit of measurement for coordinate values in signature positioning.
+/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7).
+///
+public enum UnitOfLength
+{
+ ///
+ /// Inch unit (1 inch = 25.4 mm).
+ /// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app).
+ /// Database stores all coordinates (X, Y, Width, Height) in INCHES.
+ ///
+ ///
+ /// Source: GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively.
+ ///
+ /// Evidence: VB.NET code directly assigns database values to annotation properties without conversion:
+ ///
+ /// oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES
+ /// oAnnotation.Top = CSng(pElement.Y)
+ ///
+ /// Standard Page Dimensions:
+ ///
+ /// - A4: 8.27" × 11.69" (210mm × 297mm)
+ /// - Letter: 8.5" × 11"
+ ///
+ ///
+ Inch = 0,
+
+ ///
+ /// PDF Point unit (1 point = 1/72 inch).
+ /// This is the standard unit used by PDF specification and PDF.js viewer.
+ ///
+ ///
+ /// Definition: According to PDF specification and Microsoft documentation:
+ ///
+ /// "PDF pages are sized in point units. 1 pt == 1/72 inch"
+ ///
+ /// Conversion Formula:
+ ///
+ /// points = inches * 72.0
+ /// inches = points / 72.0
+ ///
+ /// Important: Point ? Pixel!
+ ///
+ /// - Point (pt): Device-independent unit (always 1/72 inch)
+ /// - Pixel (px): Device-dependent unit (varies with screen DPI)
+ /// - At 72 DPI: 1 point = 1 pixel (coincidence)
+ /// - At 96 DPI: 1 point ? 1.33 pixels
+ /// - At 300 DPI: 1 point ? 4.17 pixels
+ ///
+ /// Standard Page Dimensions (in points):
+ ///
+ /// - A4: 595 × 842 points (8.27" × 11.69" × 72)
+ /// - Letter: 612 × 792 points (8.5" × 11" × 72)
+ ///
+ /// Usage in EnvelopeGenerator:
+ ///
+ /// - PDF.js viewer expects coordinates in points
+ /// - iText7 library uses points for PDF manipulation
+ /// - PSPDFKit (Web) uses points for annotation placement
+ ///
+ ///
+ Point
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/EnvelopeReceiverDto.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/EnvelopeReceiverDto.cs
new file mode 100644
index 00000000..718b9572
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/EnvelopeReceiverDto.cs
@@ -0,0 +1,105 @@
+namespace EnvelopeGenerator.WebUI.Client.Models;
+
+///
+/// Client-side model for the envelope receiver returned by
+/// GET api/EnvelopeReceiver/{envelopeKey}.
+///
+public record EnvelopeReceiverDto
+{
+ public int EnvelopeId { get; init; }
+ public int ReceiverId { get; init; }
+ public int Sequence { get; init; }
+
+ public string? Name { get; init; }
+ public string? JobTitle { get; init; }
+ public string? CompanyName { get; init; }
+ public string? PrivateMessage { get; init; }
+
+ public DateTime AddedWhen { get; init; }
+ public DateTime? ChangedWhen { get; init; }
+ public bool HasPhoneNumber { get; init; }
+
+ public EnvelopeClientDto? Envelope { get; init; }
+ public ReceiverClientDto? Receiver { get; init; }
+}
+
+///
+/// Client-side model for the envelope data embedded in .
+///
+public record EnvelopeClientDto
+{
+ public int Id { get; init; }
+ public int UserId { get; init; }
+ public int Status { get; init; }
+ public string StatusName { get; init; } = string.Empty;
+ public string Uuid { get; init; } = string.Empty;
+ public string Title { get; init; } = string.Empty;
+ public string Message { get; init; } = string.Empty;
+ public DateTime AddedWhen { get; init; }
+ public DateTime? ChangedWhen { get; init; }
+ public string Language { get; init; } = "de-DE";
+ public int? EnvelopeTypeId { get; init; }
+ public string? EnvelopeTypeTitle { get; init; }
+ public int? ContractType { get; init; }
+ public int? CertificationType { get; init; }
+ public bool UseAccessCode { get; init; }
+ public bool TFAEnabled { get; init; }
+ public IEnumerable? Documents { get; init; }
+ public EnvelopeSenderDto? User { get; init; }
+}
+
+///
+/// Sender (user) information embedded in .
+///
+public record EnvelopeSenderDto
+{
+ public int Id { get; init; }
+ public string? Username { get; init; }
+ public string? FullName { get; init; }
+ public string? Email { get; init; }
+}
+
+///
+/// Client-side model for a document embedded in .
+///
+public record DocumentClientDto
+{
+ public int Id { get; init; }
+ public int EnvelopeId { get; init; }
+ public DateTime AddedWhen { get; init; }
+ public IEnumerable? Elements { get; init; }
+}
+
+///
+/// Client-side model for a signature/annotation element embedded in .
+///
+public record SignatureClientDto
+{
+ public int Id { get; init; }
+ public int DocumentId { get; init; }
+ public int ReceiverId { get; init; }
+ public int ElementType { get; init; }
+ public double X { get; init; }
+ public double Y { get; init; }
+ public double Width { get; init; }
+ public double Height { get; init; }
+ public int Page { get; init; }
+ public bool Required { get; init; }
+ public string? Tooltip { get; init; }
+ public bool ReadOnly { get; init; }
+ public int AnnotationIndex { get; init; }
+ public DateTime AddedWhen { get; init; }
+ public DateTime? ChangedWhen { get; init; }
+}
+
+///
+/// Client-side model for the receiver data embedded in .
+///
+public record ReceiverClientDto
+{
+ public int Id { get; init; }
+ public string? EmailAddress { get; init; }
+ public string? Signature { get; init; }
+ public DateTime AddedWhen { get; init; }
+ public DateTime? TfaRegDeadline { get; init; }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureCaptureDto.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureCaptureDto.cs
new file mode 100644
index 00000000..6e528b06
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureCaptureDto.cs
@@ -0,0 +1,61 @@
+namespace EnvelopeGenerator.WebUI.Client.Models;
+
+///
+/// Represents a captured signature with metadata created by the receiver in the signature popup.
+/// This model holds the signature image (as base64 data URL) along with signer information
+/// used for rendering applied signatures on the PDF canvas.
+///
+///
+/// Used in: EnvelopeViewer.razor signature popup workflow
+///
+/// Creation: User draws/types/uploads signature and fills required fields
+///
+/// Storage: Session-only (Blazor component state, lost on page refresh)
+///
+/// Rendering: Applied signatures display: Image + Separator + Name/Position/Place/Date
+///
+public sealed record SignatureCaptureDto
+{
+ ///
+ /// Base64-encoded data URL of the signature image.
+ ///
+ /// Format: data:image/png;base64,iVBORw0KG...
+ ///
+ /// Source: Canvas.toDataURL() from signature pad (draw/text/image tabs)
+ ///
+ /// Usage: Set as img.src in applied signature overlay
+ ///
+ public required string DataUrl { get; init; }
+
+ ///
+ /// Full name of the signer (first and last name).
+ ///
+ /// Required: Yes (validated in popup)
+ /// Display: Bold text in applied signature block
+ ///
+ /// Example: "Max Mustermann"
+ ///
+ public required string FullName { get; init; }
+
+ ///
+ /// Job title or position of the signer.
+ ///
+ /// Required: No (optional field)
+ ///
+ /// Display: Normal weight text between name and place/date
+ ///
+ /// Example: "Geschäftsführer" or empty string
+ ///
+ public string Position { get; init; } = string.Empty;
+
+ ///
+ /// Location/place where the signature was created.
+ ///
+ /// Required: Yes (validated in popup)
+ ///
+ /// Display: Shown with current date in German format (dd.MM.yyyy)
+ ///
+ /// Example: "Berlin" ? rendered as "Berlin, 26.01.2025"
+ ///
+ public required string Place { get; init; }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureDto.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureDto.cs
new file mode 100644
index 00000000..cb27642d
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Models/SignatureDto.cs
@@ -0,0 +1,101 @@
+using EnvelopeGenerator.WebUI.Client.Models.Constants;
+
+namespace EnvelopeGenerator.WebUI.Client.Models;
+
+///
+/// Represents a signature position on a PDF page.
+/// Coordinates stored in INCHES (GdPicture14 native unit).
+/// Origin: Top-left corner, X increases right, Y increases down.
+///
+public class SignatureDto
+{
+ /// Unique identifier.
+ public int Id { get; init; }
+
+ private double _x;
+ private double _y;
+
+ /// Horizontal position in INCHES from left edge.
+ public double X
+ {
+ get => _x * Factor;
+ init => _x = value;
+ }
+
+ /// Vertical position in INCHES from top edge.
+ public double Y
+ {
+ get => _y * Factor;
+ init => _y = value;
+ }
+
+ /// 1-based page number.
+ public int Page { get; init; }
+
+ /// Sender application type that created this signature.
+ public SenderAppType SenderAppType { get; init; }
+
+ private UnitOfLength _unitOfLength;
+
+ public SignatureDto Convert(UnitOfLength unitOfLength)
+ {
+ _unitOfLength = unitOfLength;
+ return this;
+ }
+
+ public double Factor
+ {
+ get
+ {
+ if (SenderAppType != SenderAppType.LegacyFormApp)
+ {
+ throw new NotImplementedException(
+ $"SenderAppType '{SenderAppType}' is not yet implemented. " +
+ $"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " +
+ $"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types.");
+ }
+
+ // LegacyFormApp uses GdPicture14 with INCHES
+ return _unitOfLength switch
+ {
+ UnitOfLength.Inch => 1.0, // No conversion needed: INCHES ? INCHES
+ UnitOfLength.Point => 72.0, // INCHES ? PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!)
+ _ => throw new InvalidOperationException(
+ $"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.")
+ };
+ }
+ }
+}
+
+public static class SignatureDtoExtensions
+{
+ ///
+ /// Converts all signatures in the collection to the specified unit of length.
+ ///
+ /// Type of the collection (IEnumerable, List, etc.)
+ /// Collection of SignatureDto objects to convert.
+ /// Target unit of measurement (Inch or Point).
+ /// The same collection with all signatures converted to the specified unit.
+ /// Thrown when signatures collection is null.
+ ///
+ /// Usage:
+ ///
+ /// var signatures = await SignatureService.GetAsync(envelopeKey);
+ /// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point);
+ ///
+ /// Note: This method modifies each SignatureDto object in place and returns the same collection.
+ ///
+ public static T Convert(this T signatures, UnitOfLength unitOfLength)
+ where T : IEnumerable
+ {
+ if (signatures == null)
+ throw new ArgumentNullException(nameof(signatures));
+
+ foreach (var signature in signatures)
+ {
+ signature.Convert(unitOfLength);
+ }
+
+ return signatures;
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/ApiOptions.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/ApiOptions.cs
new file mode 100644
index 00000000..cbed99b9
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/ApiOptions.cs
@@ -0,0 +1,10 @@
+namespace EnvelopeGenerator.WebUI.Client.Options;
+
+public class ApiOptions
+{
+ public const string SectionName = "ApiOptions";
+
+ public string BaseUrl { get; set; } = string.Empty;
+
+ public bool UsePredefinedReports { get; set; } = false;
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/PdfViewerOptions.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/PdfViewerOptions.cs
new file mode 100644
index 00000000..421a001b
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Options/PdfViewerOptions.cs
@@ -0,0 +1,71 @@
+namespace EnvelopeGenerator.WebUI.Client.Options;
+
+public class PdfViewerOptions
+{
+ public const string SectionName = "PdfViewerOptions";
+
+ ///
+ /// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
+ /// Higher values = better quality but slower rendering
+ /// Default: 0.75
+ ///
+ public double ThumbnailBaseScale { get; set; } = 0.75;
+
+ ///
+ /// Enable HiDPI/Retina support for thumbnails
+ /// Default: true
+ ///
+ public bool ThumbnailEnableHiDPI { get; set; } = true;
+
+ ///
+ /// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
+ /// Caps DPR to avoid excessive memory usage on 4K+ displays
+ /// Default: 2.0
+ ///
+ public double ThumbnailMaxDPR { get; set; } = 2.0;
+
+ ///
+ /// Enable HiDPI/Retina support for main PDF canvas
+ /// Default: true
+ ///
+ public bool MainCanvasEnableHiDPI { get; set; } = true;
+
+ ///
+ /// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
+ /// Default: 2.0
+ ///
+ public double MainCanvasMaxDPR { get; set; } = 2.0;
+
+ ///
+ /// Enable smooth zoom transition (fade effect)
+ /// Default: true
+ ///
+ public bool EnableSmoothZoom { get; set; } = true;
+
+ ///
+ /// Zoom transition duration in milliseconds (50 - 500)
+ /// Default: 150
+ ///
+ public int ZoomTransitionDuration { get; set; } = 150;
+
+ ///
+ /// Opacity during rendering (0.0 - 1.0)
+ /// Lower values = more visible fade effect
+ /// Default: 0.85
+ ///
+ public double RenderingOpacity { get; set; } = 0.85;
+
+ ///
+ /// Delay between thumbnail renders in milliseconds (10 - 200)
+ /// Higher values = less browser stress, slower initial load
+ /// Default: 50
+ ///
+ public int ThumbnailRenderDelay { get; set; } = 50;
+
+ ///
+ /// Zoom step percentage (1 - 50)
+ /// Controls how much zoom changes per click or scroll
+ /// Default: 5 (5% per step)
+ ///
+ public int ZoomStepPercentage { get; set; } = 5;
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AnnotationService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AnnotationService.cs
new file mode 100644
index 00000000..b70e9db2
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AnnotationService.cs
@@ -0,0 +1,33 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using EnvelopeGenerator.WebUI.Client.Models;
+using EnvelopeGenerator.WebUI.Client.Options;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+///
+/// Retrieves annotation positions from the API.
+/// The URL is composed as {BaseUrl}/api/Annotation/{envelopeKey}.
+/// During development, BaseUrl is empty so the request resolves to the
+/// YARP-proxied route on the same origin, which currently serves
+/// fake-data/annotations.json. To switch to real data, update the
+/// YARP route in yarp.json – no code change required.
+///
+[Obsolete("Use SignatureService.")]
+public class AnnotationService(HttpClient http, IOptions apiOptions)
+{
+ private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
+
+ public async Task> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
+ var response = await http.GetAsync(url, cancel);
+
+ if (!response.IsSuccessStatusCode)
+ return [];
+
+ var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel);
+ return result ?? [];
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AppVersionService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AppVersionService.cs
new file mode 100644
index 00000000..7d18243d
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AppVersionService.cs
@@ -0,0 +1,26 @@
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+///
+/// Provides application version for cache busting static assets.
+/// Version is automatically incremented on each build via AssemblyVersion.
+///
+public class AppVersionService
+{
+ ///
+ /// Current application version (e.g., "1.0.0.0")
+ ///
+ public string Version { get; }
+
+ public AppVersionService()
+ {
+ // Get version from assembly metadata
+ Version = typeof(AppVersionService).Assembly.GetName().Version?.ToString() ?? "1.0.0.0";
+ }
+
+ ///
+ /// Generates versioned URL for static assets (cache busting)
+ ///
+ /// Asset path (e.g., "css/envelope-viewer.css")
+ /// Versioned URL (e.g., "css/envelope-viewer.css?v=1.0.0.0")
+ public string GetVersionedUrl(string path) => $"{path}?v={Version}";
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AuthService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AuthService.cs
new file mode 100644
index 00000000..6e1af9e6
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/AuthService.cs
@@ -0,0 +1,81 @@
+using System.Net;
+using System.Net.Http.Json;
+using EnvelopeGenerator.WebUI.Client.Options;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
+
+public enum SenderLoginResult { Success, InvalidCredentials, Error }
+
+public class AuthService(HttpClient http, IOptions apiOptions)
+{
+ private readonly ApiOptions _api = apiOptions.Value;
+
+ ///
+ /// Checks whether the current user holds a valid receiver token for the given envelope key.
+ /// Calls GET /api/auth/check/envelope/{envelopeKey}.
+ ///
+ public async Task CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
+ return response.StatusCode == HttpStatusCode.OK;
+ }
+
+ ///
+ /// Submits the access code for the given envelope key.
+ /// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data.
+ /// On success the API sets an authentication cookie automatically.
+ ///
+ public async Task LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
+ {
+ var form = new MultipartFormDataContent();
+ form.Add(new StringContent(accessCode), "AccessCode");
+
+ var response = await http.PostAsync(
+ $"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
+ form, cancel);
+
+ return response.StatusCode switch
+ {
+ HttpStatusCode.OK => EnvelopeLoginResult.Success,
+ HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
+ HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
+ _ => EnvelopeLoginResult.Error
+ };
+ }
+
+ ///
+ /// Removes the per-envelope receiver cookie for the given envelope key.
+ /// Calls POST /api/auth/logout/envelope/{envelopeKey}.
+ ///
+ public async Task LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var response = await http.PostAsync(
+ $"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
+ null, cancel);
+ return response.IsSuccessStatusCode;
+ }
+
+ ///
+ /// Authenticates a sender user with username and password.
+ /// Calls POST /api/auth?cookie=true with JSON body.
+ /// On success the API sets an authentication cookie automatically.
+ ///
+ public async Task LoginSenderAsync(string username, string password, CancellationToken cancel = default)
+ {
+ var requestBody = new { username, password };
+
+ var response = await http.PostAsJsonAsync(
+ $"{_api.BaseUrl}/api/auth?cookie=true",
+ requestBody, cancel);
+
+ return response.StatusCode switch
+ {
+ HttpStatusCode.OK => SenderLoginResult.Success,
+ HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
+ _ => SenderLoginResult.Error
+ };
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs
new file mode 100644
index 00000000..8812b0c1
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs
@@ -0,0 +1,40 @@
+using DevExpress.DataAccess.Json;
+using DevExpress.DataAccess.Web;
+using DevExpress.DataAccess.Wizard.Services;
+
+namespace EnvelopeGenerator.WebUI.Client.Services
+{
+ public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
+ {
+ public static JsonDataConnection GetDefaultConnection() {
+ var uriJsonSource = new UriJsonSource() {
+ Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
+ };
+ return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
+ }
+ public static List GetConnections() {
+ var connections = new List {
+ GetDefaultConnection()
+ };
+ return connections;
+ }
+
+ bool IJsonConnectionStorageService.CanSaveConnection => false;
+ bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
+ return GetConnections().Any(x => x.Name == connectionName);
+ }
+
+ IEnumerable IJsonConnectionStorageService.GetConnections() {
+ return GetConnections();
+ }
+
+ JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
+ var connection = GetConnections().FirstOrDefault(x => x.Name == name);
+ if(connection == null)
+ throw new InvalidOperationException();
+ return connection;
+ }
+
+ void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomJsonDataConnectionProviderFactory.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomJsonDataConnectionProviderFactory.cs
new file mode 100644
index 00000000..bbe9be74
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomJsonDataConnectionProviderFactory.cs
@@ -0,0 +1,24 @@
+using DevExpress.DataAccess.Json;
+using DevExpress.DataAccess.Web;
+namespace EnvelopeGenerator.WebUI.Client.Services
+{
+ public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
+ public IJsonDataConnectionProviderService Create() {
+ return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections());
+ }
+ }
+
+ public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService
+ {
+ readonly List jsonDataConnections;
+ public WebDocumentViewerJsonDataConnectionProvider(List jsonDataConnections) {
+ this.jsonDataConnections = jsonDataConnections;
+ }
+ public JsonDataConnection GetJsonDataConnection(string name) {
+ var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name);
+ if(connection == null)
+ throw new InvalidOperationException();
+ return connection;
+ }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomReportProvider.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomReportProvider.cs
new file mode 100644
index 00000000..a32769e6
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/CustomReportProvider.cs
@@ -0,0 +1,21 @@
+using DevExpress.XtraReports.UI;
+using DevExpress.XtraReports.Services;
+using EnvelopeGenerator.WebUI.Client.PredefinedReports;
+
+namespace EnvelopeGenerator.WebUI.Client.Services
+{
+ public class CustomReportProvider : IReportProviderAsync {
+ private readonly InMemoryReportStorageWebExtension reportStorage;
+
+ public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) {
+ this.reportStorage = reportStorage;
+ }
+
+ public Task GetReportAsync(string id, ReportProviderContext context) {
+ if(reportStorage.TryGetReport(id, out var savedReport))
+ return Task.FromResult(savedReport);
+
+ return Task.FromResult(ReportsFactory.GetReport(id));
+ }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/DocumentService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/DocumentService.cs
new file mode 100644
index 00000000..b9cf4456
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/DocumentService.cs
@@ -0,0 +1,34 @@
+using System.Net;
+using System.Net.Http;
+using Microsoft.Extensions.Options;
+using EnvelopeGenerator.WebUI.Client.Options;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+public class DocumentService(HttpClient http, IOptions apiOptions)
+{
+ private readonly ApiOptions _api = apiOptions.Value;
+
+ ///
+ /// Fetches the PDF bytes for the given envelope key from the API.
+ /// Throws HttpRequestException on failure with appropriate status code.
+ ///
+ /// Thrown when the API request fails.
+ public async Task GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var statusCode = (int)response.StatusCode;
+ var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
+ throw new HttpRequestException(
+ $"Failed to load document. Status: {statusCode} ({reasonPhrase})",
+ null,
+ response.StatusCode);
+ }
+
+ var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
+ return bytes;
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/EnvelopeReceiverService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/EnvelopeReceiverService.cs
new file mode 100644
index 00000000..0abfc7a9
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/EnvelopeReceiverService.cs
@@ -0,0 +1,40 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+using EnvelopeGenerator.WebUI.Client.Models;
+using EnvelopeGenerator.WebUI.Client.Options;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+///
+/// Retrieves the for the authenticated receiver
+/// from GET api/EnvelopeReceiver/{envelopeKey}.
+///
+public class EnvelopeReceiverService(HttpClient http, IOptions apiOptions)
+{
+ private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
+
+ ///
+ /// Fetches the envelope receiver data for the given envelope key from the API.
+ /// Throws HttpRequestException on failure with appropriate status code.
+ ///
+ /// Thrown when the API request fails.
+ public async Task GetAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
+ var response = await http.GetAsync(url, cancel);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var statusCode = (int)response.StatusCode;
+ var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
+ throw new HttpRequestException(
+ $"Failed to load envelope receiver data. Status: {statusCode} ({reasonPhrase})",
+ null,
+ response.StatusCode);
+ }
+
+ return await response.Content.ReadFromJsonAsync(_jsonOptions, cancel);
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/FontLoader.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/FontLoader.cs
new file mode 100644
index 00000000..aa48b9bb
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/FontLoader.cs
@@ -0,0 +1,12 @@
+using DevExpress.Drawing;
+
+namespace EnvelopeGenerator.WebUI.Client.Services {
+ public static class FontLoader {
+ public async static Task LoadFonts(HttpClient httpClient, List fontNames) {
+ foreach(var fontName in fontNames) {
+ var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
+ DXFontRepository.Instance.AddFont(fontBytes);
+ }
+ }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/InMemoryReportStorageWebExtension.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/InMemoryReportStorageWebExtension.cs
new file mode 100644
index 00000000..c782432f
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/InMemoryReportStorageWebExtension.cs
@@ -0,0 +1,83 @@
+using DevExpress.XtraReports.UI;
+using DevExpress.XtraReports.Web.Extensions;
+using EnvelopeGenerator.WebUI.Client.PredefinedReports;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+public class InMemoryReportStorageWebExtension : ReportStorageWebExtension
+{
+ private const string DefaultReportName = "LargeDatasetReport";
+ private static readonly Dictionary Reports = new(StringComparer.OrdinalIgnoreCase);
+
+ public override bool CanSetData(string url) => IsValidUrl(url);
+
+ public override byte[] GetData(string url)
+ {
+ url = NormalizeUrl(url);
+
+ if (Reports.TryGetValue(url, out var reportLayout))
+ return reportLayout;
+
+ if (ReportsFactory.Reports.TryGetValue(url, out var reportFactory))
+ return SaveReport(reportFactory());
+
+ throw new DevExpress.XtraReports.Web.ClientControls.FaultException($"Report '{url}' was not found.");
+ }
+
+ public override Dictionary GetUrls()
+ {
+ var urls = ReportsFactory.Reports.Keys
+ .Concat(Reports.Keys)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(name => name, name => name, StringComparer.OrdinalIgnoreCase);
+
+ return urls;
+ }
+
+ public override bool IsValidUrl(string url)
+ {
+ return !string.IsNullOrWhiteSpace(url)
+ && url.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
+ }
+
+ public override void SetData(XtraReport report, string url)
+ {
+ url = NormalizeUrl(url);
+ Reports[url] = SaveReport(report);
+ }
+
+ public override string SetNewData(XtraReport report, string defaultUrl)
+ {
+ var url = NormalizeUrl(defaultUrl);
+ Reports[url] = SaveReport(report);
+ return url;
+ }
+
+ public bool TryGetReport(string url, out XtraReport report)
+ {
+ url = NormalizeUrl(url);
+
+ if (!Reports.ContainsKey(url))
+ {
+ report = null!;
+ return false;
+ }
+
+ using var stream = new MemoryStream(Reports[url]);
+ report = XtraReport.FromXmlStream(stream, true);
+ report.Name = url;
+ return true;
+ }
+
+ private static string NormalizeUrl(string url)
+ {
+ return string.IsNullOrWhiteSpace(url) ? DefaultReportName : url;
+ }
+
+ private static byte[] SaveReport(XtraReport report)
+ {
+ using var stream = new MemoryStream();
+ report.SaveLayoutToXml(stream);
+ return stream.ToArray();
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs
new file mode 100644
index 00000000..4aba423d
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs
@@ -0,0 +1,9 @@
+using DevExpress.DataAccess.Web;
+
+namespace EnvelopeGenerator.WebUI.Client.Services {
+ public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
+ public IEnumerable GetAvailableTypes(string context) {
+ return new[] { typeof(Data.DataItemList) };
+ }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureCacheService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureCacheService.cs
new file mode 100644
index 00000000..1421ae9c
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureCacheService.cs
@@ -0,0 +1,66 @@
+using System.Net.Http.Json;
+using Microsoft.Extensions.Options;
+using EnvelopeGenerator.WebUI.Client.Options;
+using EnvelopeGenerator.WebUI.Client.Models;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+///
+/// Client service for managing cached signatures via API.
+///
+public class SignatureCacheService(HttpClient http, IOptions apiOptions)
+{
+ private readonly ApiOptions _api = apiOptions.Value;
+
+ public async Task SaveSignatureAsync(
+ string envelopeKey,
+ SignatureCaptureDto signature,
+ CancellationToken cancel = default)
+ {
+ var response = await http.PostAsJsonAsync(
+ $"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
+ signature,
+ cancel);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync(cancel);
+ throw new HttpRequestException($"Failed to cache signature: {response.StatusCode} - {error}");
+ }
+ }
+
+ public async Task GetSignatureAsync(
+ string envelopeKey,
+ CancellationToken cancel = default)
+ {
+ var response = await http.GetAsync(
+ $"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
+ cancel);
+
+ if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
+ return null;
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync(cancel);
+ throw new HttpRequestException($"Failed to retrieve signature: {response.StatusCode} - {error}");
+ }
+
+ return await response.Content.ReadFromJsonAsync(cancellationToken: cancel);
+ }
+
+ public async Task DeleteSignatureAsync(
+ string envelopeKey,
+ CancellationToken cancel = default)
+ {
+ var response = await http.DeleteAsync(
+ $"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
+ cancel);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync(cancel);
+ throw new HttpRequestException($"Failed to delete signature: {response.StatusCode} - {error}");
+ }
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureService.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureService.cs
new file mode 100644
index 00000000..0ee94bba
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI.Client/Services/SignatureService.cs
@@ -0,0 +1,24 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using EnvelopeGenerator.WebUI.Client.Models;
+using EnvelopeGenerator.WebUI.Client.Options;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.WebUI.Client.Services;
+
+public class SignatureService(HttpClient http, IOptions apiOptions)
+{
+ private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
+
+ public async Task> GetAsync(string envelopeKey, CancellationToken cancel = default)
+ {
+ var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
+ var response = await http.GetAsync(url, cancel);
+
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException($"Failed to retrieve signatures for envelope {envelopeKey}: {response.StatusCode} {response.ReasonPhrase}");
+
+ var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel);
+ return result ?? [];
+ }
+}