< Summary

Information
Class: Pomodoro.Web.Services.NotificationService
Assembly: Pomodoro.Web
File(s): /home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/NotificationService.cs
Line coverage
100%
Covered lines: 95
Uncovered lines: 0
Coverable lines: 95
Total lines: 169
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_IsNotificationPermitted()100%11100%
InitializeAsync()100%22100%
InitializeCoreAsync()100%22100%
RequestPermissionAsync()100%11100%
RefreshPermissionStateAsync()100%11100%
ShowNotificationAsync()100%22100%
OnNotificationActionClick(...)100%22100%
PlayTimerCompleteSoundAsync()100%11100%
PlayBreakCompleteSoundAsync()100%11100%
DisposeAsync()100%22100%

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/NotificationService.cs

#LineLine coverage
 1using Microsoft.JSInterop;
 2using Microsoft.Extensions.Logging;
 3using Pomodoro.Web.Models;
 4
 5namespace Pomodoro.Web.Services;
 6
 7/// <summary>
 8/// Implementation of notification service using browser APIs
 9/// </summary>
 10public class NotificationService : INotificationService, IAsyncDisposable
 11{
 12    private readonly IJSRuntime _jsRuntime;
 13    private readonly AppState _appState;
 14    private readonly ILogger<NotificationService> _logger;
 15    private DotNetObjectReference<NotificationService>? _dotNetRef;
 16    private Task? _initializationTask;
 2717    private readonly SemaphoreSlim _initSemaphore = new(1, 1);
 18
 4819    public bool IsNotificationPermitted { get; private set; }
 20
 21    public event Action<string>? OnNotificationAction;
 22
 2723    public NotificationService(IJSRuntime jsRuntime, AppState appState, ILogger<NotificationService> logger)
 2724    {
 2725        _jsRuntime = jsRuntime;
 2726        _appState = appState;
 2727        _logger = logger;
 2728    }
 29
 30    public async Task InitializeAsync()
 731    {
 32        // Use async semaphore to ensure only one initialization runs at a time
 33        // and concurrent callers wait for the same initialization to complete
 734        await _initSemaphore.WaitAsync();
 35        try
 736        {
 37            // If initialization is already in progress or completed, return the existing task
 738            if (_initializationTask != null)
 139            {
 140                return;
 41            }
 42
 43            // Start initialization and store the task
 644            _initializationTask = InitializeCoreAsync();
 645            await _initializationTask;
 646        }
 47        finally
 748        {
 749            _initSemaphore.Release();
 750        }
 751    }
 52
 53    private async Task InitializeCoreAsync()
 754    {
 55        try
 756        {
 57            // Dispose existing reference if any (shouldn't happen, but safety check)
 758            _dotNetRef?.Dispose();
 59
 60            // Create dotnet reference for JS callbacks
 761            _dotNetRef = DotNetObjectReference.Create(this);
 762            await _jsRuntime.InvokeVoidAsync(Constants.NotificationJsFunctions.RegisterDotNetRef, _dotNetRef);
 63
 664            await RequestPermissionAsync();
 665        }
 166        catch (Exception ex)
 167        {
 168            _logger.LogError(ex, Constants.Messages.ErrorInitializingNotificationService);
 69            // Reset initialization task on failure to allow retry
 170            _initializationTask = null;
 171        }
 772    }
 73
 74    public async Task<bool> RequestPermissionAsync()
 1775    {
 76        try
 1777        {
 1778            var permission = await _jsRuntime.InvokeAsync<string>(Constants.NotificationJsFunctions.RequestPermission);
 1679            IsNotificationPermitted = permission == Constants.NotificationPermissions.Granted;
 1680            return IsNotificationPermitted;
 81        }
 182        catch (Exception ex)
 183        {
 184            _logger.LogError(ex, Constants.Messages.ErrorRequestingNotificationPermission);
 185            return false;
 86        }
 1787    }
 88
 89    /// <summary>
 90    /// Refreshes the notification permission state from the browser.
 91    /// Call this when settings page opens to ensure UI shows current permission status.
 92    /// Note: This does not affect ShowNotificationAsync which checks permission directly in JS.
 93    /// </summary>
 94    public async Task RefreshPermissionStateAsync()
 395    {
 96        try
 397        {
 398            var permission = await _jsRuntime.InvokeAsync<string>(Constants.NotificationJsFunctions.RequestPermission);
 299            IsNotificationPermitted = permission == Constants.NotificationPermissions.Granted;
 2100        }
 1101        catch (Exception ex)
 1102        {
 1103            _logger.LogError(ex, Constants.Messages.ErrorRequestingNotificationPermission);
 1104        }
 3105    }
 106
 107    public async Task ShowNotificationAsync(string title, string body, SessionType sessionType, string? icon = null)
 7108    {
 109        // Fast-fail if we know permission isn't granted (avoids unnecessary JS interop)
 8110        if (!IsNotificationPermitted) return;
 111
 112        try
 6113        {
 114            // Pass session type as int (0 = Pomodoro, 1 = ShortBreak, 2 = LongBreak)
 6115            await _jsRuntime.InvokeVoidAsync(Constants.NotificationJsFunctions.ShowNotification, title, body, icon, (int
 5116        }
 1117        catch (Exception ex)
 1118        {
 1119            _logger.LogError(ex, Constants.Messages.ErrorShowingNotification);
 1120        }
 7121    }
 122
 123    // Called from JavaScript when notification action is clicked
 124    [JSInvokable(Constants.JsInvokableMethods.OnNotificationActionClick)]
 125    public void OnNotificationActionClick(string action)
 2126    {
 2127        OnNotificationAction?.Invoke(action);
 2128    }
 129
 130    public async Task PlayTimerCompleteSoundAsync()
 2131    {
 132        try
 2133        {
 2134            await _jsRuntime.InvokeVoidAsync(Constants.NotificationJsFunctions.PlayTimerCompleteSound);
 1135        }
 1136        catch (Exception ex)
 1137        {
 1138            _logger.LogWarning(ex, Constants.Messages.ErrorPlayingTimerCompleteSound);
 1139        }
 2140    }
 141
 142    public async Task PlayBreakCompleteSoundAsync()
 2143    {
 144        try
 2145        {
 2146            await _jsRuntime.InvokeVoidAsync(Constants.NotificationJsFunctions.PlayBreakCompleteSound);
 1147        }
 1148        catch (Exception ex)
 1149        {
 1150            _logger.LogWarning(ex, Constants.Messages.ErrorPlayingBreakCompleteSound);
 1151        }
 2152    }
 153
 154    public async ValueTask DisposeAsync()
 3155    {
 156        // Unregister the .NET reference from JavaScript
 157        try
 3158        {
 3159            await _jsRuntime.InvokeVoidAsync(Constants.NotificationJsFunctions.UnregisterDotNetRef);
 2160        }
 1161        catch
 1162        {
 163            // Ignore errors during disposal
 1164        }
 165
 3166        _dotNetRef?.Dispose();
 3167        _initSemaphore.Dispose();
 3168    }
 169}