[SG-703] Login request is not removed after dismissing push notification (#2125)

* [SG-703] Handle iOS dismiss notification action. Added core logic to remove passwordless notification from local storage.

* [SG-702] Added broadcast receiver to catch dismiss notfication events on android.

* [SG-703] PR fixes.

* [SG-703] Fix constants namespaces. Lazyloading services on broadcast receiver.

* [SG-703] Change services to use lazy loading

* [SG-703] Change lazy loading to be parameterless.
This commit is contained in:
André Bispo 2022-10-12 15:55:01 +01:00 committed by GitHub
parent 3972e3de8a
commit 569922805f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 67 deletions

View file

@ -152,6 +152,7 @@
<Compile Include="Utilities\IntentExtensions.cs" /> <Compile Include="Utilities\IntentExtensions.cs" />
<Compile Include="Renderers\CustomPageRenderer.cs" /> <Compile Include="Renderers\CustomPageRenderer.cs" />
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" /> <Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
<Compile Include="Receivers\NotificationDismissReceiver.cs" />
<Compile Include="Services\FileService.cs" /> <Compile Include="Services\FileService.cs" />
<Compile Include="Services\AutofillHandler.cs" /> <Compile Include="Services\AutofillHandler.cs" />
<Compile Include="Constants.cs" /> <Compile Include="Constants.cs" />

View file

@ -0,0 +1,41 @@
using Android.Content;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Services;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using CoreConstants = Bit.Core.Constants;
namespace Bit.Droid.Receivers
{
[BroadcastReceiver(Name = Constants.PACKAGE_NAME + "." + nameof(NotificationDismissReceiver), Exported = false)]
public class NotificationDismissReceiver : BroadcastReceiver
{
private readonly LazyResolve<IPushNotificationListenerService> _pushNotificationListenerService = new LazyResolve<IPushNotificationListenerService>();
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public override void OnReceive(Context context, Intent intent)
{
try
{
if (intent?.GetStringExtra(CoreConstants.NotificationData) is string notificationDataJson)
{
var notificationType = JToken.Parse(notificationDataJson).SelectToken(CoreConstants.NotificationDataType);
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
{
_pushNotificationListenerService.Value.OnNotificationDismissed(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
}
catch (System.Exception ex)
{
_logger.Value.Exception(ex);
}
}
}
}

View file

@ -10,9 +10,12 @@ using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities; using Bit.Droid.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xamarin.Forms; using Xamarin.Forms;
using static Xamarin.Essentials.Platform;
using Intent = Android.Content.Intent;
namespace Bit.Droid.Services namespace Bit.Droid.Services
{ {
@ -79,16 +82,21 @@ namespace Bit.Droid.Services
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(Core.Constants.NotificationData, JsonConvert.SerializeObject(data)); intent.PutExtra(Bit.Core.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, Core.Constants.AndroidNotificationChannelId)
var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId)
.SetContentIntent(pendingIntent) .SetContentIntent(pendingIntent)
.SetContentTitle(title) .SetContentTitle(title)
.SetContentText(message) .SetContentText(message)
.SetSmallIcon(Resource.Drawable.ic_notification) .SetSmallIcon(Resource.Drawable.ic_notification)
.SetColor((int)Android.Graphics.Color.White) .SetColor((int)Android.Graphics.Color.White)
.SetDeleteIntent(deletePendingIntent)
.SetAutoCancel(true); .SetAutoCancel(true);
if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0) if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0)

View file

@ -11,6 +11,7 @@ namespace Bit.App.Abstractions
void OnUnregistered(string device); void OnUnregistered(string device);
void OnError(string message, string device); void OnError(string message, string device);
Task OnNotificationTapped(BaseNotificationData data); Task OnNotificationTapped(BaseNotificationData data);
Task OnNotificationDismissed(BaseNotificationData data);
bool ShouldShowNotification(); bool ShouldShowNotification();
} }
} }

View file

@ -34,5 +34,10 @@ namespace Bit.App.Services
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task OnNotificationDismissed(BaseNotificationData data)
{
return Task.FromResult(0);
}
} }
} }

View file

