Refactor envelope receiver auth flow and state handling
- Introduce IReceiverAuthService and ReceiverAuthService for all envelope receiver authentication and status API calls - Add ReceiverAuthModel as a client-side DTO for API responses - Refactor EnvelopeState to store all relevant fields and update via ApplyApiResponse - Overhaul EnvelopePage.razor to use new service and state, with improved status handling and UI - Enhance ApiResponse and ApiServiceBase to support structured error deserialization - Register IReceiverAuthService in DI container
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-seitiges DTO für die Antwort des ReceiverAuthControllers.
|
||||
/// Wird 1:1 aus dem JSON deserialisiert.
|
||||
///
|
||||
/// WARUM ein eigenes Client-Model statt das API-Model zu referenzieren?
|
||||
/// - Das API-Projekt hat Server-Abhängigkeiten (EF Core, SqlClient, etc.)
|
||||
/// - Diese Pakete existieren nicht für browser-wasm → Build-Fehler
|
||||
/// - Die Property-Namen müssen nur zum JSON passen (case-insensitive)
|
||||
/// </summary>
|
||||
public record ReceiverAuthModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Aktueller Status des Empfänger-Flows.
|
||||
/// Werte: "requires_access_code", "requires_tfa", "show_document",
|
||||
/// "already_signed", "rejected", "not_found", "expired", "error"
|
||||
/// </summary>
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
public string? Title { get; init; }
|
||||
public string? Message { get; init; }
|
||||
public string? SenderEmail { get; init; }
|
||||
public string? ReceiverName { get; init; }
|
||||
public bool TfaEnabled { get; init; }
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public string? TfaType { get; init; }
|
||||
public DateTime? TfaExpiration { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@page "/envelope/{EnvelopeKey}"
|
||||
@rendermode InteractiveAuto
|
||||
@inject IEnvelopeService EnvelopeService
|
||||
@inject IReceiverAuthService ReceiverAuthService
|
||||
@inject EnvelopeState State
|
||||
@implements IDisposable
|
||||
|
||||
@@ -13,24 +13,59 @@
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.NotFound:
|
||||
<ErrorDisplay Title="Nicht gefunden"
|
||||
Message="Dieses Dokument existiert nicht oder ist nicht mehr verfügbar." />
|
||||
<StatusPage Type="not_found" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.AlreadySigned:
|
||||
<ErrorDisplay Title="Bereits unterschrieben"
|
||||
Message="Dieses Dokument wurde bereits unterschrieben."
|
||||
Icon="check-circle" />
|
||||
<StatusPage Type="signed"
|
||||
Title="@State.Title"
|
||||
SenderEmail="@State.SenderEmail" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.Rejected:
|
||||
<StatusPage Type="rejected"
|
||||
Title="@State.Title"
|
||||
SenderEmail="@State.SenderEmail" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.Expired:
|
||||
<StatusPage Type="expired" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.RequiresAccessCode:
|
||||
<AccessCodeForm EnvelopeKey="@EnvelopeKey"
|
||||
ErrorMessage="@State.ErrorMessage"
|
||||
SenderEmail="@State.SenderEmail"
|
||||
Title="@State.Title"
|
||||
TfaEnabled="@State.TfaEnabled"
|
||||
HasPhoneNumber="@State.HasPhoneNumber"
|
||||
OnSubmit="HandleAccessCodeSubmit" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.RequiresTwoFactor:
|
||||
<TfaForm EnvelopeKey="@EnvelopeKey"
|
||||
TfaType="@(State.TfaType ?? "authenticator")"
|
||||
TfaExpiration="@State.TfaExpiration"
|
||||
HasPhoneNumber="@State.HasPhoneNumber"
|
||||
ErrorMessage="@State.ErrorMessage"
|
||||
OnSubmit="HandleTfaSubmit" />
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.ShowDocument:
|
||||
<PdfViewer DocumentBytes="@_documentBytes" />
|
||||
@* Phase 4 (PSPDFKit) kommt später — vorerst Platzhalter *@
|
||||
<div class="text-center mt-5">
|
||||
<div class="status-icon signed">
|
||||
<i class="bi bi-file-earmark-check"></i>
|
||||
</div>
|
||||
<h2>Dokument bereit</h2>
|
||||
<p class="text-muted">
|
||||
«@State.Title» — PDF-Viewer wird in Phase 4 integriert.
|
||||
</p>
|
||||
@if (State.ReadOnly)
|
||||
{
|
||||
<span class="badge bg-secondary">Nur Lesen</span>
|
||||
}
|
||||
</div>
|
||||
break;
|
||||
|
||||
case EnvelopePageStatus.Error:
|
||||
@@ -41,44 +76,83 @@
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; set; } = default!;
|
||||
|
||||
private byte[]? _documentBytes;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
State.OnChange += StateHasChanged;
|
||||
await LoadEnvelopeAsync();
|
||||
}
|
||||
|
||||
private async Task LoadEnvelopeAsync()
|
||||
{
|
||||
State.SetLoading();
|
||||
|
||||
var result = await EnvelopeService.GetEnvelopeReceiversAsync();
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
if (result.StatusCode == 401)
|
||||
State.SetAccessCodeRequired();
|
||||
else if (result.StatusCode == 404)
|
||||
State.SetNotFound();
|
||||
else
|
||||
State.SetError(result.ErrorMessage ?? "Unbekannter Fehler");
|
||||
return;
|
||||
}
|
||||
|
||||
State.SetDocument();
|
||||
await LoadStatusAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empfängt das Tuple (Code, PreferSms) von AccessCodeForm.OnSubmit.
|
||||
/// AccessCodeForm gibt immer ein Tuple zurück, weil es auch den
|
||||
/// SMS-Toggle-Zustand enthält (für TFA).
|
||||
/// Erster API-Call: Status prüfen.
|
||||
/// Entspricht dem GET /Envelope/{key} im Web-Projekt.
|
||||
/// Die API entscheidet, was passiert (AccessCode nötig? Bereits signiert? etc.)
|
||||
/// </summary>
|
||||
private async Task LoadStatusAsync()
|
||||
{
|
||||
State.SetLoading();
|
||||
|
||||
var result = await ReceiverAuthService.GetStatusAsync(EnvelopeKey);
|
||||
|
||||
if (result.IsSuccess && result.Data is not null)
|
||||
{
|
||||
State.ApplyApiResponse(result.Data);
|
||||
}
|
||||
else if (result.StatusCode == 404)
|
||||
{
|
||||
State.SetNotFound();
|
||||
}
|
||||
else
|
||||
{
|
||||
State.SetError(result.ErrorMessage ?? "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zweiter API-Call: AccessCode senden.
|
||||
/// Wird von AccessCodeForm aufgerufen (OnSubmit-Callback).
|
||||
/// Die API prüft den Code und antwortet mit dem nächsten Status.
|
||||
/// </summary>
|
||||
private async Task HandleAccessCodeSubmit((string Code, bool PreferSms) submission)
|
||||
{
|
||||
// AccessCode an API senden
|
||||
// Bei Erfolg: State.SetDocument() oder State.SetTwoFactorRequired()
|
||||
// Bei Fehler: State.SetError(...)
|
||||
var result = await ReceiverAuthService.SubmitAccessCodeAsync(
|
||||
EnvelopeKey, submission.Code, submission.PreferSms);
|
||||
|
||||
if (result.IsSuccess && result.Data is not null)
|
||||
{
|
||||
State.ApplyApiResponse(result.Data);
|
||||
}
|
||||
else if (result.Data is not null)
|
||||
{
|
||||
// 401 mit Body → falscher Code, API gibt trotzdem ReceiverAuthModel zurück
|
||||
State.ApplyApiResponse(result.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
State.SetError(result.ErrorMessage ?? "Fehler bei der Code-Prüfung.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dritter API-Call: TFA-Code senden.
|
||||
/// Wird von TfaForm aufgerufen (OnSubmit-Callback).
|
||||
/// </summary>
|
||||
private async Task HandleTfaSubmit((string Code, string Type) submission)
|
||||
{
|
||||
var result = await ReceiverAuthService.SubmitTfaCodeAsync(
|
||||
EnvelopeKey, submission.Code, submission.Type);
|
||||
|
||||
if (result.IsSuccess && result.Data is not null)
|
||||
{
|
||||
State.ApplyApiResponse(result.Data);
|
||||
}
|
||||
else if (result.Data is not null)
|
||||
{
|
||||
State.ApplyApiResponse(result.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
State.SetError(result.ErrorMessage ?? "Fehler bei der TFA-Prüfung.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => State.OnChange -= StateHasChanged;
|
||||
|
||||
@@ -20,6 +20,7 @@ builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
|
||||
// API-Services: Je ein Service pro API-Controller
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IEnvelopeService, EnvelopeService>();
|
||||
builder.Services.AddScoped<IReceiverAuthService, ReceiverAuthService>();
|
||||
|
||||
// State: Ein State-Objekt pro Browser-Tab
|
||||
builder.Services.AddScoped<EnvelopeState>();
|
||||
|
||||
@@ -19,6 +19,14 @@ public record ApiResponse<T>
|
||||
|
||||
public static ApiResponse<T> Failure(int statusCode, string? error = null)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
|
||||
|
||||
/// <summary>
|
||||
/// Failure mit deserialisiertem Body — für Fälle wo die API
|
||||
/// bei 401/404 trotzdem ein strukturiertes JSON zurückgibt
|
||||
/// (z.B. ReceiverAuthResponse mit ErrorMessage + Status).
|
||||
/// </summary>
|
||||
public static ApiResponse<T> Failure(int statusCode, string? error, T? data)
|
||||
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error, Data = data };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -37,7 +37,13 @@ public abstract class ApiServiceBase
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("GET {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody);
|
||||
|
||||
// Versuche den Body trotzdem zu deserialisieren —
|
||||
// die API gibt bei 401/404 oft strukturierte JSON-Antworten zurück
|
||||
// (z.B. ReceiverAuthResponse mit ErrorMessage + Status)
|
||||
var errorData = await TryDeserializeAsync<T>(response, ct);
|
||||
|
||||
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody, errorData);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
@@ -70,7 +76,10 @@ public abstract class ApiServiceBase
|
||||
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
||||
Logger.LogWarning("POST {Endpoint} failed: {Status} - {Body}",
|
||||
endpoint, (int)response.StatusCode, errorBody);
|
||||
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody);
|
||||
|
||||
var errorData = await TryDeserializeAsync<TResponse>(response, ct);
|
||||
|
||||
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody, errorData);
|
||||
}
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: ct);
|
||||
@@ -107,4 +116,26 @@ public abstract class ApiServiceBase
|
||||
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versucht den Response-Body als JSON zu deserialisieren.
|
||||
/// Gibt null zurück wenn es nicht klappt (z.B. bei HTML-Fehlerseiten).
|
||||
/// </summary>
|
||||
private static async Task<T?> TryDeserializeAsync<T>(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Nur versuchen wenn der Content-Type JSON ist
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/json")
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignorieren — der Body war kein valides JSON
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Kommuniziert mit dem ReceiverAuthController der API.
|
||||
///
|
||||
/// Drei Methoden — eine pro Endpunkt:
|
||||
/// 1. GetStatusAsync → GET /api/receiverauth/{key}/status
|
||||
/// 2. SubmitAccessCodeAsync → POST /api/receiverauth/{key}/access-code
|
||||
/// 3. SubmitTfaCodeAsync → POST /api/receiverauth/{key}/tfa
|
||||
/// </summary>
|
||||
public interface IReceiverAuthService
|
||||
{
|
||||
/// <summary>Prüft den aktuellen Status des Empfänger-Flows</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(string key, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Sendet den Zugangscode zur Prüfung</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
|
||||
string key, string accessCode, bool preferSms, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Sendet den TFA-Code (SMS oder Authenticator) zur Prüfung</summary>
|
||||
Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
|
||||
string key, string code, string type, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Spricht mit dem ReceiverAuthController der API.
|
||||
///
|
||||
/// Nutzt die Basisklasse ApiServiceBase für einheitliches Error-Handling.
|
||||
/// Jede Methode gibt ApiResponse<ReceiverAuthModel> zurück —
|
||||
/// egal ob Erfolg oder Fehler. Die aufrufende Komponente prüft dann
|
||||
/// result.IsSuccess und result.Data.Status.
|
||||
///
|
||||
/// WARUM gibt die API bei 401 trotzdem ein ReceiverAuthModel zurück?
|
||||
/// Weil auch bei "falscher Code" der Client wissen muss, welchen
|
||||
/// Status er anzeigen soll (z.B. "requires_access_code" + ErrorMessage).
|
||||
/// Deshalb deserialisieren wir auch bei Fehler-Statuscodes den Body.
|
||||
/// </summary>
|
||||
public class ReceiverAuthService : ApiServiceBase, IReceiverAuthService
|
||||
{
|
||||
public ReceiverAuthService(HttpClient http, ILogger<ReceiverAuthService> logger)
|
||||
: base(http, logger) { }
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(
|
||||
string key, CancellationToken ct = default)
|
||||
=> GetAsync<ReceiverAuthModel>($"api/receiverauth/{key}/status", ct);
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
|
||||
string key, string accessCode, bool preferSms, CancellationToken ct = default)
|
||||
=> PostAsync<object, ReceiverAuthModel>(
|
||||
$"api/receiverauth/{key}/access-code",
|
||||
new { AccessCode = accessCode, PreferSms = preferSms },
|
||||
ct);
|
||||
|
||||
public Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
|
||||
string key, string code, string type, CancellationToken ct = default)
|
||||
=> PostAsync<object, ReceiverAuthModel>(
|
||||
$"api/receiverauth/{key}/tfa",
|
||||
new { Code = code, Type = type },
|
||||
ct);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.State;
|
||||
using EnvelopeGenerator.ReceiverUI.Client.Models;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Client.State;
|
||||
|
||||
/// <summary>
|
||||
/// Hält den aktuellen Zustand des geladenen Umschlags.
|
||||
@@ -9,11 +11,13 @@
|
||||
/// - StateHasChanged() informiert automatisch alle Subscriber
|
||||
///
|
||||
/// PATTERN: "Observable State" — Services setzen den State, Komponenten reagieren darauf.
|
||||
///
|
||||
/// Die Set-Methoden nehmen jetzt ein ReceiverAuthModel entgegen,
|
||||
/// damit alle Felder (Title, SenderEmail, TfaType etc.) zentral gespeichert werden.
|
||||
/// </summary>
|
||||
public class EnvelopeState
|
||||
{
|
||||
private EnvelopePageStatus _status = EnvelopePageStatus.Loading;
|
||||
private string? _errorMessage;
|
||||
|
||||
/// <summary>Aktueller Seitenstatus</summary>
|
||||
public EnvelopePageStatus Status
|
||||
@@ -26,42 +30,87 @@ public class EnvelopeState
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fehlermeldung (falls vorhanden)</summary>
|
||||
public string? ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
private set
|
||||
{
|
||||
_errorMessage = value;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
// ── Felder aus ReceiverAuthModel ──
|
||||
|
||||
// --- Zustandsübergänge (öffentliche Methoden) ---
|
||||
/// <summary>Titel des Umschlags (z.B. "Vertragsdokument")</summary>
|
||||
public string? Title { get; private set; }
|
||||
|
||||
public void SetLoading() => Status = EnvelopePageStatus.Loading;
|
||||
/// <summary>Nachricht des Absenders</summary>
|
||||
public string? Message { get; private set; }
|
||||
|
||||
public void SetAccessCodeRequired()
|
||||
/// <summary>E-Mail des Absenders (für Rückfragen-Hinweis)</summary>
|
||||
public string? SenderEmail { get; private set; }
|
||||
|
||||
/// <summary>Ob TFA für diesen Umschlag aktiviert ist</summary>
|
||||
public bool TfaEnabled { get; private set; }
|
||||
|
||||
/// <summary>Ob der Empfänger eine Telefonnummer hat (für SMS-TFA)</summary>
|
||||
public bool HasPhoneNumber { get; private set; }
|
||||
|
||||
/// <summary>Ob das Dokument nur gelesen werden soll (ReadAndConfirm)</summary>
|
||||
public bool ReadOnly { get; private set; }
|
||||
|
||||
/// <summary>TFA-Typ: "sms" oder "authenticator"</summary>
|
||||
public string? TfaType { get; private set; }
|
||||
|
||||
/// <summary>Ablaufzeit des SMS-Codes (für Countdown-Timer)</summary>
|
||||
public DateTime? TfaExpiration { get; private set; }
|
||||
|
||||
/// <summary>Fehlermeldung (z.B. "Falscher Zugangscode")</summary>
|
||||
public string? ErrorMessage { get; private set; }
|
||||
|
||||
// ── Zustandsübergänge ──
|
||||
|
||||
public void SetLoading()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
Status = EnvelopePageStatus.RequiresAccessCode;
|
||||
Status = EnvelopePageStatus.Loading;
|
||||
}
|
||||
|
||||
public void SetTwoFactorRequired() => Status = EnvelopePageStatus.RequiresTwoFactor;
|
||||
/// <summary>
|
||||
/// Setzt den State aus einer API-Antwort.
|
||||
/// Zentrale Methode — alle Endpunkte liefern ReceiverAuthModel,
|
||||
/// und diese Methode mappt den Status-String auf das richtige Enum.
|
||||
/// </summary>
|
||||
public void ApplyApiResponse(ReceiverAuthModel model)
|
||||
{
|
||||
// Gemeinsame Felder immer übernehmen
|
||||
Title = model.Title ?? Title;
|
||||
Message = model.Message ?? Message;
|
||||
SenderEmail = model.SenderEmail ?? SenderEmail;
|
||||
TfaEnabled = model.TfaEnabled;
|
||||
HasPhoneNumber = model.HasPhoneNumber;
|
||||
ReadOnly = model.ReadOnly;
|
||||
TfaType = model.TfaType ?? TfaType;
|
||||
TfaExpiration = model.TfaExpiration ?? TfaExpiration;
|
||||
ErrorMessage = model.ErrorMessage;
|
||||
|
||||
public void SetDocument() => Status = EnvelopePageStatus.ShowDocument;
|
||||
// Status-String → Enum
|
||||
Status = model.Status switch
|
||||
{
|
||||
"requires_access_code" => EnvelopePageStatus.RequiresAccessCode,
|
||||
"requires_tfa" => EnvelopePageStatus.RequiresTwoFactor,
|
||||
"show_document" => EnvelopePageStatus.ShowDocument,
|
||||
"already_signed" => EnvelopePageStatus.AlreadySigned,
|
||||
"rejected" => EnvelopePageStatus.Rejected,
|
||||
"not_found" => EnvelopePageStatus.NotFound,
|
||||
"expired" => EnvelopePageStatus.Expired,
|
||||
"error" => EnvelopePageStatus.Error,
|
||||
_ => EnvelopePageStatus.Error
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Setzt Fehler wenn der API-Call selbst fehlschlägt (Netzwerk etc.)</summary>
|
||||
public void SetError(string message)
|
||||
{
|
||||
ErrorMessage = message;
|
||||
Status = EnvelopePageStatus.Error;
|
||||
}
|
||||
|
||||
public void SetAlreadySigned() => Status = EnvelopePageStatus.AlreadySigned;
|
||||
public void SetRejected() => Status = EnvelopePageStatus.Rejected;
|
||||
/// <summary>Setzt NotFound (z.B. bei 404 ohne Body)</summary>
|
||||
public void SetNotFound() => Status = EnvelopePageStatus.NotFound;
|
||||
|
||||
// --- Event: Benachrichtigt Komponenten über Änderungen ---
|
||||
// ── Event ──
|
||||
public event Action? OnChange;
|
||||
private void NotifyStateChanged() => OnChange?.Invoke();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user