feat(sender-editor): consume bootstrap payload and auto-save draft

Accept bootstrapToken query parameter and consume one-time payload from sessionStorage-backed service.

Hydrate editor state with title, message, envelope type, receivers, and PDF document from bootstrap data.

Refactor draft save into reusable core flow and trigger non-navigating auto-save after bootstrap is applied.

Prevent stale token replay by clearing query token from URL and handling invalid tokens gracefully.
This commit is contained in:
2026-10-05 14:54:06 +02:00
parent 715add7f51
commit bb35a68b1f

View File

@@ -21,6 +21,7 @@
@inject ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer
@inject IStringLocalizer<Model> ModelLocalizer
@@ -589,6 +590,9 @@
[SupplyParameterFromQuery(Name = "envelopeId")]
public int? EnvelopeId { get; set; }
[SupplyParameterFromQuery(Name = "bootstrapToken")]
public string? BootstrapToken { get; set; }
// ── Constants ──
// Signature field size in PDF points (fixed): 1.77" × 1.96"
const double SigWidthPt = 1.77 * 72; // 127.44 pt
@@ -609,6 +613,8 @@
int? _loadedEnvelopeId;
int? _editingEnvelopeId;
bool _isLoadingEnvelopeForEdit;
bool _bootstrapApplied;
string? _pendingBootstrapToken;
List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
@@ -629,6 +635,7 @@
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
string _saveSuccessBody = string.Empty;
bool _saveNavigateToDashboard = false;
bool _saveDraftAfterBootstrap;
// ── Envelope metadata ──
string _envelopeTitle = string.Empty;
@@ -697,6 +704,9 @@
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
_editingEnvelopeId = EnvelopeId.Value;
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(BootstrapToken))
_pendingBootstrapToken = BootstrapToken;
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
{
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
@@ -707,15 +717,91 @@
return;
}
protected override Task OnAfterRenderAsync(bool firstRender)
async Task TryApplyBootstrapAsync(string token)
{
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
_bootstrapApplied = true;
if (payload is null)
{
NavigationManager.NavigateTo("/sender/editor", replace: true);
return;
}
_envelopeTitle = payload.Title ?? string.Empty;
_envelopeMessage = payload.Message ?? string.Empty;
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
_receivers = payload.Receivers
.Select((receiver, index) => new ReceiverDraft(
Guid.NewGuid(),
receiver.FullName,
receiver.EmailAddress,
string.Empty,
ReceiverPalette[index % ReceiverPalette.Length]))
.ToList();
_originalPdfBytes = payload.DocumentBytes;
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
_signatureFields.Clear();
_pendingReceiverForPlacement = null;
_saveDraftAfterBootstrap = true;
if (_pdfLoaded && _originalPdfBytes is not null)
{
try
{
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
.OrderBy(p => p.PageNumber)
.ToList();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
_pagePreviews = [];
}
_pageCount = _pagePreviews.Count > 0
? _pagePreviews.Count
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
}
else
{
_pagePreviews = [];
_pageCount = 0;
}
NavigationManager.NavigateTo("/sender/editor", replace: true);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_isPageReady = true;
StateHasChanged();
await InvokeAsync(StateHasChanged);
}
return Task.CompletedTask;
if (!OperatingSystem.IsBrowser())
return;
if (_saveDraftAfterBootstrap && !_isSaving)
{
_saveDraftAfterBootstrap = false;
await SaveDraftAndStayOnEditorAsync();
return;
}
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
{
var token = _pendingBootstrapToken;
_pendingBootstrapToken = null;
await TryApplyBootstrapAsync(token);
}
}
async Task LoadEnvelopeForEditAsync(int envelopeId)
@@ -1108,12 +1194,17 @@
}
async Task SaveDraftAsync()
{
await SaveDraftCoreAsync(navigateToDashboard: true);
}
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
{
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
return null;
}
_isSaving = true;
@@ -1134,25 +1225,37 @@
_loadedEnvelopeId = result.Id;
_editingEnvelopeId = result.Id;
if (previousEnvelopeId != result.Id)
if (navigateToDashboard)
{
NavigationManager.NavigateTo("/sender", forceLoad: false);
}
else if (previousEnvelopeId != result.Id)
{
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
}
NavigationManager.NavigateTo("/sender", forceLoad: false);
return result.Id;
}
catch (Exception ex)
{
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
_saveErrorMessage = ex.Message;
_savePopupVisible = true;
return null;
}
finally
{
_isSaving = false;
await InvokeAsync(StateHasChanged);
}
}
async Task SaveDraftAndStayOnEditorAsync()
{
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
await SaveDraftCoreAsync(navigateToDashboard: false);
}
// ── Send envelope ──
async Task SendAsync()
{