153 lines
5.6 KiB
C#
153 lines
5.6 KiB
C#
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<Guid> 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<string> 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}");
|
|
}
|
|
}
|
|
}
|
|
}
|