Skip to content

Commit

Permalink
feat(push): use APNs instead of FCM for Apple devices (#219)
Browse files Browse the repository at this point in the history
* feat(push): use APNs instead of FCM for Apple devices

* fix(push): use CertContent and singletons

* feat(push): enable use of APNs dev server

* refactor(push): move expo to separate configuration

* refactor(push): improved DRYness and documentation
  • Loading branch information
Fenrikur authored Sep 6, 2024
1 parent aa85bde commit c4b94bc
Show file tree
Hide file tree
Showing 17 changed files with 626 additions and 84 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
appsettings.json
appsettings-backoffice.json
firebase.json
*.p8

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Configuration;

namespace Eurofurence.App.Server.Services.Abstractions.PushNotifications
{
public class ApnsConfiguration
{
public bool IsConfigured => !string.IsNullOrWhiteSpace(BundleId) && !string.IsNullOrWhiteSpace(CertContent) && !string.IsNullOrWhiteSpace(KeyId) && !string.IsNullOrWhiteSpace(TeamId);
public string BundleId { get; set; }
public string CertFilePath { get; set; }
public string CertContent { get; set; }
public string KeyId { get; set; }
public string TeamId { get; set; }
public bool UseDevelopmentServer { get; set; }

public static ApnsConfiguration FromConfiguration(IConfiguration configuration)
=> new ApnsConfiguration
{
BundleId = configuration["push:apns:bundleId"],
CertContent = configuration["push:apns:certContent"],
KeyId = configuration["push:apns:keyId"],
TeamId = configuration["push:apns:teamId"],
UseDevelopmentServer = bool.TryParse(configuration["push:apns:useDevelopmentServer"], out bool shouldUseDevelopmentServer) ? shouldUseDevelopmentServer : true,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;

namespace Eurofurence.App.Server.Services.Abstractions.PushNotifications
{
public class ExpoConfiguration
{
public bool IsConfigured => !string.IsNullOrWhiteSpace(ExperienceId) && !string.IsNullOrWhiteSpace(ScopeKey);
public string ExperienceId { get; set; }
public string ScopeKey { get; set; }

public static ExpoConfiguration FromConfiguration(IConfiguration configuration)
=> new ExpoConfiguration
{
ExperienceId = configuration["push:expo:experienceId"],
ScopeKey = configuration["push:expo:scopeKey"]
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ namespace Eurofurence.App.Server.Services.Abstractions.PushNotifications
{
public class FirebaseConfiguration
{
public bool IsConfigured => !string.IsNullOrEmpty(GoogleServiceCredentialKeyFile);
public bool IsConfigured => !string.IsNullOrWhiteSpace(GoogleServiceCredentialKeyFile);
public string GoogleServiceCredentialKeyFile { get; set; }
public string ExpoExperienceId { get; set; }
public string ExpoScopeKey { get; set; }
public string[] FirebaseTopics { get; set; }

public static FirebaseConfiguration FromConfiguration(IConfiguration configuration)
=> new FirebaseConfiguration
{
GoogleServiceCredentialKeyFile = configuration["firebase:googleServiceCredentialKeyFile"],
ExpoExperienceId = configuration["firebase:expo:experienceId"],
ExpoScopeKey = configuration["firebase:expo:scopeKey"],
FirebaseTopics = configuration.GetSection("firebase:topics").Get<string[]>()
GoogleServiceCredentialKeyFile = configuration["push:firebase:googleServiceCredentialKeyFile"]
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Eurofurence.App.Server.Services.Abstractions.PushNotifications
{
public interface IFirebaseChannelManager
public interface IPushNotificationChannelManager
{
Task RegisterDeviceAsync(
string deviceToken,
Expand All @@ -23,15 +23,15 @@ Task PushAnnouncementNotificationAsync(

Task PushPrivateMessageNotificationToIdentityIdAsync(
string identityId,
string toastTitle,
string toastMessage,
string title,
string message,
Guid relatedId,
CancellationToken cancellationToken = default);

Task PushPrivateMessageNotificationToRegSysIdAsync(
string regSysId,
string toastTitle,
string toastMessage,
string title,
string message,
Guid relatedId,
CancellationToken cancellationToken = default);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ namespace Eurofurence.App.Server.Services.Communication
public class PrivateMessageService : EntityServiceBase<PrivateMessageRecord>, IPrivateMessageService
{
private readonly AppDbContext _appDbContext;
private readonly IFirebaseChannelManager _firebaseChannelManager;
private readonly IPushNotificationChannelManager _pushNotificationChannelManager;

private readonly ConcurrentQueue<QueuedNotificationParameters> _notificationQueue = new();

public PrivateMessageService(
AppDbContext appDbContext,
IStorageServiceFactory storageServiceFactory,
IFirebaseChannelManager firebaseChannelManager
IPushNotificationChannelManager pushNotificationChannelManager
)
: base(appDbContext, storageServiceFactory)
{
_appDbContext = appDbContext;
_firebaseChannelManager = firebaseChannelManager;
_pushNotificationChannelManager = pushNotificationChannelManager;
}

public async Task<List<PrivateMessageRecord>> GetPrivateMessagesForRecipientAsync(
Expand Down Expand Up @@ -192,7 +192,7 @@ public async Task<int> FlushPrivateMessageQueueNotifications(
{
if (!string.IsNullOrWhiteSpace(parameters.RecipientRegSysId))
{
await _firebaseChannelManager.PushPrivateMessageNotificationToRegSysIdAsync(
await _pushNotificationChannelManager.PushPrivateMessageNotificationToRegSysIdAsync(
parameters.RecipientRegSysId,
parameters.ToastTitle,
parameters.ToastMessage,
Expand All @@ -202,7 +202,7 @@ await _firebaseChannelManager.PushPrivateMessageNotificationToRegSysIdAsync(
}
else
{
await _firebaseChannelManager.PushPrivateMessageNotificationToIdentityIdAsync(
await _pushNotificationChannelManager.PushPrivateMessageNotificationToIdentityIdAsync(
parameters.RecipientIdentityId,
parameters.ToastTitle,
parameters.ToastMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ private void RegisterConfigurations(ContainerBuilder builder)

builder.RegisterInstance(ConventionSettings.FromConfiguration(_configuration));
builder.RegisterInstance(FirebaseConfiguration.FromConfiguration(_configuration));
builder.RegisterInstance(ApnsConfiguration.FromConfiguration(_configuration));
builder.RegisterInstance(ExpoConfiguration.FromConfiguration(_configuration));
builder.RegisterInstance(TelegramConfiguration.FromConfiguration(_configuration));
builder.RegisterInstance(CollectionGameConfiguration.FromConfiguration(_configuration));
builder.RegisterInstance(ArtistAlleyConfiguration.FromConfiguration(_configuration));
Expand Down Expand Up @@ -106,7 +108,8 @@ private void RegisterServices(ContainerBuilder builder)
builder.RegisterType<EventConferenceTrackService>().As<IEventConferenceTrackService>();
builder.RegisterType<EventFeedbackService>().As<IEventFeedbackService>();
builder.RegisterType<EventService>().As<IEventService>();
builder.RegisterType<FirebaseChannelManager>().As<IFirebaseChannelManager>();
//builder.RegisterType<FirebaseChannelManager>().As<IPushNotificationChannelManager>();
builder.RegisterType<PushNotificationChannelManager>().As<IPushNotificationChannelManager>().SingleInstance();
builder.RegisterType<FursuitBadgeService>().As<IFursuitBadgeService>();
builder.RegisterType<GanssHtmlSanitizer>().As<IHtmlSanitizer>();
builder.RegisterType<ImageService>().As<IImageService>();
Expand All @@ -118,7 +121,7 @@ private void RegisterServices(ContainerBuilder builder)
builder.RegisterType<LostAndFoundService>().As<ILostAndFoundService>();
builder.RegisterType<LostAndFoundLassieImporter>().As<ILostAndFoundLassieImporter>();
builder.RegisterType<MapService>().As<IMapService>();
builder.RegisterType<PrivateMessageService>().As<IPrivateMessageService>();
builder.RegisterType<PrivateMessageService>().As<IPrivateMessageService>().SingleInstance();
builder.RegisterType<PushNotificationChannelStatisticsService>().As<IPushNotificationChannelStatisticsService>();
builder.RegisterType<QrCodeService>().As<IQrCodeService>();
builder.RegisterType<StorageServiceFactory>().As<IStorageServiceFactory>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@
<ItemGroup>
<PackageReference Include="Autofac" Version="8.1.0" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="dotAPNS" Version="4.4.1" />
<PackageReference Include="dotAPNS.AspNetCore" Version="2.2.2" />
<PackageReference Include="FirebaseAdmin" Version="3.0.0" />
<PackageReference Include="HtmlSanitizer" Version="8.1.870" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Minio" Version="6.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.2" />
<PackageReference Include="Minio" Version="6.0.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Telegram.Bot" Version="19.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="HtmlSanitizer" Version="8.1.870" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@

namespace Eurofurence.App.Server.Services.PushNotifications
{
public class FirebaseChannelManager : IFirebaseChannelManager
public class FirebaseChannelManager : IPushNotificationChannelManager
{
private readonly IDeviceIdentityService _deviceService;
private readonly IRegistrationIdentityService _registrationService;
private readonly AppDbContext _appDbContext;
private readonly FirebaseConfiguration _configuration;
private readonly ExpoConfiguration _expoConfiguration;
private readonly ConventionSettings _conventionSettings;
private readonly FirebaseMessaging _firebaseMessaging;

Expand All @@ -31,12 +32,14 @@ public FirebaseChannelManager(
IRegistrationIdentityService registrationService,
AppDbContext appDbContext,
FirebaseConfiguration configuration,
ExpoConfiguration expoConfiguration,
ConventionSettings conventionSettings)
{
_deviceService = deviceService;
_registrationService = registrationService;
_appDbContext = appDbContext;
_configuration = configuration;
_expoConfiguration = expoConfiguration;
_conventionSettings = conventionSettings;

if (_configuration.GoogleServiceCredentialKeyFile is not { Length: > 0 } file) return;
Expand Down Expand Up @@ -79,8 +82,8 @@ public Task PushAnnouncementNotificationAsync(
{ "CID", _conventionSettings.ConventionIdentifier },

// For Expo / React Native
{ "experienceId", _configuration.ExpoExperienceId },
{ "scopeKey", _configuration.ExpoScopeKey },
{ "experienceId", _expoConfiguration.ExperienceId },
{ "scopeKey", _expoConfiguration.ScopeKey },
}
};

Expand Down Expand Up @@ -116,8 +119,8 @@ public Task PushAnnouncementNotificationAsync(

public async Task PushPrivateMessageNotificationToIdentityIdAsync(
string identityId,
string toastTitle,
string toastMessage,
string title,
string message,
Guid relatedId,
CancellationToken cancellationToken = default)
{
Expand All @@ -126,17 +129,17 @@ public async Task PushPrivateMessageNotificationToIdentityIdAsync(
var devices = await _deviceService.FindByIdentityId(identityId, cancellationToken);

await PushPrivateMessageNotificationAsync(devices,
toastTitle,
toastMessage,
title,
message,
relatedId,
cancellationToken
);
}

public async Task PushPrivateMessageNotificationToRegSysIdAsync(
string regSysId,
string toastTitle,
string toastMessage,
string title,
string message,
Guid relatedId,
CancellationToken cancellationToken = default)
{
Expand All @@ -145,8 +148,8 @@ public async Task PushPrivateMessageNotificationToRegSysIdAsync(
var devices = await _deviceService.FindByRegSysId(regSysId, cancellationToken);

await PushPrivateMessageNotificationAsync(devices,
toastTitle,
toastMessage,
title,
message,
relatedId,
cancellationToken
);
Expand All @@ -166,8 +169,8 @@ public Task PushSyncRequestAsync(CancellationToken cancellationToken = default)
{ "CID", _conventionSettings.ConventionIdentifier },

// For Expo / React Native
{ "experienceId", _configuration.ExpoExperienceId },
{ "scopeKey", _configuration.ExpoScopeKey },
{ "experienceId", _expoConfiguration.ExperienceId },
{ "scopeKey", _expoConfiguration.ScopeKey },
{
"body", JsonSerializer.Serialize(new
{
Expand Down Expand Up @@ -268,8 +271,8 @@ await _deviceService.InsertOneAsync(new DeviceIdentityRecord

private async Task PushPrivateMessageNotificationAsync(
List<DeviceIdentityRecord> devices,
string toastTitle,
string toastMessage,
string title,
string message,
Guid relatedId,
CancellationToken cancellationToken = default)
{
Expand All @@ -287,8 +290,8 @@ private async Task PushPrivateMessageNotificationAsync(
{
Notification = new AndroidNotification()
{
Title = toastTitle,
Body = toastMessage,
Title = title,
Body = message,
Icon = "notification_icon",
Color = "#006459"
}
Expand All @@ -297,14 +300,14 @@ private async Task PushPrivateMessageNotificationAsync(
{
// For Legacy Native Android App
{ "Event", "Notification" },
{ "Title", toastTitle },
{ "Message", toastMessage },
{ "Title", title },
{ "Message", message },
{ "RelatedId", relatedId.ToString() },
{ "CID", _conventionSettings.ConventionIdentifier },

// For Expo / React Native
{ "experienceId", _configuration.ExpoExperienceId },
{ "scopeKey", _configuration.ExpoScopeKey }
{ "experienceId", _expoConfiguration.ExperienceId },
{ "scopeKey", _expoConfiguration.ScopeKey }
}
});
break;
Expand All @@ -320,8 +323,8 @@ private async Task PushPrivateMessageNotificationAsync(
},
Notification = new Notification()
{
Title = toastTitle,
Body = toastMessage,
Title = title,
Body = message,
},
Apns = new ApnsConfig()
{
Expand Down
Loading

0 comments on commit c4b94bc

Please sign in to comment.