Skip to content

Commit

Permalink
Translation for Push Notifications (#675)
Browse files Browse the repository at this point in the history
* feat: first attempt at providing translations for push notifications

* feat: restrict notifications to IPushNotification. Prepare to load language code from Devices table.

* chore: fix formatting

* chore: update csproj. Add test stub

* chore: further experiments with tests

* feat: add test to ensure PushNotifications translation strings exist

* chore: fix merge

* fix: missing translations

* feat: remove usage of Localizer. Use ResourceManager directly. Redo tests

* refactor: fix formatting

* chore: remove needless AssemblyInfo files

* refactor: rename to PushNotificationTextProvider

* refactor: rename variable

* chore: restore Data for TestPushNotification

* feat: add reference to pt resx file

* chore: try another way
the CI pipeline is failing for some reason.

* feat: use device communication language code for sending notifications

* refactor: move IPushNotification to Application module

* chore: add data to test with TestPushNotification

* refactor: rename vars

* chore: tests' fixes

* chore: move resource files elsewhere, simplify their usage.

* refactor: fix formatting

* refactor: resource → resources

* chore: tests must extend AbstractTestBase

* test: add TestDataGenerator

* refactor: simplify/improve PushNotificationTextProvider

* chore: add further tests and complete pt translations

* refactor: extract domain event id randomization into base class

* refactor: introduce PushNotificationResourceManager

* refactor: rename notificationTextService to notificationTextProvider at all places

* test: change test names to match naming convention

* feat: register PushNotificationResourceManager to avoid creation of multiple instances

* refactor: add abstraction for PushNotificationTextProvider

* test: use refactor: add abstraction for PushNotificationTextProvider to fix ApplePushNotificationServiceConnectorTests

* chore: extract IPushNotificationTextProvider into file

* refactor: make setter of Device.CommunicationLanguage private

* test: use [Theory] for PushNotificationTextProviderTests

* chore: fix formatting

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
4 people authored Jun 4, 2024
1 parent 2b468be commit 97839bc
Show file tree
Hide file tree
Showing 40 changed files with 720 additions and 143 deletions.
3 changes: 2 additions & 1 deletion Backbone.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/Intellisense/CodeCompletion/IntelliSenseCompletingCharacters/CSharpCompletingCharacters/UpgradedFromVSSettings/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToConstant_002ELocal/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToPrimaryConstructor/@EntryIndexedValue">DO_NOT_SHOW</s:String>
Expand Down Expand Up @@ -40,6 +40,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Iddict/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=inexistent/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=jwts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=localizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mediat/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Onboarded/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=parallelly/@EntryIndexedValue">True</s:Boolean>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Backbone.BuildingBlocks.Application.PushNotifications;

public interface IPushNotification;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ namespace Backbone.BuildingBlocks.Application.PushNotifications;

public interface IPushNotificationSender
{
Task SendNotification(IdentityAddress recipient, object notification, CancellationToken cancellationToken);
Task SendNotification(IdentityAddress recipient, IPushNotification notification, CancellationToken cancellationToken);
}

11 changes: 7 additions & 4 deletions BuildingBlocks/src/BuildingBlocks.Domain/Events/DomainEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ namespace Backbone.BuildingBlocks.Domain.Events;

public class DomainEvent
{
public DomainEvent() : this(Guid.NewGuid().ToString())
protected DomainEvent() : this(Guid.NewGuid().ToString())
{
}

public DomainEvent(string domainEventId)
protected DomainEvent(string domainEventId, bool randomizeId = false)
{
if (domainEventId.Length is 0 or > 128)
var randomPart = randomizeId ? "/" + Guid.NewGuid().ToString("N")[..3] : "";

DomainEventId = domainEventId + randomPart;

if (DomainEventId.Length is 0 or > 128)
throw new ArgumentException($"{nameof(domainEventId)} must be between 1 and 128 characters long.");

DomainEventId = domainEventId;
CreationDate = SystemTime.UtcNow;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task Sends_push_notification_for_each_relationship_of_each_identity
// Assert
foreach (var identityAddress in new[] { identityAddress1, identityAddress2, identityAddress3 })
{
A.CallTo(() => mockPushNotificationSender.SendNotification(identityAddress, A<object>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
A.CallTo(() => mockPushNotificationSender.SendNotification(identityAddress, A<IPushNotification>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Backbone.BuildingBlocks.Domain;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
using Backbone.Modules.Devices.Domain;
using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Tooling.Extensions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Datawallet;

[NotificationText(Title = NotificationTextAttribute.DEFAULT_TITLE, Body = NotificationTextAttribute.DEFAULT_BODY)]
public record DatawalletModificationsCreatedPushNotification(string CreatedByDevice);
public record DatawalletModificationsCreatedPushNotification(string CreatedByDevice) : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "A deletion process has been approved", Body = "One of your identity's deletion processes was approved and will be processed shortly.")]
public record DeletionProcessApprovedNotification(int DaysUntilDeletion);
public record DeletionProcessApprovedNotification(int DaysUntilDeletion) : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "A deletion process has been cancelled", Body = "One of your identity's deletion processes was cancelled by you.")]
public record DeletionProcessCancelledByOwnerNotification();
public record DeletionProcessCancelledByOwnerNotification : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "A deletion process has been cancelled", Body = "One of your identity's deletion processes was cancelled by the support team.")]
public record DeletionProcessCancelledBySupportNotification();
public record DeletionProcessCancelledBySupportNotification : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "Your Identity will be deleted", Body = "Your Identity will be deleted in a few days. You can still cancel up to this point.")]
public record DeletionProcessGracePeriodReminderPushNotification(int DaysUntilDeletion);
public record DeletionProcessGracePeriodReminderPushNotification(int DaysUntilDeletion) : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "Deletion process started", Body = "A Deletion Process was started for your Identity.")]
public record DeletionProcessStartedPushNotification;
public record DeletionProcessStartedPushNotification : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "Deletion process waiting for approval.",
Body = "There is a deletion process for your identity that waits for your approval. If you don't approve it within a few days, the process will be terminated.")]
public record DeletionProcessWaitingForApprovalReminderPushNotification(int DaysUntilApprovalPeriodEnds);
public record DeletionProcessWaitingForApprovalReminderPushNotification(int DaysUntilApprovalPeriodEnds) : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
using System.Reflection;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;

