From abada481b71b9eef1118fdd27809540098935655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Fri, 7 Oct 2022 12:06:57 +0100 Subject: [PATCH] [SG-702] Tapping Push Notification does not open the account the request is for (#2112) * [SG-702] Tap notification now switches accounts if it is a passwordless notification. * [SG-702] Fix compilation errors * [SG-702] Fixed iOS notification tap fix * [SG-702] Notification data model * [SG-702] Change method signature with object containing properties. PR fixes. --- src/Android/MainActivity.cs | 13 +++++++ .../AndroidPushNotificationService.cs | 17 ++++++--- .../IPushNotificationListenerService.cs | 2 ++ .../Abstractions/IPushNotificationService.cs | 3 +- src/App/Models/NotificationData.cs | 23 ++++++++++++ .../NoopPushNotificationListenerService.cs | 6 ++++ .../Services/NoopPushNotificationService.cs | 3 +- .../PushNotificationListenerService.cs | 35 ++++++++++++++++++- src/Core/Constants.cs | 2 ++ .../Services/iOSPushNotificationHandler.cs | 20 +++++++++-- .../Services/iOSPushNotificationService.cs | 16 +++++++-- 11 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 src/App/Models/NotificationData.cs diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 55eacba73..9ff6afc42 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -20,6 +20,8 @@ using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Droid.Receivers; using Bit.Droid.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xamarin.Essentials; using ZXing.Net.Mobile.Android; using FileProvider = AndroidX.Core.Content.FileProvider; @@ -39,6 +41,7 @@ namespace Bit.Droid private IStateService _stateService; private IAppIdService _appIdService; private IEventService _eventService; + private IPushNotificationListenerService _pushNotificationListenerService; private ILogger _logger; private PendingIntent _eventUploadPendingIntent; private AppOptions _appOptions; @@ -61,6 +64,7 @@ namespace Bit.Droid _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _eventService = ServiceContainer.Resolve("eventService"); + _pushNotificationListenerService = ServiceContainer.Resolve(); _logger = ServiceContainer.Resolve("logger"); TabLayoutResource = Resource.Layout.Tabbar; @@ -145,6 +149,15 @@ namespace Bit.Droid AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this) .GetAwaiter() .GetResult(); + + if (Intent?.GetStringExtra(Constants.NotificationData) is string notificationDataJson) + { + var notificationType = JToken.Parse(notificationDataJson).SelectToken(Constants.NotificationDataType); + if (notificationType.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.OnNotificationTapped(JsonConvert.DeserializeObject(notificationDataJson)).FireAndForget(); + } + } } protected override void OnNewIntent(Intent intent) diff --git a/src/Android/Services/AndroidPushNotificationService.cs b/src/Android/Services/AndroidPushNotificationService.cs index 9365b19ec..97e5c2daf 100644 --- a/src/Android/Services/AndroidPushNotificationService.cs +++ b/src/Android/Services/AndroidPushNotificationService.cs @@ -1,14 +1,17 @@ #if !FDROID using System; +using System.Collections.Generic; using System.Threading.Tasks; using Android.App; using Android.Content; using Android.OS; using AndroidX.Core.App; using Bit.App.Abstractions; +using Bit.App.Models; using Bit.Core; using Bit.Core.Abstractions; using Bit.Droid.Utilities; +using Newtonsoft.Json; using Xamarin.Forms; namespace Bit.Droid.Services @@ -67,28 +70,34 @@ namespace Bit.Droid.Services } } - public void SendLocalNotification(string title, string message, string notificationId) + public void SendLocalNotification(string title, string message, BaseNotificationData data) { - if (string.IsNullOrEmpty(notificationId)) + if (string.IsNullOrEmpty(data.Id)) { throw new ArgumentNullException("notificationId cannot be null or empty."); } var context = Android.App.Application.Context; var intent = new Intent(context, typeof(MainActivity)); + intent.PutExtra(Constants.NotificationData, JsonConvert.SerializeObject(data)); + var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true); var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags); var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId) .SetContentIntent(pendingIntent) .SetContentTitle(title) .SetContentText(message) - .SetTimeoutAfter(Constants.PasswordlessNotificationTimeoutInMinutes * 60000) .SetSmallIcon(Resource.Drawable.ic_notification) .SetColor((int)Android.Graphics.Color.White) .SetAutoCancel(true); + if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0) + { + builder.SetTimeoutAfter(passwordlessNotificationData.TimeoutInMinutes * 60000); + } + var notificationManager = NotificationManagerCompat.From(context); - notificationManager.Notify(int.Parse(notificationId), builder.Build()); + notificationManager.Notify(int.Parse(data.Id), builder.Build()); } } } diff --git a/src/App/Abstractions/IPushNotificationListenerService.cs b/src/App/Abstractions/IPushNotificationListenerService.cs index 4a57c75a5..f5d2cb39f 100644 --- a/src/App/Abstractions/IPushNotificationListenerService.cs +++ b/src/App/Abstractions/IPushNotificationListenerService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Bit.App.Models; using Newtonsoft.Json.Linq; namespace Bit.App.Abstractions @@ -9,6 +10,7 @@ namespace Bit.App.Abstractions Task OnRegisteredAsync(string token, string device); void OnUnregistered(string device); void OnError(string message, string device); + Task OnNotificationTapped(BaseNotificationData data); bool ShouldShowNotification(); } } diff --git a/src/App/Abstractions/IPushNotificationService.cs b/src/App/Abstractions/IPushNotificationService.cs index f0d56691e..5d69be1aa 100644 --- a/src/App/Abstractions/IPushNotificationService.cs +++ b/src/App/Abstractions/IPushNotificationService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Bit.App.Models; namespace Bit.App.Abstractions { @@ -10,7 +11,7 @@ namespace Bit.App.Abstractions Task GetTokenAsync(); Task RegisterAsync(); Task UnregisterAsync(); - void SendLocalNotification(string title, string message, string notificationId); + void SendLocalNotification(string title, string message, BaseNotificationData data); void DismissLocalNotification(string notificationId); } } diff --git a/src/App/Models/NotificationData.cs b/src/App/Models/NotificationData.cs new file mode 100644 index 000000000..3ddd758d9 --- /dev/null +++ b/src/App/Models/NotificationData.cs @@ -0,0 +1,23 @@ +using System; +namespace Bit.App.Models +{ + public abstract class BaseNotificationData + { + public abstract string Type { get; } + + public string Id { get; set; } + } + + public class PasswordlessNotificationData : BaseNotificationData + { + public const string TYPE = "passwordlessNotificationData"; + + public override string Type => TYPE; + + public int TimeoutInMinutes { get; set; } + + public string UserEmail { get; set; } + } + +} + diff --git a/src/App/Services/NoopPushNotificationListenerService.cs b/src/App/Services/NoopPushNotificationListenerService.cs index f15a258b5..b8b6b2890 100644 --- a/src/App/Services/NoopPushNotificationListenerService.cs +++ b/src/App/Services/NoopPushNotificationListenerService.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Models; using Newtonsoft.Json.Linq; namespace Bit.App.Services @@ -28,5 +29,10 @@ namespace Bit.App.Services { return false; } + + public Task OnNotificationTapped(BaseNotificationData data) + { + return Task.FromResult(0); + } } } diff --git a/src/App/Services/NoopPushNotificationService.cs b/src/App/Services/NoopPushNotificationService.cs index 273473c10..af876618e 100644 --- a/src/App/Services/NoopPushNotificationService.cs +++ b/src/App/Services/NoopPushNotificationService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Models; namespace Bit.App.Services { @@ -30,6 +31,6 @@ namespace Bit.App.Services public void DismissLocalNotification(string notificationId) { } - public void SendLocalNotification(string title, string message, string notificationId) { } + public void SendLocalNotification(string title, string message, BaseNotificationData data) { } } } diff --git a/src/App/Services/PushNotificationListenerService.cs b/src/App/Services/PushNotificationListenerService.cs index 6f489f88c..b2a58f627 100644 --- a/src/App/Services/PushNotificationListenerService.cs +++ b/src/App/Services/PushNotificationListenerService.cs @@ -1,9 +1,11 @@ #if !FDROID using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Models; using Bit.App.Pages; using Bit.App.Resources; using Bit.Core; @@ -11,6 +13,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Response; +using Bit.Core.Services; using Bit.Core.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -30,6 +33,7 @@ namespace Bit.App.Services private IApiService _apiService; private IMessagingService _messagingService; private IPushNotificationService _pushNotificationService; + private ILogger _logger; public async Task OnMessageAsync(JObject value, string deviceType) { @@ -147,7 +151,14 @@ namespace Bit.App.Services await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId); var userEmail = await _stateService.GetEmailAsync(passwordlessLoginMessage?.UserId); - _pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), Constants.PasswordlessNotificationId); + var notificationData = new PasswordlessNotificationData() + { + Id = Constants.PasswordlessNotificationId, + TimeoutInMinutes = Constants.PasswordlessNotificationTimeoutInMinutes, + UserEmail = userEmail, + }; + + _pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData); _messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage); break; default: @@ -213,6 +224,27 @@ namespace Bit.App.Services Debug.WriteLine($"{TAG} error - {message}"); } + public async Task OnNotificationTapped(BaseNotificationData data) + { + Resolve(); + try + { + if (data is PasswordlessNotificationData passwordlessNotificationData) + { + var notificationUserId = await _stateService.GetUserIdAsync(passwordlessNotificationData.UserEmail); + if (notificationUserId != null) + { + await _stateService.SetActiveUserAsync(notificationUserId); + _messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); + } + } + } + catch (Exception ex) + { + _logger.Exception(ex); + } + } + public bool ShouldShowNotification() { return _showNotification; @@ -230,6 +262,7 @@ namespace Bit.App.Services _apiService = ServiceContainer.Resolve("apiService"); _messagingService = ServiceContainer.Resolve("messagingService"); _pushNotificationService = ServiceContainer.Resolve(); + _logger = ServiceContainer.Resolve(); _resolved = true; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index fb5b4b300..7250c02b8 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -32,6 +32,8 @@ public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier"; public const string PasswordlessNotificationId = "26072022"; public const string AndroidNotificationChannelId = "general_notification_channel"; + public const string NotificationData = "notificationData"; + public const string NotificationDataType = "NotificationType"; public const int SelectFileRequestCode = 42; public const int SelectFilePermissionRequestCode = 43; public const int SaveFileRequestCode = 44; diff --git a/src/iOS/Services/iOSPushNotificationHandler.cs b/src/iOS/Services/iOSPushNotificationHandler.cs index a6de3d491..de484d5d2 100644 --- a/src/iOS/Services/iOSPushNotificationHandler.cs +++ b/src/iOS/Services/iOSPushNotificationHandler.cs @@ -1,8 +1,13 @@ using System; using System.Diagnostics; using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.Core; +using Bit.Core.Enums; using Bit.Core.Services; +using CoreData; using Foundation; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UserNotifications; using Xamarin.Forms; @@ -92,9 +97,20 @@ namespace Bit.iOS.Services { Debug.WriteLine($"{TAG} DidReceiveNotificationResponse {response?.Notification?.Request?.Content?.UserInfo}"); - if (response.IsDefaultAction) + if (response.IsDefaultAction && response?.Notification?.Request?.Content?.UserInfo != null) { - OnMessageReceived(response?.Notification?.Request?.Content?.UserInfo); + var userInfo = response?.Notification?.Request?.Content?.UserInfo; + OnMessageReceived(userInfo); + + if (userInfo.TryGetValue(NSString.FromObject(Constants.NotificationData), out NSObject nsObject)) + { + var token = JToken.Parse(NSString.FromObject(nsObject).ToString()); + var typeToken = token.SelectToken(Constants.NotificationDataType); + if (typeToken.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.OnNotificationTapped(token.ToObject()); + } + } } // Inform caller it has been handled diff --git a/src/iOS/Services/iOSPushNotificationService.cs b/src/iOS/Services/iOSPushNotificationService.cs index 1146d7cbd..bcc22b904 100644 --- a/src/iOS/Services/iOSPushNotificationService.cs +++ b/src/iOS/Services/iOSPushNotificationService.cs @@ -4,8 +4,13 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Services; +using Bit.Core; using Bit.Core.Services; using Foundation; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using UIKit; using UserNotifications; @@ -69,9 +74,9 @@ namespace Bit.iOS.Services return Task.FromResult(0); } - public void SendLocalNotification(string title, string message, string notificationId) + public void SendLocalNotification(string title, string message, BaseNotificationData data) { - if (string.IsNullOrEmpty(notificationId)) + if (string.IsNullOrEmpty(data.Id)) { throw new ArgumentNullException("notificationId cannot be null or empty."); } @@ -82,7 +87,12 @@ namespace Bit.iOS.Services Body = message }; - var request = UNNotificationRequest.FromIdentifier(notificationId, content, null); + if (data != null) + { + content.UserInfo = NSDictionary.FromObjectAndKey(NSData.FromString(JsonConvert.SerializeObject(data), NSStringEncoding.UTF8), new NSString(Constants.NotificationData)); + } + + var request = UNNotificationRequest.FromIdentifier(data.Id, content, null); UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) => { if (err != null)