[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.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<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
_logger = ServiceContainer.Resolve<ILogger>("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<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
}
protected override void OnNewIntent(Intent intent)

View File

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

View File

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

View File

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

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

View File

@ -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) { }
}
}

View File

@ -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<IApiService>("apiService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_logger = ServiceContainer.Resolve<ILogger>();
_resolved = true;
}
}

View File

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

View File

@ -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<PasswordlessNotificationData>());
}
}
}
// Inform caller it has been handled

View File

@ -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)