diff --git a/subtrack.DAL/Entities/DateTimeSetting.cs b/subtrack.DAL/Entities/DateTimeSetting.cs index 1ab07d2..fadea33 100644 --- a/subtrack.DAL/Entities/DateTimeSetting.cs +++ b/subtrack.DAL/Entities/DateTimeSetting.cs @@ -4,6 +4,7 @@ public class DateTimeSetting : SettingsBase { public const string LastAutoPaymentTimeStampKey = "LastAutoPaymentTimeStamp"; public const string LastSubscriptionExportTimeStampKey = "LastSubscriptionExportTimeStamp"; + public const string LastSubscriptionReminderTimeStampKey = "LastSubscriptionReminderTimeStampKey"; public DateTime? Value { get; set; } } } diff --git a/subtrack.DAL/Entities/Subscription.cs b/subtrack.DAL/Entities/Subscription.cs index 5d70855..e3f4438 100644 --- a/subtrack.DAL/Entities/Subscription.cs +++ b/subtrack.DAL/Entities/Subscription.cs @@ -9,7 +9,7 @@ public class Subscription : ICloneable public int Id { get; set; } [Required] - [StringLength(50,ErrorMessage = "Service name should be less than 50 characters.")] + [StringLength(50, ErrorMessage = "Service name should be less than 50 characters.")] public string Name { get; set; } = null!; public string? Description { get; set; } public bool IsAutoPaid { get; set; } @@ -17,7 +17,7 @@ public class Subscription : ICloneable public decimal Cost { get; set; } [Required] - public int FirstPaymentDay { get; set; } + public int FirstPaymentDay { get; set; } [Required] public DateTime LastPayment { get; set; } @@ -32,6 +32,9 @@ public class Subscription : ICloneable public string SecondaryColor { get; set; } + [Range(0, 31, ErrorMessage = "Notification can be set up to 31 days before due date")] + public int? NotificationDays { get; set; } + public object Clone() { return (Subscription)MemberwiseClone(); diff --git a/subtrack.DAL/Migrations/20240509182319_NotificationDays.Designer.cs b/subtrack.DAL/Migrations/20240509182319_NotificationDays.Designer.cs new file mode 100644 index 0000000..d8824ab --- /dev/null +++ b/subtrack.DAL/Migrations/20240509182319_NotificationDays.Designer.cs @@ -0,0 +1,118 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using subtrack.DAL; + +#nullable disable + +namespace subtrack.DAL.Migrations +{ + [DbContext(typeof(SubtrackDbContext))] + [Migration("20240509182319_NotificationDays")] + partial class NotificationDays + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); + + modelBuilder.Entity("subtrack.DAL.Entities.SettingsBase", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("settings_type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + + b.HasDiscriminator("settings_type").HasValue("SettingsBase"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("subtrack.DAL.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BillingInterval") + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("BillingOccurrence") + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("Cost") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FirstPaymentDay") + .HasColumnType("INTEGER"); + + b.Property("Icon") + .HasColumnType("TEXT"); + + b.Property("IsAutoPaid") + .HasColumnType("INTEGER"); + + b.Property("LastPayment") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("NotificationDays") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .IsRequired() + .HasColumnType("TEXT") + .HasDefaultValue("#282828"); + + b.Property("SecondaryColor") + .IsRequired() + .HasColumnType("TEXT") + .HasDefaultValue("#2a9fd6"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("subtrack.DAL.Entities.DateTimeSetting", b => + { + b.HasBaseType("subtrack.DAL.Entities.SettingsBase"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("DateTimeSetting"); + + b.HasData( + new + { + Id = "LastAutoPaymentTimeStamp" + }, + new + { + Id = "LastSubscriptionExportTimeStamp" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/subtrack.DAL/Migrations/20240509182319_NotificationDays.cs b/subtrack.DAL/Migrations/20240509182319_NotificationDays.cs new file mode 100644 index 0000000..881371a --- /dev/null +++ b/subtrack.DAL/Migrations/20240509182319_NotificationDays.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace subtrack.DAL.Migrations +{ + /// + public partial class NotificationDays : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "NotificationDays", + table: "Subscriptions", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "NotificationDays", + table: "Subscriptions"); + } + } +} diff --git a/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.Designer.cs b/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.Designer.cs new file mode 100644 index 0000000..6b52792 --- /dev/null +++ b/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.Designer.cs @@ -0,0 +1,122 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using subtrack.DAL; + +#nullable disable + +namespace subtrack.DAL.Migrations +{ + [DbContext(typeof(SubtrackDbContext))] + [Migration("20240510155420_LastReminderTimestamp")] + partial class LastReminderTimestamp + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.14"); + + modelBuilder.Entity("subtrack.DAL.Entities.SettingsBase", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("settings_type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + + b.HasDiscriminator("settings_type").HasValue("SettingsBase"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("subtrack.DAL.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BillingInterval") + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("BillingOccurrence") + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("Cost") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FirstPaymentDay") + .HasColumnType("INTEGER"); + + b.Property("Icon") + .HasColumnType("TEXT"); + + b.Property("IsAutoPaid") + .HasColumnType("INTEGER"); + + b.Property("LastPayment") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("NotificationDays") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .IsRequired() + .HasColumnType("TEXT") + .HasDefaultValue("#282828"); + + b.Property("SecondaryColor") + .IsRequired() + .HasColumnType("TEXT") + .HasDefaultValue("#2a9fd6"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("subtrack.DAL.Entities.DateTimeSetting", b => + { + b.HasBaseType("subtrack.DAL.Entities.SettingsBase"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasDiscriminator().HasValue("DateTimeSetting"); + + b.HasData( + new + { + Id = "LastAutoPaymentTimeStamp" + }, + new + { + Id = "LastSubscriptionExportTimeStamp" + }, + new + { + Id = "LastSubscriptionReminderTimeStampKey" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.cs b/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.cs new file mode 100644 index 0000000..288df5f --- /dev/null +++ b/subtrack.DAL/Migrations/20240510155420_LastReminderTimestamp.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace subtrack.DAL.Migrations +{ + /// + public partial class LastReminderTimestamp : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Settings", + columns: new[] { "Id", "Value", "settings_type" }, + values: new object[] { "LastSubscriptionReminderTimeStampKey", null, "DateTimeSetting" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Settings", + keyColumn: "Id", + keyValue: "LastSubscriptionReminderTimeStampKey"); + } + } +} diff --git a/subtrack.DAL/Migrations/SubtrackDbContextModelSnapshot.cs b/subtrack.DAL/Migrations/SubtrackDbContextModelSnapshot.cs index 8e5e2e0..b9df5d6 100644 --- a/subtrack.DAL/Migrations/SubtrackDbContextModelSnapshot.cs +++ b/subtrack.DAL/Migrations/SubtrackDbContextModelSnapshot.cs @@ -72,6 +72,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("TEXT"); + b.Property("NotificationDays") + .HasColumnType("INTEGER"); + b.Property("PrimaryColor") .IsRequired() .HasColumnType("TEXT") @@ -104,6 +107,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = "LastSubscriptionExportTimeStamp" + }, + new + { + Id = "LastSubscriptionReminderTimeStampKey" }); }); #pragma warning restore 612, 618 diff --git a/subtrack.DAL/SubtrackDbContext.cs b/subtrack.DAL/SubtrackDbContext.cs index b7ec703..b6fd100 100644 --- a/subtrack.DAL/SubtrackDbContext.cs +++ b/subtrack.DAL/SubtrackDbContext.cs @@ -19,6 +19,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasData(new DateTimeSetting { Id = DateTimeSetting.LastAutoPaymentTimeStampKey, Value = null }); modelBuilder.Entity().HasData(new DateTimeSetting { Id = DateTimeSetting.LastSubscriptionExportTimeStampKey, Value = null }); + modelBuilder.Entity().HasData(new DateTimeSetting { Id = DateTimeSetting.LastSubscriptionReminderTimeStampKey, Value = null }); modelBuilder.Entity(entity => { diff --git a/subtrack.MAUI/MauiProgram.cs b/subtrack.MAUI/MauiProgram.cs index bb914e1..eb1eced 100644 --- a/subtrack.MAUI/MauiProgram.cs +++ b/subtrack.MAUI/MauiProgram.cs @@ -101,9 +101,9 @@ private static void SeedDb(SubtrackDbContext dbContext) var todayLastMonth = DateTime.Now.AddMonths(-1); var availableBackgroundColors = CssUtil.AvailableBackgroundColors; dbContext.Subscriptions.AddRange( - new DAL.Entities.Subscription() { Name = "paramount", LastPayment = todayLastMonth.AddDays(-1), FirstPaymentDay = todayLastMonth.AddDays(-1).Day, Cost = 3m, BillingOccurrence = DAL.Entities.BillingOccurrence.Week, BillingInterval = 2, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-tv" }, - new DAL.Entities.Subscription() { Name = "Disney+", LastPayment = todayLastMonth, FirstPaymentDay = todayLastMonth.Day, Cost = 3m, BillingOccurrence = DAL.Entities.BillingOccurrence.Month, BillingInterval = 1, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-circle-play" }, - new DAL.Entities.Subscription() { Name = "Netflix Premium Plan", LastPayment = DateTime.Now.AddDays(-1), FirstPaymentDay = DateTime.Now.AddDays(-1).Day, IsAutoPaid = true, Description = "family plan", Cost = 1000, BillingOccurrence = DAL.Entities.BillingOccurrence.Month, BillingInterval = 1, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6" }, + new DAL.Entities.Subscription() { Name = "paramount", LastPayment = todayLastMonth.AddDays(-1), FirstPaymentDay = todayLastMonth.AddDays(-1).Day, Cost = 3m, BillingOccurrence = DAL.Entities.BillingOccurrence.Week, BillingInterval = 2, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-tv", NotificationDays = 0 }, + new DAL.Entities.Subscription() { Name = "Disney+", LastPayment = todayLastMonth, FirstPaymentDay = todayLastMonth.Day, Cost = 3m, BillingOccurrence = DAL.Entities.BillingOccurrence.Month, BillingInterval = 1, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-circle-play", NotificationDays = 0 }, + new DAL.Entities.Subscription() { Name = "Netflix Premium Plan", LastPayment = DateTime.Now.AddDays(-1), FirstPaymentDay = DateTime.Now.AddDays(-1).Day, IsAutoPaid = true, Description = "family plan", Cost = 1000, BillingOccurrence = DAL.Entities.BillingOccurrence.Month, BillingInterval = 1, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", NotificationDays = 1 }, new DAL.Entities.Subscription() { Name = "Something Family Plan", LastPayment = DateTime.Now.AddDays(-150), FirstPaymentDay = DateTime.Now.AddDays(-150).Day, IsAutoPaid = false, Description = "family plan", Cost = 1000, BillingOccurrence = DAL.Entities.BillingOccurrence.Week, BillingInterval = 2, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-shield" }, new DAL.Entities.Subscription() { Name = "hbo", LastPayment = DateTime.Now, FirstPaymentDay = DateTime.Now.Day, Cost = 1.5m, BillingOccurrence = DAL.Entities.BillingOccurrence.Year, BillingInterval = 1, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6" }, new DAL.Entities.Subscription() { Name = "hulu Standard plan", LastPayment = DateTime.Now.AddMonths(-2), FirstPaymentDay = 1, Cost = 1.5m, IsAutoPaid = true, BillingOccurrence = DAL.Entities.BillingOccurrence.Month, BillingInterval = 3, PrimaryColor = availableBackgroundColors.First(), SecondaryColor = "#2a9fd6", Icon = "fa fa-circle-play" } diff --git a/subtrack.MAUI/Pages/UpsertSubscription.razor b/subtrack.MAUI/Pages/UpsertSubscription.razor index ff4b49a..9a8f01b 100644 --- a/subtrack.MAUI/Pages/UpsertSubscription.razor +++ b/subtrack.MAUI/Pages/UpsertSubscription.razor @@ -1,11 +1,14 @@ @page "/CreateSubscription" @page "/EditSubscription/{Id:int}" + @using System.Globalization; @using subtrack.MAUI.Shared.Components @using subtrack.MAUI.Shared.JsInterop; + @inject NavigationManager NavigationManager @inject ISubscriptionService SubscriptionService @implements IAsyncDisposable +
- +
@@ -66,6 +69,20 @@ + +
+ + +
+ @if (_isNotificationsEnabled) + { +
+ Notify + + days before due +
+ } +
@@ -83,6 +100,7 @@ [SupplyParameterFromQuery] public string ReturnUrl { get; set; } + bool _isNotificationsEnabled = false; string _currency = ""; private ElementReference costInputElement; @@ -106,10 +124,13 @@ protected override async Task OnParametersSetAsync() { - if (Id != null) + if (Id == null) { - _subscription = await SubscriptionService.GetByIdIfExists(Id.Value); + return; } + + _subscription = await SubscriptionService.GetByIdIfExists(Id.Value); + _isNotificationsEnabled = _subscription.NotificationDays.HasValue; } protected async Task HightlightHandler() => await HighlightJsInterop.HighLight(costInputElement); @@ -140,6 +161,12 @@ return billingOccurrence.ToQuantity(_subscription.BillingInterval, showQuantityAs: ShowQuantityAs.None); } + private async Task OnNotificationsEnabledChanged() + { + _subscription.NotificationDays = _isNotificationsEnabled ? 0 : null; + await NotificationsUtil.EnsureNotificationsAreEnabled(); + } + private void UpdateBackgroundColor(ChangeEventArgs e) => _subscription.PrimaryColor = e.Value!.ToString(); private void UpdateSecondaryColor(ChangeEventArgs e) => _subscription.SecondaryColor = e.Value!.ToString(); diff --git a/subtrack.MAUI/Services/Abstractions/IDateProvider.cs b/subtrack.MAUI/Services/Abstractions/IDateProvider.cs index 2141f9a..e16e2c5 100644 --- a/subtrack.MAUI/Services/Abstractions/IDateProvider.cs +++ b/subtrack.MAUI/Services/Abstractions/IDateProvider.cs @@ -3,5 +3,6 @@ public interface IDateProvider { public DateTime Today { get; } + public DateTime Now { get; } } } diff --git a/subtrack.MAUI/Services/Android/NotifyDueSubscriptionsJob.cs b/subtrack.MAUI/Services/Android/NotifyDueSubscriptionsJob.cs index 3c8599b..1077f17 100644 --- a/subtrack.MAUI/Services/Android/NotifyDueSubscriptionsJob.cs +++ b/subtrack.MAUI/Services/Android/NotifyDueSubscriptionsJob.cs @@ -1,5 +1,9 @@ -using Plugin.LocalNotification; +using Humanizer; +using Plugin.LocalNotification; using Shiny.Jobs; +using subtrack.DAL.Entities; +using subtrack.MAUI.Services.Abstractions; +using subtrack.MAUI.Utilities; namespace subtrack.MAUI.Services.Android; @@ -13,15 +17,6 @@ public NotifyDueSubscriptionsJob(IServiceScopeFactory serviceScopeFactory) _serviceScopeFactory = serviceScopeFactory; } - private static async Task EnsureNotificationsAreEnabled() - { - var hasEnabledNotifications = await LocalNotificationCenter.Current.AreNotificationsEnabled(); - if (!hasEnabledNotifications) - { - await LocalNotificationCenter.Current.RequestNotificationPermission(); - } - } - private static void SendNotification(int notificationId, string title, string group) { var sampleNotification = new NotificationRequest() @@ -42,16 +37,66 @@ public async Task Run(JobInfo jobInfo, CancellationToken cancellationToken) if (cancellationToken.IsCancellationRequested) break; - // jobs are singletons, use this to create a scope for fetching transient/scoped dependencies using var scope = _serviceScopeFactory.CreateScope(); + var serviceProvider = scope.ServiceProvider; + var settingsService = serviceProvider.GetRequiredService(); + var dateProvider = serviceProvider.GetRequiredService(); + + var lastSubscriptionReminderTimeStamp = await settingsService.GetByIdAsync(DateTimeSetting.LastSubscriptionReminderTimeStampKey); - await EnsureNotificationsAreEnabled(); + var now = dateProvider.Now; + var today = dateProvider.Today; - var notificationGroup = DateTime.Today.Day.ToString(); - SendNotification(1, "Netflix is due in 2 days", notificationGroup); - SendNotification(2, "Missed payment for disney", notificationGroup); + if (lastSubscriptionReminderTimeStamp.Value?.Date != today) + { + await NotifyDueSubscriptionsJob.RunInternal(serviceProvider, today); - await Task.Delay(TimeSpan.FromHours(12), cancellationToken); + lastSubscriptionReminderTimeStamp.Value = now; + await settingsService.UpdateAsync(lastSubscriptionReminderTimeStamp); + } + + var tomorrow5PM = today.AddDays(1).AddHours(17); + var timeUntilTomorrow5PM = tomorrow5PM - now; + + await Task.Delay(timeUntilTomorrow5PM, cancellationToken); } } + + private static async Task RunInternal(IServiceProvider serviceProvider, DateTime today) + { + var subscriptionService = serviceProvider.GetRequiredService(); + var subscriptionsCalculator = serviceProvider.GetRequiredService(); + + var subscriptions = await subscriptionService.GetAllAsync(); + var subscriptionsWithNotificationsEnabled = subscriptions.Where(x => x.NotificationDays.HasValue).ToList(); + if (!subscriptionsWithNotificationsEnabled.Any()) + { + return; + } + + await NotificationsUtil.EnsureNotificationsAreEnabled(); + + var notificationGroup = today.Day.ToString(); + subscriptionsWithNotificationsEnabled.ForEach(sub => + { + var dueDate = subscriptionsCalculator.GetNextPaymentDate(sub); + var timeUntilNextPayment = dueDate.Subtract(today); + var dueDays = timeUntilNextPayment.Days; + + if (dueDays == sub.NotificationDays) + { + SendNotification(sub.Id, $"{sub.Name} is due {GetDueDaysText(dueDays)} ({dueDate.DayOfWeek.Humanize(LetterCasing.LowerCase)})", notificationGroup); + } + }); + } + + private static string GetDueDaysText(int dueDays) + { + return dueDays switch + { + 0 => "today", + 1 => "tomorrow", + _ => $"in {dueDays} days" + }; + } } diff --git a/subtrack.MAUI/Services/DateProvider.cs b/subtrack.MAUI/Services/DateProvider.cs index 678d66e..0e236f3 100644 --- a/subtrack.MAUI/Services/DateProvider.cs +++ b/subtrack.MAUI/Services/DateProvider.cs @@ -5,5 +5,6 @@ namespace subtrack.MAUI.Services public class DateProvider : IDateProvider { public DateTime Today => DateTime.Today; + public DateTime Now => DateTime.Now; } } diff --git a/subtrack.MAUI/Services/SubscriptionService.cs b/subtrack.MAUI/Services/SubscriptionService.cs index bf3dbad..91543d7 100644 --- a/subtrack.MAUI/Services/SubscriptionService.cs +++ b/subtrack.MAUI/Services/SubscriptionService.cs @@ -45,6 +45,7 @@ public async Task Update(Subscription subscriptionToUpdate) sub.PrimaryColor = subscriptionToUpdate.PrimaryColor; sub.SecondaryColor = subscriptionToUpdate.SecondaryColor; sub.Icon = subscriptionToUpdate.Icon; + sub.NotificationDays = subscriptionToUpdate.NotificationDays; AutoPay(sub); sub.Cost = subscriptionToUpdate.Cost; diff --git a/subtrack.MAUI/Utilities/NotificationsUtil.cs b/subtrack.MAUI/Utilities/NotificationsUtil.cs new file mode 100644 index 0000000..007a4ac --- /dev/null +++ b/subtrack.MAUI/Utilities/NotificationsUtil.cs @@ -0,0 +1,14 @@ +using Plugin.LocalNotification; + +namespace subtrack.MAUI.Utilities; +public static class NotificationsUtil +{ + public static async Task EnsureNotificationsAreEnabled() + { + var hasEnabledNotifications = await LocalNotificationCenter.Current.AreNotificationsEnabled(); + if (!hasEnabledNotifications) + { + await LocalNotificationCenter.Current.RequestNotificationPermission(); + } + } +} diff --git a/subtrack.Tests/Integration/SubscriptionImporterTests.cs b/subtrack.Tests/Integration/SubscriptionImporterTests.cs index ffbab8e..dd7de2d 100644 --- a/subtrack.Tests/Integration/SubscriptionImporterTests.cs +++ b/subtrack.Tests/Integration/SubscriptionImporterTests.cs @@ -15,8 +15,8 @@ public SubscriptionImporterTests() [Fact] public async Task ImportCsv_InvalidCsv_ShouldThrow() { - var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor -Subscription1,Description1,True,29.99,1,2023-12-01,Monthly,1,,,,,, + var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor,NotificationDays +Subscription1,Description1,True,29.99,1,2023-12-01,Monthly,1,,,,,,,, "; var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); @@ -26,9 +26,9 @@ public async Task ImportCsv_InvalidCsv_ShouldThrow() [Fact] public async Task ImportCsv_EmptyValuesForOptionalProperties_ShouldReturnImportedItems() { - var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor -Subscription1,Description1,True,29.99,1,2023-12-01,Month,1,,,,,, -Subscription2,,True,29.99,1,2023-12-01,Month,1,,,,,, + var csvText = @"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor,NotificationDays +Subscription1,Description1,True,29.99,1,2023-12-01,Month,1,,,,,,,, +Subscription2,,True,29.99,1,2023-12-01,Month,1,,,,,,,, "; var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText)); @@ -41,10 +41,10 @@ public async Task ImportCsv_EmptyValuesForOptionalProperties_ShouldReturnImporte public async Task ImportCsv_WithSameDetailsAsExisting_ShouldReturnImportedSubs() { var existingSubscription = CreateSubscription(DateTime.Today, description: null); - var csvText = $@"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor + var csvText = $@"Name,Description,IsAutoPaid,Cost,FirstPaymentDay,LastPayment,BillingOccurrence,BillingInterval,PrimaryColor,Icon,SecondaryColor,NotificationDays {existingSubscription.Name},,{existingSubscription.IsAutoPaid},{existingSubscription.Cost},{existingSubscription.FirstPaymentDay},{existingSubscription.LastPayment},{existingSubscription.BillingOccurrence},{existingSubscription.BillingInterval},,,,,, -Subscription1,,True,29.99,1,2023-12-01,Month,1,,,,,, -Subscription2,,False,29.99,1,2022-12-01,Week,1,,,,,, +Subscription1,,True,29.99,1,2023-12-01,Month,1,,,,,,,, +Subscription2,,False,29.99,1,2022-12-01,Week,1,,,,,,,, "; var csv = new MemoryStream(Encoding.UTF8.GetBytes(csvText));