@ -26,20 +26,18 @@ namespace Bit.App.Services
const string TAG = "##PUSH NOTIFICATIONS"; const string TAG = "##PUSH NOTIFICATIONS";
private bool _showNotification; private bool _showNotification;
private bool _resolved; private LazyResolve<ISyncService> _syncService = new LazyResolve<ISyncService>();
private ISyncService _syncService; private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
private IStateService _stateService; private LazyResolve<IAppIdService> _appIdService = new LazyResolve<IAppIdService>();
private IAppIdService _appIdService; private LazyResolve<IApiService> _apiService = new LazyResolve<IApiService>();
private IApiService _apiService; private LazyResolve<IMessagingService> _messagingService = new LazyResolve<IMessagingService>();
private IMessagingService _messagingService; private LazyResolve<IPushNotificationService> _pushNotificationService = new LazyResolve<IPushNotificationService>();
private IPushNotificationService _pushNotificationService; private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
private ILogger _logger;
public async Task OnMessageAsync(JObject value, string deviceType) public async Task OnMessageAsync(JObject value, string deviceType)
{ {
Debug.WriteLine($"{TAG} OnMessageAsync called"); Debug.WriteLine($"{TAG} OnMessageAsync called");
Resolve();
if (value == null) if (value == null)
{ {
return; return;
@ -65,14 +63,14 @@ namespace Bit.App.Services
Debug.WriteLine($"{TAG} - Notification object created: t:{notification?.Type} - p:{notification?.Payload}"); Debug.WriteLine($"{TAG} - Notification object created: t:{notification?.Type} - p:{notification?.Payload}");
var appId = await _appIdService.GetAppIdAsync(); var appId = await _appIdService.Value.GetAppIdAsync();
if (notification?.Payload == null || notification.ContextId == appId) if (notification?.Payload == null || notification.ContextId == appId)
{ {
return; return;
} }
var myUserId = await _stateService.GetActiveUserIdAsync(); var myUserId = await _stateService.Value.GetActiveUserIdAsync();
var isAuthenticated = await _stateService.IsAuthenticatedAsync(); var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync();
switch (notification.Type) switch (notification.Type)
{ {
case NotificationType.SyncCipherUpdate: case NotificationType.SyncCipherUpdate:
@ -81,7 +79,7 @@ namespace Bit.App.Services
notification.Payload); notification.Payload);
if (isAuthenticated && cipherCreateUpdateMessage.UserId == myUserId) if (isAuthenticated && cipherCreateUpdateMessage.UserId == myUserId)
{ {
await _syncService.SyncUpsertCipherAsync(cipherCreateUpdateMessage, await _syncService.Value.SyncUpsertCipherAsync(cipherCreateUpdateMessage,
notification.Type == NotificationType.SyncCipherUpdate); notification.Type == NotificationType.SyncCipherUpdate);
} }
break; break;
@ -91,7 +89,7 @@ namespace Bit.App.Services
notification.Payload); notification.Payload);
if (isAuthenticated && folderCreateUpdateMessage.UserId == myUserId) if (isAuthenticated && folderCreateUpdateMessage.UserId == myUserId)
{ {
await _syncService.SyncUpsertFolderAsync(folderCreateUpdateMessage, await _syncService.Value.SyncUpsertFolderAsync(folderCreateUpdateMessage,
notification.Type == NotificationType.SyncFolderUpdate); notification.Type == NotificationType.SyncFolderUpdate);
} }
break; break;
@ -101,7 +99,7 @@ namespace Bit.App.Services
notification.Payload); notification.Payload);
if (isAuthenticated && loginDeleteMessage.UserId == myUserId) if (isAuthenticated && loginDeleteMessage.UserId == myUserId)
{ {
await _syncService.SyncDeleteCipherAsync(loginDeleteMessage); await _syncService.Value.SyncDeleteCipherAsync(loginDeleteMessage);
} }
break; break;
case NotificationType.SyncFolderDelete: case NotificationType.SyncFolderDelete:
@ -109,7 +107,7 @@ namespace Bit.App.Services
notification.Payload); notification.Payload);
if (isAuthenticated && folderDeleteMessage.UserId == myUserId) if (isAuthenticated && folderDeleteMessage.UserId == myUserId)
{ {
await _syncService.SyncDeleteFolderAsync(folderDeleteMessage); await _syncService.Value.SyncDeleteFolderAsync(folderDeleteMessage);
} }
break; break;
case NotificationType.SyncCiphers: case NotificationType.SyncCiphers:
@ -117,27 +115,27 @@ namespace Bit.App.Services
case NotificationType.SyncSettings: case NotificationType.SyncSettings:
if (isAuthenticated) if (isAuthenticated)
{ {
await _syncService.FullSyncAsync(false); await _syncService.Value.FullSyncAsync(false);
} }
break; break;
case NotificationType.SyncOrgKeys: case NotificationType.SyncOrgKeys:
if (isAuthenticated) if (isAuthenticated)
{ {
await _apiService.RefreshIdentityTokenAsync(); await _apiService.Value.RefreshIdentityTokenAsync();
await _syncService.FullSyncAsync(true); await _syncService.Value.FullSyncAsync(true);
} }
break; break;
case NotificationType.LogOut: case NotificationType.LogOut:
if (isAuthenticated) if (isAuthenticated)
{ {
_messagingService.Send("logout"); _messagingService.Value.Send("logout");
} }
break; break;
case NotificationType.AuthRequest: case NotificationType.AuthRequest:
var passwordlessLoginMessage = JsonConvert.DeserializeObject<PasswordlessRequestNotification>(notification.Payload); var passwordlessLoginMessage = JsonConvert.DeserializeObject<PasswordlessRequestNotification>(notification.Payload);
// if the user has not enabled passwordless logins ignore requests // if the user has not enabled passwordless logins ignore requests
if (!await _stateService.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId)) if (!await _stateService.Value.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId))
{ {
return; return;
} }
@ -148,8 +146,8 @@ namespace Bit.App.Services
return; return;
} }
await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId); await _stateService.Value.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId);
var userEmail = await _stateService.GetEmailAsync(passwordlessLoginMessage?.UserId); var userEmail = await _stateService.Value.GetEmailAsync(passwordlessLoginMessage?.UserId);
var notificationData = new PasswordlessNotificationData() var notificationData = new PasswordlessNotificationData()
{ {
@ -158,8 +156,8 @@ namespace Bit.App.Services
UserEmail = userEmail, UserEmail = userEmail,
}; };
_pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData); _pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData);
_messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage); _messagingService.Value.Send("passwordlessLoginRequest", passwordlessLoginMessage);
break; break;
default: default:
break; break;
@ -168,31 +166,30 @@ namespace Bit.App.Services
public async Task OnRegisteredAsync(string token, string deviceType) public async Task OnRegisteredAsync(string token, string deviceType)
{ {
Resolve();
Debug.WriteLine($"{TAG} - Device Registered - Token : {token}"); Debug.WriteLine($"{TAG} - Device Registered - Token : {token}");
var isAuthenticated = await _stateService.IsAuthenticatedAsync(); var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync();
if (!isAuthenticated) if (!isAuthenticated)
{ {
Debug.WriteLine($"{TAG} - not auth"); Debug.WriteLine($"{TAG} - not auth");
return; return;
} }
var appId = await _appIdService.GetAppIdAsync(); var appId = await _appIdService.Value.GetAppIdAsync();
try try
{ {
#if DEBUG #if DEBUG
await _stateService.SetPushInstallationRegistrationErrorAsync(null); await _stateService.Value.SetPushInstallationRegistrationErrorAsync(null);
#endif #endif
await _apiService.PutDeviceTokenAsync(appId, await _apiService.Value.PutDeviceTokenAsync(appId,
new Core.Models.Request.DeviceTokenRequest { PushToken = token }); new Core.Models.Request.DeviceTokenRequest { PushToken = token });
Debug.WriteLine($"{TAG} Registered device with server."); Debug.WriteLine($"{TAG} Registered device with server.");
await _stateService.SetPushLastRegistrationDateAsync(DateTime.UtcNow); await _stateService.Value.SetPushLastRegistrationDateAsync(DateTime.UtcNow);
if (deviceType == Device.Android) if (deviceType == Device.Android)
{ {
await _stateService.SetPushCurrentTokenAsync(token); await _stateService.Value.SetPushCurrentTokenAsync(token);
} }
} }
#if DEBUG #if DEBUG
@ -200,11 +197,11 @@ namespace Bit.App.Services
{ {
Debug.WriteLine($"{TAG} Failed to register device."); Debug.WriteLine($"{TAG} Failed to register device.");
await _stateService.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message); await _stateService.Value.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message);
} }
catch (Exception e) catch (Exception e)
{ {
await _stateService.SetPushInstallationRegistrationErrorAsync(e.Message); await _stateService.Value.SetPushInstallationRegistrationErrorAsync(e.Message);
throw; throw;
} }
#else #else
@ -226,22 +223,44 @@ namespace Bit.App.Services
public async Task OnNotificationTapped(BaseNotificationData data) public async Task OnNotificationTapped(BaseNotificationData data)
{ {
Resolve();
try try
{ {
if (data is PasswordlessNotificationData passwordlessNotificationData) if (data is PasswordlessNotificationData passwordlessNotificationData)
{ {
var notificationUserId = await _stateService.GetUserIdAsync(passwordlessNotificationData.UserEmail); var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail);
if (notificationUserId != null) if (notificationUserId != null)
{ {
await _stateService.SetActiveUserAsync(notificationUserId); await _stateService.Value.SetActiveUserAsync(notificationUserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT); _messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Exception(ex); _logger.Value.Exception(ex);
}
}
public async Task OnNotificationDismissed(BaseNotificationData data)
{
try
{
if (data is PasswordlessNotificationData passwordlessNotificationData)
{
var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail);
if (notificationUserId != null)
{
var savedNotification = await _stateService.Value.GetPasswordlessLoginNotificationAsync(notificationUserId);
if (savedNotification != null)
{
await _stateService.Value.SetPasswordlessLoginNotificationAsync(null, notificationUserId);
}
}
}
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
} }
} }
@ -249,22 +268,6 @@ namespace Bit.App.Services
{ {
return _showNotification; return _showNotification;
} }
private void Resolve()
{
if (_resolved)
{
return;
}
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_logger = ServiceContainer.Resolve<ILogger>();
_resolved = true;
}
} }
} }
#endif #endif

