[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.
This commit is contained in:
André Bispo 2022-10-07 12:06:57 +01:00 committed by GitHub
parent 1e5eab0574
commit abada481b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 128 additions and 12 deletions

View file

@ -20,6 +20,8 @@ using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Receivers; using Bit.Droid.Receivers;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xamarin.Essentials; using Xamarin.Essentials;
using ZXing.Net.Mobile.Android; using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider; using FileProvider = AndroidX.Core.Content.FileProvider;
@ -39,6 +41,7 @@ namespace Bit.Droid
private IStateService _stateService; private IStateService _stateService;
private IAppIdService _appIdService; private IAppIdService _appIdService;
private IEventService _eventService; private IEventService _eventService;
private IPushNotificationListenerService _pushNotificationListenerService;
private ILogger _logger; private ILogger _logger;
private PendingIntent _eventUploadPendingIntent; private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions; private AppOptions _appOptions;
@ -61,6 +64,7 @@ namespace Bit.Droid
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService"); _appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService"); _eventService = ServiceContainer.Resolve<IEventService>("eventService");
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
_logger = ServiceContainer.Resolve<ILogger>("logger"); _logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar; TabLayoutResource = Resource.Layout.Tabbar;
@ -145,6 +149,15 @@ namespace Bit.Droid
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this) AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
.GetAwaiter() .GetAwaiter()
.GetResult(); .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<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
} }
protected override void OnNewIntent(Intent intent) protected override void OnNewIntent(Intent intent)

View file

@ -1,14 +1,17 @@
#if !FDROID #if !FDROID
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using AndroidX.Core.App; using AndroidX.Core.App;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.Droid.Services 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."); throw new ArgumentNullException("notificationId cannot be null or empty.");
} }
var context = Android.App.Application.Context; var context = Android.App.Application.Context;
var intent = new Intent(context, typeof(MainActivity)); var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra(Constants.NotificationData, JsonConvert.SerializeObject(data));
var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true); var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags); var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId) var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
.SetContentIntent(pendingIntent) .SetContentIntent(pendingIntent)
.SetContentTitle(title) .SetContentTitle(title)
.SetContentText(message) .SetContentText(message)
.SetTimeoutAfter(Constants.PasswordlessNotificationTimeoutInMinutes * 60000)
.SetSmallIcon(Resource.Drawable.ic_notification) .SetSmallIcon(Resource.Drawable.ic_notification)
.SetColor((int)Android.Graphics.Color.White) .SetColor((int)Android.Graphics.Color.White)
.SetAutoCancel(true); .SetAutoCancel(true);
if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0)
{
builder.SetTimeoutAfter(passwordlessNotificationData.TimeoutInMinutes * 60000);
}
var notificationManager = NotificationManagerCompat.From(context); var notificationManager = NotificationManagerCompat.From(context);
notificationManager.Notify(int.Parse(notificationId), builder.Build()); notificationManager.Notify(int.Parse(data.Id), builder.Build());
} }
} }
} }

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Bit.App.Abstractions namespace Bit.App.Abstractions
@ -9,6 +10,7 @@ namespace Bit.App.Abstractions
Task OnRegisteredAsync(string token, string device); Task OnRegisteredAsync(string token, string device);
void OnUnregistered(string device); void OnUnregistered(string device);
void OnError(string message, string device); void OnError(string message, string device);
Task OnNotificationTapped(BaseNotificationData data);
bool ShouldShowNotification(); bool ShouldShowNotification();
} }
} }

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions namespace Bit.App.Abstractions
{ {
@ -10,7 +11,7 @@ namespace Bit.App.Abstractions
Task<string> GetTokenAsync(); Task<string> GetTokenAsync();
Task RegisterAsync(); Task RegisterAsync();
Task UnregisterAsync(); Task UnregisterAsync();
void SendLocalNotification(string title, string message, string notificationId); void SendLocalNotification(string title, string message, BaseNotificationData data);
void DismissLocalNotification(string notificationId); void DismissLocalNotification(string notificationId);
} }
} }

View file

@ -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; }
}
}

View file

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Bit.App.Services namespace Bit.App.Services
@ -28,5 +29,10 @@ namespace Bit.App.Services
{ {
return false; return false;
} }
public Task OnNotificationTapped(BaseNotificationData data)
{
return Task.FromResult(0);
}
} }
} }

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@ -30,6 +31,6 @@ namespace Bit.App.Services
public void DismissLocalNotification(string notificationId) { } public void DismissLocalNotification(string notificationId) { }
public void SendLocalNotification(string title, string message, string notificationId) { } public void SendLocalNotification(string title, string message, BaseNotificationData data) { }
} }
} }

View file

@ -1,9 +1,11 @@
#if !FDROID #if !FDROID
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core; using Bit.Core;
@ -11,6 +13,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -30,6 +33,7 @@ namespace Bit.App.Services
private IApiService _apiService; private IApiService _apiService;
private IMessagingService _messagingService; private IMessagingService _messagingService;
private IPushNotificationService _pushNotificationService; private IPushNotificationService _pushNotificationService;
private ILogger _logger;
public async Task OnMessageAsync(JObject value, string deviceType) public async Task OnMessageAsync(JObject value, string deviceType)
{ {
@ -147,7 +151,14 @@ namespace Bit.App.Services
await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId); await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId);
var userEmail = await _stateService.GetEmailAsync(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); _messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage);
break; break;
default: default:
@ -213,6 +224,27 @@ namespace Bit.App.Services
Debug.WriteLine($"{TAG} error - {message}"); 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() public bool ShouldShowNotification()
{ {
return _showNotification; return _showNotification;
@ -230,6 +262,7 @@ namespace Bit.App.Services
_apiService = ServiceContainer.Resolve<IApiService>("apiService"); _apiService = ServiceContainer.Resolve<IApiService>("apiService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService"); _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>(); _pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_logger = ServiceContainer.Resolve<ILogger>();
_resolved = true; _resolved = true;
} }
} }

View file

@ -32,6 +32,8 @@
public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier"; public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier";
public const string PasswordlessNotificationId = "26072022"; public const string PasswordlessNotificationId = "26072022";
public const string AndroidNotificationChannelId = "general_notification_channel"; 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 SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43; public const int SelectFilePermissionRequestCode = 43;
public const int SaveFileRequestCode = 44; public const int SaveFileRequestCode = 44;

View file

@ -1,8 +1,13 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using CoreData;
using Foundation; using Foundation;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UserNotifications; using UserNotifications;
using Xamarin.Forms; using Xamarin.Forms;
@ -92,9 +97,20 @@ namespace Bit.iOS.Services
{ {
Debug.WriteLine($"{TAG} DidReceiveNotificationResponse {response?.Notification?.Request?.Content?.UserInfo}"); 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<PasswordlessNotificationData>());
}
}
} }
// Inform caller it has been handled // Inform caller it has been handled

View file

@ -4,8 +4,13 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Services;
using Bit.Core;
using Bit.Core.Services; using Bit.Core.Services;
using Foundation; using Foundation;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UIKit; using UIKit;
using UserNotifications; using UserNotifications;
@ -69,9 +74,9 @@ namespace Bit.iOS.Services
return Task.FromResult(0); 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."); throw new ArgumentNullException("notificationId cannot be null or empty.");
} }
@ -82,7 +87,12 @@ namespace Bit.iOS.Services
Body = message 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) => UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) =>
{ {
if (err != null) if (err != null)