Add feature to open receiver envelope URLs
Introduce a new feature allowing users to open URLs for receiver envelopes directly from the sender page. This includes: - Injecting `EnvelopeReceiverService` in `SenderPage.razor`. - Adding a UI button to open receiver envelope URLs. - Implementing `OpenReceiverEnvelopeUrlAsync` for URL handling. - Extending `EnvelopeReceiverService` with methods for URL retrieval and feature check. - Updating `EnvelopeReceiverController` with new endpoints for URL access. - Introducing `SenderFeatureAccessOptions` for managing feature access. - Configuring feature access in `Program.cs` and `appsettings.json`. - Restricting feature usage to specified email addresses.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
||||
@inject ILogger<SenderPage> Logger
|
||||
@inject NavigationManager Navigation
|
||||
@@ -315,12 +316,13 @@
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
@{
|
||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||
var accessCode = receiver.AccessCode;
|
||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||
}
|
||||
@{
|
||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||
var accessCode = receiver.AccessCode;
|
||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||
var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver);
|
||||
}
|
||||
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||
@if (isRejected)
|
||||
{
|
||||
@@ -349,27 +351,40 @@
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||
</div>
|
||||
<div class="sender-access-code">
|
||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||
<div class="sender-access-code__row">
|
||||
<input class="sender-access-code__value"
|
||||
<div class="sender-access-code">
|
||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||
<div class="sender-access-code__row">
|
||||
<input class="sender-access-code__value"
|
||||
value="@(hasAccessCode ? accessCode : "-")"
|
||||
readonly
|
||||
title="@L("AccessCode", "Access code")" />
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasAccessCode)"
|
||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||
title="@L("Copy", "Copy")">
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasAccessCode)"
|
||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||
title="@L("Copy", "Copy")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@if (_canUseReceiverEnvelopeUrl)
|
||||
{
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasReceiverEnvelopeUrl)"
|
||||
@onclick='() => OpenReceiverEnvelopeUrlAsync(envelope, receiver)'
|
||||
title="@L("Open", "Open")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M6.364 13.5a1.5 1.5 0 0 1-1.5-1.5V4a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 1 0v4a1.5 1.5 0 0 1-1.5 1.5h-8z"/>
|
||||
<path fill-rule="evenodd" d="M8.5 1a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L8.854 7.354a.5.5 0 1 1-.708-.708L12.793 2H9a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -544,6 +559,7 @@
|
||||
private string _activeTab = "active";
|
||||
private bool _isLoading = true;
|
||||
private bool _isLoggingOut = false;
|
||||
private bool _canUseReceiverEnvelopeUrl = false;
|
||||
private string? _errorMessage;
|
||||
private DxGrid? _grid;
|
||||
|
||||
@@ -603,6 +619,8 @@
|
||||
try { await ThemeService.LoadFromServerAsync(); }
|
||||
catch { /* swallow — default theme remains active */ }
|
||||
|
||||
_canUseReceiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync();
|
||||
|
||||
// Load logo config — authenticated, so this should succeed
|
||||
var logos = await CustomImagesService.GetAsync();
|
||||
_appLogo = logos.App;
|
||||
@@ -987,6 +1005,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
async Task OpenReceiverEnvelopeUrlAsync(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||
{
|
||||
if (!_canUseReceiverEnvelopeUrl)
|
||||
return;
|
||||
|
||||
if (!CanOpenReceiverEnvelopeUrl(envelope, receiver))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var receiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlAsync(
|
||||
envelope.Uuid,
|
||||
receiver.Receiver!.Signature!);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(receiverEnvelopeUrl))
|
||||
return;
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("open", receiverEnvelopeUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.warn", "[Sender] Failed to open receiver envelope URL.");
|
||||
}
|
||||
}
|
||||
|
||||
static bool CanOpenReceiverEnvelopeUrl(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||
=> !string.IsNullOrWhiteSpace(envelope.Uuid)
|
||||
&& !string.IsNullOrWhiteSpace(receiver.Receiver?.Signature);
|
||||
|
||||
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
||||
{
|
||||
// Future: Add custom row coloring based on status if needed
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private sealed record ReceiverEnvelopeUrlResponse(string Url);
|
||||
private sealed record ReceiverEnvelopeUrlEnabledResponse(bool Enabled);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the envelope receiver data for the given envelope key from the API.
|
||||
@@ -63,4 +65,32 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
||||
}
|
||||
|
||||
public async Task<string?> GetReceiverEnvelopeUrlAsync(string envelopeUuid, string receiverSignature, CancellationToken cancel = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature))
|
||||
return null;
|
||||
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var url = $"/api/EnvelopeReceiver/receiver-envelope-url?envelopeUuid={Uri.EscapeDataString(envelopeUuid)}&receiverSignature={Uri.EscapeDataString(receiverSignature)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlResponse>(_jsonOptions, cancel);
|
||||
return payload?.Url;
|
||||
}
|
||||
|
||||
public async Task<bool> GetReceiverEnvelopeUrlFeatureEnabledAsync(CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.GetAsync("/api/EnvelopeReceiver/receiver-envelope-url/enabled", cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return false;
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlEnabledResponse>(_jsonOptions, cancel);
|
||||
return payload?.Enabled == true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user