using System.Net.Http.Json; using System.Text.Json; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Models; namespace Fitness_core_BE.Services { // Sends real device pushes via Expo's push API and, independently, persists a // CustomerNotification row so the mobile Notification tab always has a history even if the // push itself never reached the device (permission denied, no token registered, etc.). public class PushNotificationService : IPushNotificationService { private const string ExpoPushUrl = "https://exp.host/--/api/v2/push/send"; private const int ExpoBatchSize = 100; private readonly HttpClient _httpClient; private readonly ICustomerNotificationRepository _repository; private readonly IUnitOfWork _uow; public PushNotificationService(IHttpClientFactory httpClientFactory, ICustomerNotificationRepository repository, IUnitOfWork uow) { _httpClient = httpClientFactory.CreateClient(); _repository = repository; _uow = uow; } public async Task NotifyCustomerAsync( Guid customerId, string title, string body, string notificationType, string? routeName = null, object? routeParams = null) { var notification = BuildNotification(customerId, title, body, notificationType, routeName, routeParams); await _uow.BeginAsync(); try { await _repository.CreateAsync(notification); await _uow.CommitAsync(); } catch { await _uow.RollbackAsync(); throw; } var pushToken = await _repository.GetCustomerPushTokenAsync(customerId); if (IsExpoPushToken(pushToken)) { await SendExpoPushBatchAsync(new[] { pushToken! }, title, body, notificationType, routeName, routeParams); } } public async Task NotifyCustomersAsync( IEnumerable customerIds, string title, string body, string notificationType, string? routeName = null, object? routeParams = null) { var ids = customerIds.Distinct().ToList(); if (ids.Count == 0) return; var notifications = ids .Select(id => BuildNotification(id, title, body, notificationType, routeName, routeParams)) .ToList(); await _uow.BeginAsync(); try { await _repository.CreateManyAsync(notifications); await _uow.CommitAsync(); } catch { await _uow.RollbackAsync(); throw; } var tokens = await _repository.GetCustomerPushTokensAsync(ids); var validTokens = tokens.Where(IsExpoPushToken).ToList(); for (var offset = 0; offset < validTokens.Count; offset += ExpoBatchSize) { var chunk = validTokens.Skip(offset).Take(ExpoBatchSize).ToList(); await SendExpoPushBatchAsync(chunk, title, body, notificationType, routeName, routeParams); } } private static CustomerNotification BuildNotification( Guid customerId, string title, string body, string notificationType, string? routeName, object? routeParams) { return new CustomerNotification { CustomerNotificationId = Guid.NewGuid(), FKCustomerId = customerId, Title = title, Body = body, NotificationType = notificationType, RouteName = routeName, RouteParamsJson = routeParams != null ? JsonSerializer.Serialize(routeParams) : null, IsRead = false, CreatedAt = DateTime.UtcNow }; } private static bool IsExpoPushToken(string? token) => !string.IsNullOrWhiteSpace(token) && token.StartsWith("ExponentPushToken", StringComparison.OrdinalIgnoreCase); private async Task SendExpoPushBatchAsync( IReadOnlyCollection tokens, string title, string body, string notificationType, string? routeName, object? routeParams) { if (tokens.Count == 0) return; var messages = tokens.Select(token => new { to = token, title, body, sound = "default", data = new { notificationType = routeName != null ? "navigate" : "normal", routeName, routeParams, notificationKind = notificationType } }); try { var response = await _httpClient.PostAsJsonAsync(ExpoPushUrl, messages); if (!response.IsSuccessStatusCode) { var responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Expo push send failed ({(int)response.StatusCode}): {responseBody}"); } } catch (Exception ex) { // Push delivery is best-effort — the notification is already persisted above, // so a network hiccup here must not fail the caller's larger operation. Console.WriteLine($"Expo push send threw: {ex.Message}"); } } } }