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 ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService @inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService @inject EnvelopeTypeService EnvelopeTypeService
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject AuthService AuthService @inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer @inject IStringLocalizer<Resource> UiLocalizer
@inject IStringLocalizer<Model> ModelLocalizer @inject IStringLocalizer<Model> ModelLocalizer
@@ -589,6 +590,9 @@
[SupplyParameterFromQuery(Name = "envelopeId")] [SupplyParameterFromQuery(Name = "envelopeId")]
public int? EnvelopeId { get; set; } public int? EnvelopeId { get; set; }
[SupplyParameterFromQuery(Name = "bootstrapToken")]
public string? BootstrapToken { get; set; }
// ── Constants ── // ── Constants ──
// Signature field size in PDF points (fixed): 1.77" × 1.96" // Signature field size in PDF points (fixed): 1.77" × 1.96"
const double SigWidthPt = 1.77 * 72; // 127.44 pt const double SigWidthPt = 1.77 * 72; // 127.44 pt
@@ -609,6 +613,8 @@
int? _loadedEnvelopeId; int? _loadedEnvelopeId;
int? _editingEnvelopeId; int? _editingEnvelopeId;
bool _isLoadingEnvelopeForEdit; bool _isLoadingEnvelopeForEdit;
bool _bootstrapApplied;
string? _pendingBootstrapToken;
List<SignatureFieldDraft> _signatureFields = []; List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen" ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
@@ -629,6 +635,7 @@
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen."; string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
string _saveSuccessBody = string.Empty; string _saveSuccessBody = string.Empty;
bool _saveNavigateToDashboard = false; bool _saveNavigateToDashboard = false;
bool _saveDraftAfterBootstrap;
// ── Envelope metadata ── // ── Envelope metadata ──
string _envelopeTitle = string.Empty; string _envelopeTitle = string.Empty;
@@ -697,6 +704,9 @@
if (EnvelopeId.HasValue && EnvelopeId.Value > 0) if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
_editingEnvelopeId = EnvelopeId.Value; _editingEnvelopeId = EnvelopeId.Value;
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(BootstrapToken))
_pendingBootstrapToken = BootstrapToken;
if (IsEditMode && _loadedEnvelopeId != EnvelopeId) if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
{ {
await LoadEnvelopeForEditAsync(EnvelopeId!.Value); await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
@@ -707,15 +717,91 @@
return; 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) if (firstRender)
{ {
_isPageReady = true; _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) async Task LoadEnvelopeForEditAsync(int envelopeId)
@@ -1108,12 +1194,17 @@
} }
async Task SaveDraftAsync() async Task SaveDraftAsync()
{
await SaveDraftCoreAsync(navigateToDashboard: true);
}
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
{ {
if (!ValidateSettings(out var settingsError)) if (!ValidateSettings(out var settingsError))
{ {
_saveErrorMessage = settingsError; _saveErrorMessage = settingsError;
_savePopupVisible = true; _savePopupVisible = true;
return; return null;
} }
_isSaving = true; _isSaving = true;
@@ -1134,25 +1225,37 @@
_loadedEnvelopeId = result.Id; _loadedEnvelopeId = result.Id;
_editingEnvelopeId = 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/editor?envelopeId={result.Id}", forceLoad: false);
} }
NavigationManager.NavigateTo("/sender", forceLoad: false); return result.Id;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "[SenderEditor] Failed to save draft"); Logger.LogError(ex, "[SenderEditor] Failed to save draft");
_saveErrorMessage = ex.Message; _saveErrorMessage = ex.Message;
_savePopupVisible = true; _savePopupVisible = true;
return null;
} }
finally finally
{ {
_isSaving = false; _isSaving = false;
await InvokeAsync(StateHasChanged);
} }
} }
async Task SaveDraftAndStayOnEditorAsync()
{
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
await SaveDraftCoreAsync(navigateToDashboard: false);
}
// ── Send envelope ── // ── Send envelope ──
async Task SendAsync() async Task SendAsync()
{ {