View file

@ -33,7 +33,7 @@
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 NotificationData = "notificationData";
public const string NotificationDataType = "NotificationType"; public const string NotificationDataType = "Type";
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

@ -96,9 +96,12 @@ namespace Bit.iOS.Services
public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler) public void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
{ {
Debug.WriteLine($"{TAG} DidReceiveNotificationResponse {response?.Notification?.Request?.Content?.UserInfo}"); Debug.WriteLine($"{TAG} DidReceiveNotificationResponse {response?.Notification?.Request?.Content?.UserInfo}");
if ((response?.Notification?.Request?.Content?.UserInfo) == null)
if (response.IsDefaultAction && response?.Notification?.Request?.Content?.UserInfo != null)
{ {
completionHandler();
return;
}
var userInfo = response?.Notification?.Request?.Content?.UserInfo; var userInfo = response?.Notification?.Request?.Content?.UserInfo;
OnMessageReceived(userInfo); OnMessageReceived(userInfo);
@ -106,11 +109,20 @@ namespace Bit.iOS.Services
{ {
var token = JToken.Parse(NSString.FromObject(nsObject).ToString()); var token = JToken.Parse(NSString.FromObject(nsObject).ToString());
var typeToken = token.SelectToken(Constants.NotificationDataType); var typeToken = token.SelectToken(Constants.NotificationDataType);
if (response.IsDefaultAction)
{
if (typeToken.ToString() == PasswordlessNotificationData.TYPE) if (typeToken.ToString() == PasswordlessNotificationData.TYPE)
{ {
_pushNotificationListenerService.OnNotificationTapped(token.ToObject<PasswordlessNotificationData>()); _pushNotificationListenerService.OnNotificationTapped(token.ToObject<PasswordlessNotificationData>());
} }
} }
else if (response.IsDismissAction)
{
if (typeToken.ToString() == PasswordlessNotificationData.TYPE)
{
_pushNotificationListenerService.OnNotificationDismissed(token.ToObject<PasswordlessNotificationData>());
}
}
} }
// Inform caller it has been handled // Inform caller it has been handled