@using AliasVault.Client.Auth.Models @using AliasVault.Client.Auth.Services @using AliasVault.Client.Main.Components.Layout @using AliasVault.Client.Utilities @using Microsoft.Extensions.Localization @using Microsoft.Extensions.DependencyInjection @implements IDisposable @if (IsOpen) {

@Title

@Description

@if (!string.IsNullOrEmpty(_errorMessage)) {
@_errorMessage
} @if (!string.IsNullOrEmpty(_qrCodeUrl)) {
@if (!_isLoading) {
@FormatTime(_timeRemaining)
}
} @if (_isLoading && string.IsNullOrEmpty(_errorMessage)) {
}
} @code { private string? _qrCodeUrl; private MobileLoginErrorCode? _errorCode; private string? _errorMessage; private int _timeRemaining = 120; // 2 minutes in seconds private MobileLoginUtility? _mobileLoginUtility; private bool _isLoading = true; private System.Threading.Timer? _countdownTimer; private string _qrElementId = $"mobile-unlock-qr-{Guid.NewGuid():N}"; [Inject] private HttpClient Http { get; set; } = default!; [Inject] private JsInteropService JsInteropService { get; set; } = default!; [Inject] private ILogger Logger { get; set; } = default!; [Inject] private IStringLocalizerFactory LocalizerFactory { get; set; } = default!; [Inject] private IServiceProvider ScopedServices { get; set; } = default!; private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client"); private IStringLocalizer MobileLoginLocalizer => LocalizerFactory.Create("MobileLogin", "AliasVault.Client"); /// /// Whether the modal is open. /// [Parameter] public bool IsOpen { get; set; } /// /// Callback when the modal is closed. /// [Parameter] public EventCallback OnClose { get; set; } /// /// Callback when mobile login/unlock succeeds with the result. /// [Parameter] public EventCallback OnSuccess { get; set; } /// /// Mode - 'login' or 'unlock'. /// [Parameter] public string Mode { get; set; } = "login"; /// /// Modal title based on mode. /// private string Title => Mode == "unlock" ? LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["UnlockTitle"] : LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["PageTitle"]; /// /// Modal description based on mode. /// private string Description => LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["ScanQrCodeDescription"]; /// protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); if (IsOpen && _mobileLoginUtility == null) { await InitiateMobileLoginAsync(); } else if (!IsOpen) { Cleanup(); } } /// /// Initialize mobile login when modal opens. /// private async Task InitiateMobileLoginAsync() { try { _isLoading = true; _errorCode = null; _errorMessage = null; _qrCodeUrl = null; _timeRemaining = 120; StateHasChanged(); // Initialize mobile login utility var utilityLogger = ScopedServices.GetRequiredService>(); _mobileLoginUtility = new MobileLoginUtility(Http, JsInteropService, utilityLogger); // Initiate mobile login and get QR code data var requestId = await _mobileLoginUtility.InitiateAsync(); // Generate QR code with AliasVault prefix for mobile login _qrCodeUrl = $"aliasvault://open/mobile-unlock/{requestId}"; // Render QR code while showing loading StateHasChanged(); await Task.Delay(100); // Give DOM time to render await JsInteropService.GenerateQrCode(_qrElementId); // Wait for QR code to be fully rendered before hiding loading await Task.Delay(300); _isLoading = false; StateHasChanged(); // Start countdown timer StartCountdownTimer(); // Start polling for response await _mobileLoginUtility.StartPollingAsync( HandleSuccessAsync, HandleErrorAsync); } catch (MobileLoginException ex) { Logger.LogError(ex, "Error initiating mobile login"); _isLoading = false; _errorCode = ex.ErrorCode; _errorMessage = GetErrorMessage(ex.ErrorCode); StateHasChanged(); } catch (Exception ex) { Logger.LogError(ex, "Error initiating mobile login"); _isLoading = false; _errorCode = MobileLoginErrorCode.Generic; _errorMessage = GetErrorMessage(MobileLoginErrorCode.Generic); StateHasChanged(); } } /// /// Handle successful mobile login/unlock. /// private async Task HandleSuccessAsync(MobileLoginResult result) { try { // Call parent success callback await OnSuccess.InvokeAsync(result); // Close modal after successful processing await HandleClose(); } catch (Exception ex) { Logger.LogError(ex, "Error handling mobile login success"); _errorMessage = SharedLocalizer["ErrorUnknown"]; StateHasChanged(); } } /// /// Handle error. /// private void HandleErrorAsync(MobileLoginErrorCode errorCode) { _isLoading = false; _qrCodeUrl = null; // Hide QR code when error occurs _errorCode = errorCode; _errorMessage = GetErrorMessage(errorCode); StateHasChanged(); } /// /// Get translated error message for error code. /// private string GetErrorMessage(MobileLoginErrorCode errorCode) { return errorCode switch { MobileLoginErrorCode.Timeout => MobileLoginLocalizer["ErrorTimeout"], MobileLoginErrorCode.Generic => SharedLocalizer["ErrorUnknown"], _ => SharedLocalizer["ErrorUnknown"] }; } /// /// Handle modal close. /// private async Task HandleClose() { Cleanup(); await OnClose.InvokeAsync(); } /// /// Cleanup resources. /// private void Cleanup() { _mobileLoginUtility?.Cleanup(); _mobileLoginUtility?.Dispose(); _mobileLoginUtility = null; _countdownTimer?.Dispose(); _countdownTimer = null; _qrCodeUrl = null; _errorCode = null; _errorMessage = null; _timeRemaining = 120; _isLoading = true; } /// /// Format time remaining as MM:SS. /// private string FormatTime(int seconds) { var mins = seconds / 60; var secs = seconds % 60; return $"{mins}:{secs:D2}"; } /// /// Start countdown timer. /// private void StartCountdownTimer() { _countdownTimer = new System.Threading.Timer(_ => { if (_timeRemaining > 0) { _timeRemaining--; InvokeAsync(StateHasChanged); } else { _countdownTimer?.Dispose(); _mobileLoginUtility?.StopPolling(); } }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); } /// public void Dispose() { Cleanup(); } }