diff --git a/EnvelopeGenerator.API/Controllers/AnnotationController.cs b/EnvelopeGenerator.API/Controllers/AnnotationController.cs
index be40fb0d..c1961687 100644
--- a/EnvelopeGenerator.API/Controllers/AnnotationController.cs
+++ b/EnvelopeGenerator.API/Controllers/AnnotationController.cs
@@ -1,5 +1,6 @@
using DigitalData.Core.Abstraction.Application.DTO;
using DigitalData.Core.Exceptions;
+using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
@@ -7,11 +8,13 @@ using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.API.Extensions;
+using EnvelopeGenerator.API.Models;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using System.Dynamic;
namespace EnvelopeGenerator.API.Controllers;
@@ -115,4 +118,127 @@ public class AnnotationController : ControllerBase
_logger.LogNotice(histRes.Notices);
return StatusCode(500, histRes.Messages);
}
+
+ ///
+ /// Returns the signature placeholders (id, page, x, y, width, height)
+ /// the authenticated receiver must sign on the current envelope.
+ /// Used by the Blazor receiver UI to render a custom signature overlay
+ /// on top of the DevExpress PDF viewer.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpGet("elements")]
+ public async Task GetElements(CancellationToken cancel)
+ {
+ var signature = User.GetReceiverSignatureOfReceiver();
+ var uuid = User.GetEnvelopeUuidOfReceiver();
+
+ var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
+ ?? throw new NotFoundException("Envelope receiver is not found.");
+
+ var elements = envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements
+ ?? Enumerable.Empty();
+
+ // Only expose what the overlay needs (no internal columns).
+ var payload = elements
+ .Where(e => e.ReceiverId == envelopeReceiver.ReceiverId)
+ .Select(e => new
+ {
+ id = e.Id,
+ page = e.Page,
+ x = e.X,
+ y = e.Y,
+ width = e.Width,
+ height = e.Height,
+ required = e.Required,
+ tooltip = e.Tooltip,
+ });
+
+ return Ok(payload);
+ }
+
+ ///
+ /// Signs the document for the authenticated receiver using a Blazor /
+ /// DevExpress friendly payload (one PNG image per placeholder, plus
+ /// optional position / city per placeholder).
+ ///
+ /// Internally produces a wrapping a
+ /// list of entries so that the
+ /// existing notification pipeline (mail / status / annotation persistence)
+ /// is fully reused. The Instant JSON is left blank because the Blazor
+ /// pipeline does not depend on PSPDFKit-side rendering.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost("blazor")]
+ public async Task SignBlazor([FromBody] BlazorSignaturePayload payload, CancellationToken cancel)
+ {
+ if (payload is null || payload.Signatures.Count == 0)
+ return BadRequest();
+
+ var signature = User.GetReceiverSignatureOfReceiver();
+ var uuid = User.GetEnvelopeUuidOfReceiver();
+
+ var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
+ ?? throw new NotFoundException("Envelope receiver is not found.");
+
+ if (await _mediator.IsSignedAsync(uuid, signature, cancel))
+ return Problem(statusCode: StatusCodes.Status409Conflict);
+
+ if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
+ return Problem(statusCode: StatusCodes.Status423Locked);
+
+ // Build a structured AnnotationCreateDto list out of the lightweight
+ // payload. One DTO per (element, kind) tuple, mirroring how the legacy
+ // PSPDFKit pipeline produced multiple form fields per placeholder.
+ var structured = new List();
+ foreach (var entry in payload.Signatures)
+ {
+ structured.Add(new AnnotationCreateDto
+ {
+ ElementId = entry.ElementId,
+ Name = $"{entry.ElementId}#signature",
+ Type = "signature",
+ Value = entry.SignatureDataUrl,
+ });
+ if (!string.IsNullOrWhiteSpace(entry.Position))
+ {
+ structured.Add(new AnnotationCreateDto
+ {
+ ElementId = entry.ElementId,
+ Name = $"{entry.ElementId}#position",
+ Type = "position",
+ Value = entry.Position!,
+ });
+ }
+ if (!string.IsNullOrWhiteSpace(entry.City))
+ {
+ structured.Add(new AnnotationCreateDto
+ {
+ ElementId = entry.ElementId,
+ Name = $"{entry.ElementId}#city",
+ Type = "city",
+ Value = entry.City!,
+ });
+ }
+ structured.Add(new AnnotationCreateDto
+ {
+ ElementId = entry.ElementId,
+ Name = $"{entry.ElementId}#date",
+ Type = "date",
+ Value = entry.SignedAt.ToString("o"),
+ });
+ }
+
+ // No PSPDFKit Instant JSON for the Blazor flow — pass an empty object.
+ var psPdfKitAnnotation = new PsPdfKitAnnotation(new ExpandoObject(), structured);
+
+ var docSignedNotification = await _mediator
+ .ReadEnvelopeReceiverAsync(uuid, signature, cancel)
+ .ToDocSignedNotification(psPdfKitAnnotation)
+ ?? throw new NotFoundException("Envelope receiver is not found.");
+
+ await _mediator.PublishSafely(docSignedNotification, cancel);
+ await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+
+ return Ok();
+ }
}