first commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user