feat(templates): add localized push templates and actions
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
This commit is contained in:
@@ -3,42 +3,91 @@ namespace HrynCo.NotificationService.Worker.Services.Push;
|
||||
using FirebaseAdmin.Messaging;
|
||||
using FirebaseAdmin;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using HrynCo.NotificationService.Contracts.Messages;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Push;
|
||||
using HrynCo.NotificationService.DAL.Abstract.PushChannels;
|
||||
|
||||
public sealed class FirebasePushClient : IFirebasePushClient
|
||||
{
|
||||
public async Task<PushDeliveryOutcome> SendAsync(Guid messageId, SendPushMessageData data,
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
public async Task<PushDeliveryOutcome> SendAsync(Guid messageId, RenderedPushMessageData data,
|
||||
FirebaseServiceAccountCredentials credentials, CancellationToken cancellationToken)
|
||||
{
|
||||
var remaining = data.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
if (!PushMessageValidator.IsSupportedVersion(data.Delivery.Version))
|
||||
return new PushDeliveryOutcome("Unsupported push payload version. Supported versions are 1 and 2.");
|
||||
var remaining = data.Delivery.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero) return new PushDeliveryOutcome("Push expired.");
|
||||
var payload = BuildPayload(messageId, data);
|
||||
if (data.Delivery.Version == 2 && Encoding.UTF8.GetByteCount(JsonSerializer.Serialize(payload)) > 3500)
|
||||
return new PushDeliveryOutcome("Rendered push payload is too large.");
|
||||
return await SendMessageAsync(new Message
|
||||
{
|
||||
Token = data.Token,
|
||||
Token = data.Delivery.Token,
|
||||
Android = new AndroidConfig { Priority = Priority.High, TimeToLive = remaining },
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["version"] = "1", ["messageId"] = messageId.ToString("D"),
|
||||
["bindingId"] = data.BindingId.ToString("D"), ["title"] = data.Title,
|
||||
["body"] = data.Body, ["route"] = data.Route,
|
||||
["expiresAt"] = data.ExpiresAt.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||
}
|
||||
Data = payload
|
||||
}, credentials, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PushDeliveryOutcome> SendTestAsync(string token, FirebaseServiceAccountCredentials credentials,
|
||||
CancellationToken cancellationToken) => SendMessageAsync(new Message
|
||||
internal static Dictionary<string, string> BuildPayload(Guid messageId, RenderedPushMessageData data)
|
||||
{
|
||||
var payload = BasePayload(messageId, data.Delivery.BindingId, data.Delivery.ExpiresAt, data.Title, data.Body);
|
||||
switch (data.Delivery.Version)
|
||||
{
|
||||
Token = token,
|
||||
Notification = new Notification
|
||||
{
|
||||
Title = "Notification Service test",
|
||||
Body = "This is a test notification from the channel administration page."
|
||||
},
|
||||
case 1:
|
||||
payload["version"] = "1";
|
||||
payload["route"] = data.Delivery.Route;
|
||||
break;
|
||||
case 2:
|
||||
AddActions(payload, data.DefaultAction, data.Buttons);
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(data), "Unsupported push payload version.");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BasePayload(Guid messageId, Guid bindingId, DateTimeOffset expiresAt, string title, string body) => new()
|
||||
{
|
||||
["messageId"] = messageId.ToString("D"), ["bindingId"] = bindingId.ToString("D"),
|
||||
["title"] = title, ["body"] = body,
|
||||
["expiresAt"] = expiresAt.ToUnixTimeSeconds().ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
private static void AddActions(Dictionary<string, string> payload, RenderedPushAction? defaultAction, IReadOnlyList<RenderedPushAction> buttons)
|
||||
{
|
||||
payload["version"] = "2";
|
||||
payload["actions"] = JsonSerializer.Serialize(new { defaultAction, buttons }, JsonOptions);
|
||||
}
|
||||
|
||||
public Task<PushDeliveryOutcome> SendTestAsync(PushChannelTestMessage message, FirebaseServiceAccountCredentials credentials,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (message.Validate() is { } error) return Task.FromResult(new PushDeliveryOutcome(error));
|
||||
return SendMessageAsync(BuildTestMessage(message), credentials, cancellationToken);
|
||||
}
|
||||
|
||||
internal static Message BuildTestMessage(PushChannelTestMessage test)
|
||||
{
|
||||
if (test.Validate() is { } error) throw new ArgumentException(error, nameof(test));
|
||||
const string title = "Notification Service test";
|
||||
const string body = "This is a test notification from the channel administration page.";
|
||||
var message = new Message
|
||||
{
|
||||
Token = test.Token,
|
||||
Android = new AndroidConfig { Priority = Priority.High, TimeToLive = TimeSpan.FromMinutes(5) }
|
||||
}, credentials, cancellationToken);
|
||||
};
|
||||
if (test.Version == 1) message.Notification = new Notification { Title = title, Body = body };
|
||||
else
|
||||
{
|
||||
var payload = BasePayload(Guid.NewGuid(), test.BindingId!.Value, DateTimeOffset.UtcNow.AddMinutes(5), title, body);
|
||||
AddActions(payload, new RenderedPushAction(test.ActionKey!), [new RenderedPushAction(test.ActionKey!, Label: test.ButtonLabel)]);
|
||||
message.Data = payload;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task<PushDeliveryOutcome> CheckAuthorizationAsync(FirebaseServiceAccountCredentials credentials,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user