[NotificationText(Title = "Your identity is now deleted.", Body = "")]
public record DeletionStartsPushNotification
{
public DeletionStartsPushNotification() => GetType().GetCustomAttribute<NotificationTextAttribute>()!.Body = IdentityDeletionConfiguration.DeletionStartsNotification.Text;
};
public record DeletionStartsPushNotification : IPushNotification;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.ExternalEvents;

[NotificationText(Title = NotificationTextAttribute.DEFAULT_TITLE, Body = NotificationTextAttribute.DEFAULT_BODY)]
public record ExternalEventCreatedPushNotification;
public record ExternalEventCreatedPushNotification : IPushNotification;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications;

public record TestPushNotification : IPushNotification
{
public object? Data { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications;
using MediatR;

namespace Backbone.Modules.Devices.Application.PushNotifications.Commands.SendTestNotification;
Expand All @@ -18,7 +19,7 @@ public Handler(IUserContext userContext, IPushNotificationSender pushSenderServi

public async Task<Unit> Handle(SendTestNotificationCommand request, CancellationToken cancellationToken)
{
await _pushSenderService.SendNotification(_activeIdentity, request.Data, cancellationToken);
await _pushSenderService.SendNotification(_activeIdentity, new TestPushNotification { Data = request.Data }, cancellationToken);
return Unit.Value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;

public class TierOfIdentityChangedDomainEvent : DomainEvent
{
public TierOfIdentityChangedDomainEvent(Identity identity, TierId oldTierIdId, TierId newTierIdId) : base($"{identity.Address}/TierOfIdentityChanged/{Guid.NewGuid()}")
public TierOfIdentityChangedDomainEvent(Identity identity, TierId oldTierIdId, TierId newTierId) : base($"{identity.Address}/TierOfIdentityChanged", randomizeId: true)
{
OldTierId = oldTierIdId;
NewTierId = newTierIdId;
NewTierId = newTierId;
IdentityAddress = identity.Address;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public Device(Identity identity, CommunicationLanguage communicationLanguage, De

public DateTime CreatedAt { get; set; }

public CommunicationLanguage CommunicationLanguage { get; set; }
public CommunicationLanguage CommunicationLanguage { get; private set; }

public DeviceId CreatedByDevice { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ public class IdentityDeletionConfiguration
{
Time = 2
};

public static DeletionStartsNotification DeletionStartsNotification { get; } = new()
{
Text = "The grace period for the deletion of your identity has expired. The deletion starts now."
};
}

public class GracePeriodNotificationConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="FirebaseAdmin" Version="3.0.0"/>
<PackageReference Include="FirebaseAdmin" Version="3.0.0" />
<PackageReference Include="JWT" Version="10.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
Expand All @@ -14,16 +14,14 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.5.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="5.5.0" />
<PackageReference Include="Polly" Version="8.4.0"/>
<PackageReference Include="Polly" Version="8.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\BuildingBlocks\src\BuildingBlocks.Infrastructure\BuildingBlocks.Infrastructure.csproj" />
<ProjectReference Include="..\Devices.Application\Devices.Application.csproj" />
<ProjectReference Include="..\Devices.Domain\Devices.Domain.csproj">
<TreatAsUsed>true</TreatAsUsed>
</ProjectReference>
<ProjectReference Include="..\Devices.Domain\Devices.Domain.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Text.Json;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.BuildingBlocks.Infrastructure.Exceptions;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications;
Expand All @@ -14,20 +13,22 @@ namespace Backbone.Modules.Devices.Infrastructure.PushNotifications.DirectPush.A
public class ApplePushNotificationServiceConnector : IPnsConnector
{
private readonly IJwtGenerator _jwtGenerator;
private readonly IPushNotificationTextProvider _notificationTextProvider;
private readonly HttpClient _httpClient;
private readonly ILogger<ApplePushNotificationServiceConnector> _logger;
private readonly DirectPnsCommunicationOptions.ApnsOptions _options;

public ApplePushNotificationServiceConnector(IHttpClientFactory httpClientFactory, IOptions<DirectPnsCommunicationOptions.ApnsOptions> options, IJwtGenerator jwtGenerator,
ILogger<ApplePushNotificationServiceConnector> logger)
IPushNotificationTextProvider notificationTextProvider, ILogger<ApplePushNotificationServiceConnector> logger)
{
_httpClient = httpClientFactory.CreateClient();
_jwtGenerator = jwtGenerator;
_notificationTextProvider = notificationTextProvider;
_logger = logger;
_options = options.Value;
}

public async Task<SendResults> Send(IEnumerable<PnsRegistration> registrations, IdentityAddress recipient, object notification)
public async Task<SendResults> Send(IEnumerable<PnsRegistration> registrations, IdentityAddress recipient, IPushNotification notification)
{
ValidateRegistrations(registrations);

Expand All @@ -53,20 +54,20 @@ public void ValidateRegistration(PnsRegistration registration)
throw new InfrastructureException(InfrastructureErrors.InvalidPushNotificationConfiguration(_options.GetSupportedBundleIds()));
}

private async Task SendNotification(PnsRegistration registration, object notification, SendResults sendResults)
private async Task SendNotification(PnsRegistration registration, IPushNotification notification, SendResults sendResults)
{
var (notificationTitle, notificationBody) = GetNotificationText(notification);
var (notificationTitle, notificationBody) = await _notificationTextProvider.GetNotificationTextForDeviceId(notification.GetType(), registration.DeviceId);
var notificationId = GetNotificationId(notification);
var notificationContent = new NotificationContent(registration.IdentityAddress, registration.DevicePushIdentifier, notification);

var keyInformation = _options.GetKeyInformationForBundleId(registration.AppId);
var jwt = _jwtGenerator.Generate(keyInformation.PrivateKey, keyInformation.KeyId, keyInformation.TeamId, registration.AppId);

var request = new ApnsMessageBuilder(registration.AppId, BuildUrl(registration.Environment, registration.Handle.Value), jwt.Value)
.AddContent(notificationContent)
.SetNotificationText(notificationTitle, notificationBody)
.SetNotificationId(notificationId)
.Build();
.AddContent(notificationContent)
.SetNotificationText(notificationTitle, notificationBody)
.SetNotificationId(notificationId)
.Build();

_logger.LogDebug("Sending push notification (type '{eventName}') to '{address}' with handle '{handle}'.", notificationContent.EventName, registration.IdentityAddress, registration.Handle);

Expand Down Expand Up @@ -101,31 +102,4 @@ private static int GetNotificationId(object pushNotification)
var attribute = pushNotification.GetType().GetCustomAttribute<NotificationIdAttribute>();
return attribute?.Value ?? 0;
}

private static (string Title, string Body) GetNotificationText(object pushNotification)
{
switch (pushNotification)
{
case null:
return ("", "");
case JsonElement jsonElement:
{
var notification = jsonElement.Deserialize<NotificationTextAttribute>();
return notification == null ? ("", "") : (notification.Title, notification.Body);
}
default:
{
var attribute = pushNotification.GetType().GetCustomAttribute<NotificationTextAttribute>();
return attribute == null ? ("", "") : (attribute.Title, attribute.Body);
}
}
}
}

public static class TypeExtensions
{
public static T? GetCustomAttribute<T>(this Type type) where T : Attribute
{
return (T?)type.GetCustomAttribute(typeof(T));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public DirectPushService(IPnsRegistrationsRepository pnsRegistrationRepository,
_logger = logger;
}

public async Task SendNotification(IdentityAddress recipient, object notification, CancellationToken cancellationToken)
public async Task SendNotification(IdentityAddress recipient, IPushNotification notification, CancellationToken cancellationToken)
{
var registrations = await _pnsRegistrationsRepository.FindWithAddress(recipient, cancellationToken);

Expand Down
Loading

0 comments on commit 97839bc

Please sign in to comment.