From 10df9e7cd5b99b74541104e06ac2863bc264aef1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Apr 2018 13:37:46 -0400 Subject: [PATCH] multibutton alert, autofill and save new uri --- src/Android/Resources/Resource.Designer.cs | 33 +++++++-- src/Android/Services/DeviceActionService.cs | 73 +++++++++++++++++++ .../Services/IDeviceActionService.cs | 1 + .../Vault/VaultAutofillListCiphersPage.cs | 55 ++++++++++++-- src/App/Pages/Vault/VaultListCiphersPage.cs | 42 ++++++++++- src/App/Resources/AppResources.Designer.cs | 18 +++++ src/App/Resources/AppResources.resx | 6 ++ src/App/Services/CipherService.cs | 71 +++++++++++------- src/App/Utilities/Helpers.cs | 6 ++ src/UWP/Services/DeviceActionService.cs | 17 +++++ .../Services/NoopDeviceActionService.cs | 5 ++ src/iOS/Services/DeviceActionService.cs | 26 +++++++ 12 files changed, 313 insertions(+), 40 deletions(-) diff --git a/src/Android/Resources/Resource.Designer.cs b/src/Android/Resources/Resource.Designer.cs index 0b96097c2..81cb43c92 100644 --- a/src/Android/Resources/Resource.Designer.cs +++ b/src/Android/Resources/Resource.Designer.cs @@ -6506,17 +6506,17 @@ namespace Bit.Android // aapt resource value: 0x7f0a0051 public const int ApplicationName = 2131361873; - // aapt resource value: 0x7f0a00ab - public const int AutoFillServiceDescription = 2131361963; + // aapt resource value: 0x7f0a00b2 + public const int AutoFillServiceDescription = 2131361970; - // aapt resource value: 0x7f0a00aa - public const int AutoFillServiceSummary = 2131361962; + // aapt resource value: 0x7f0a00b1 + public const int AutoFillServiceSummary = 2131361969; // aapt resource value: 0x7f0a0050 public const int Hello = 2131361872; - // aapt resource value: 0x7f0a00ac - public const int MyVault = 2131361964; + // aapt resource value: 0x7f0a00b3 + public const int MyVault = 2131361971; // aapt resource value: 0x7f0a0027 public const int abc_action_bar_home_description = 2131361831; @@ -6671,6 +6671,27 @@ namespace Bit.Android // aapt resource value: 0x7f0a000f public const int common_signin_button_text_long = 2131361807; + // aapt resource value: 0x7f0a00ac + public const int default_web_client_id = 2131361964; + + // aapt resource value: 0x7f0a00ad + public const int firebase_database_url = 2131361965; + + // aapt resource value: 0x7f0a00aa + public const int gcm_defaultSenderId = 2131361962; + + // aapt resource value: 0x7f0a00ae + public const int google_api_key = 2131361966; + + // aapt resource value: 0x7f0a00ab + public const int google_app_id = 2131361963; + + // aapt resource value: 0x7f0a00af + public const int google_crash_reporting_api_key = 2131361967; + + // aapt resource value: 0x7f0a00b0 + public const int google_storage_bucket = 2131361968; + // aapt resource value: 0x7f0a0052 public const int hockeyapp_crash_dialog_app_name_fallback = 2131361874; diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 4918166a6..93527ed14 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -514,5 +514,78 @@ namespace Bit.Android.Services alert.Show(); return result.Task; } + + public Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) + { + var activity = (MainActivity)CurrentContext; + if(activity == null) + { + return Task.FromResult(null); + } + + var result = new TaskCompletionSource(); + var alertBuilder = new AlertDialog.Builder(activity); + alertBuilder.SetTitle(title); + + if(!string.IsNullOrWhiteSpace(message)) + { + if(buttons != null && buttons.Length > 2) + { + if(!string.IsNullOrWhiteSpace(title)) + { + alertBuilder.SetTitle($"{title}: {message}"); + } + else + { + alertBuilder.SetTitle(message); + } + } + else + { + alertBuilder.SetMessage(message); + } + } + + if(buttons != null) + { + if(buttons.Length > 2) + { + alertBuilder.SetItems(buttons, (sender, args) => + { + result.TrySetResult(buttons[args.Which]); + }); + } + else + { + if(buttons.Length > 0) + { + alertBuilder.SetPositiveButton(buttons[0], (sender, args) => + { + result.TrySetResult(buttons[0]); + }); + } + if(buttons.Length > 1) + { + alertBuilder.SetNeutralButton(buttons[1], (sender, args) => + { + result.TrySetResult(buttons[1]); + }); + } + } + } + + if(!string.IsNullOrWhiteSpace(cancel)) + { + alertBuilder.SetNegativeButton(cancel, (sender, args) => + { + result.TrySetResult(cancel); + }); + } + + var alert = alertBuilder.Create(); + alert.CancelEvent += (o, args) => { result.TrySetResult(null); }; + alert.Show(); + return result.Task; + } } } diff --git a/src/App/Abstractions/Services/IDeviceActionService.cs b/src/App/Abstractions/Services/IDeviceActionService.cs index 074fd86d9..bb3b874d5 100644 --- a/src/App/Abstractions/Services/IDeviceActionService.cs +++ b/src/App/Abstractions/Services/IDeviceActionService.cs @@ -23,5 +23,6 @@ namespace Bit.App.Abstractions void OpenAutofillSettings(); Task LaunchAppAsync(string appName, Page page); Task DisplayPromptAync(string title = null, string description = null, string text = null); + Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons); } } diff --git a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs b/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs index 2f35883d7..4b1d8edfa 100644 --- a/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs +++ b/src/App/Pages/Vault/VaultAutofillListCiphersPage.cs @@ -13,6 +13,7 @@ using Bit.App.Models; using System.Collections.Generic; using Bit.App.Enums; using static Bit.App.Models.Page.VaultListPageModel; +using Plugin.Connectivity.Abstractions; namespace Bit.App.Pages { @@ -22,6 +23,7 @@ namespace Bit.App.Pages private readonly IDeviceInfoService _deviceInfoService; private readonly ISettingsService _settingsService; private readonly IAppSettingsService _appSettingsService; + public readonly IConnectivity _connectivity; private CancellationTokenSource _filterResultsCancellationTokenSource; private readonly string _name; private readonly AppOptions _appOptions; @@ -47,6 +49,7 @@ namespace Bit.App.Pages _settingsService = Resolver.Resolve(); _appSettingsService = Resolver.Resolve(); GoogleAnalyticsService = Resolver.Resolve(); + _connectivity = Resolver.Resolve(); Init(); } @@ -237,17 +240,57 @@ namespace Bit.App.Pages } else { - bool doAutofill = true; + var autofillResponse = AppResources.Yes; if(cipher.Fuzzy) { - doAutofill = await DisplayAlert(null, - string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, _name), - AppResources.Yes, AppResources.No); + var options = new List { AppResources.Yes }; + if(cipher.Type == CipherType.Login && _connectivity.IsConnected) + { + options.Add(AppResources.YesAndSave); + } + + autofillResponse = await DeviceActionService.DisplayAlertAsync(null, + string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, _name), AppResources.No, + options.ToArray()); } - if(doAutofill) + if(autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login) { - GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App"); + if(!_connectivity.IsConnected) + { + Helpers.AlertNoConnection(this); + } + else + { + var uris = cipher.CipherModel.Login?.Uris?.ToList(); + if(uris == null) + { + uris = new List(); + } + + uris.Add(new LoginUri + { + Uri = Uri.Encrypt(cipher.CipherModel.OrganizationId), + Match = null + }); + + cipher.CipherModel.Login.Uris = uris; + + await DeviceActionService.ShowLoadingAsync(AppResources.Saving); + var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); + await DeviceActionService.HideLoadingAsync(); + + if(saveTask.Succeeded) + { + GoogleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); + } + } + } + + if(autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave) + { + GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", + Uri.StartsWith("http") ? "Website" : "App"); DeviceActionService.Autofill(cipher); } } diff --git a/src/App/Pages/Vault/VaultListCiphersPage.cs b/src/App/Pages/Vault/VaultListCiphersPage.cs index 8346e5ac4..7be3f2087 100644 --- a/src/App/Pages/Vault/VaultListCiphersPage.cs +++ b/src/App/Pages/Vault/VaultListCiphersPage.cs @@ -403,8 +403,14 @@ namespace Bit.App.Pages string selection = null; if(!string.IsNullOrWhiteSpace(_uri)) { + var options = new List { AppResources.Autofill }; + if(cipher.Type == Enums.CipherType.Login && _connectivity.IsConnected) + { + options.Add(AppResources.AutofillAndSave); + } + options.Add(AppResources.View); selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null, - AppResources.Autofill, AppResources.View); + options.ToArray()); } if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri)) @@ -412,8 +418,40 @@ namespace Bit.App.Pages var page = new VaultViewCipherPage(cipher.Type, cipher.Id); await Navigation.PushForDeviceAsync(page); } - else if(selection == AppResources.Autofill) + else if(selection == AppResources.Autofill || selection == AppResources.AutofillAndSave) { + if(selection == AppResources.AutofillAndSave) + { + if(!_connectivity.IsConnected) + { + Helpers.AlertNoConnection(this); + } + else + { + var uris = cipher.CipherModel.Login?.Uris?.ToList(); + if(uris == null) + { + uris = new List(); + } + + uris.Add(new Models.LoginUri + { + Uri = _uri.Encrypt(cipher.CipherModel.OrganizationId), + Match = null + }); + + cipher.CipherModel.Login.Uris = uris; + + await _deviceActionService.ShowLoadingAsync(AppResources.Saving); + var saveTask = await _cipherService.SaveAsync(cipher.CipherModel); + await _deviceActionService.HideLoadingAsync(); + if(saveTask.Succeeded) + { + _googleAnalyticsService.TrackAppEvent("AddedLoginUriDuringAutofill"); + } + } + } + if(_deviceInfoService.Version < 21) { Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri)); diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index dcca7fd15..2f419a5ad 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -330,6 +330,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Auto-fill and save. + /// + public static string AutofillAndSave { + get { + return ResourceManager.GetString("AutofillAndSave", resourceCulture); + } + } + /// /// Looks up a localized string similar to Do you want to auto-fill or view this item?. /// @@ -3291,6 +3300,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Yes, and Save. + /// + public static string YesAndSave { + get { + return ResourceManager.GetString("YesAndSave", resourceCulture); + } + } + /// /// Looks up a localized string similar to To continue, hold your YubiKey NEO against the back of the device or insert your YubiKey into your device's USB port, then touch its button.. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 654da4e7d..514690cc1 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1283,4 +1283,10 @@ Match Detection URI match detection for auto-fill. + + Yes, and Save + + + Auto-fill and save + \ No newline at end of file diff --git a/src/App/Services/CipherService.cs b/src/App/Services/CipherService.cs index ab3af6dda..f31a85905 100644 --- a/src/App/Services/CipherService.cs +++ b/src/App/Services/CipherService.cs @@ -93,7 +93,8 @@ namespace Bit.App.Services return ciphers.Where(c => cipherIds.Contains(c.Id)); } - public async Task, IEnumerable, IEnumerable>> GetAllAsync(string uriString) + public async Task, IEnumerable, IEnumerable>> GetAllAsync( + string uriString) { if(string.IsNullOrWhiteSpace(uriString)) { @@ -173,42 +174,42 @@ namespace Bit.App.Services break; } - var added = false; + var match = false; switch(u.Match) { case null: case Enums.UriMatchType.Domain: - added = CheckDefaultUriMatch(cipher, loginUriString, matchingLogins, matchingFuzzyLogins, + match = CheckDefaultUriMatch(cipher, loginUriString, matchingLogins, matchingFuzzyLogins, matchingDomainsArray, matchingFuzzyDomainsArray, mobileApp, mobileAppSearchTerms); break; case Enums.UriMatchType.Host: var urlHost = Helpers.GetUrlHost(uriString); - added = urlHost != null && urlHost == Helpers.GetUrlHost(loginUriString); - if(added) + match = urlHost != null && urlHost == Helpers.GetUrlHost(loginUriString); + if(match) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case Enums.UriMatchType.Exact: - added = uriString == loginUriString; - if(added) + match = uriString == loginUriString; + if(match) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case Enums.UriMatchType.StartsWith: - added = uriString.StartsWith(loginUriString); - if(added) + match = uriString.StartsWith(loginUriString); + if(match) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case Enums.UriMatchType.RegularExpression: var regex = new Regex(loginUriString, RegexOptions.IgnoreCase, TimeSpan.FromSeconds(1)); - added = regex.IsMatch(uriString); - if(added) + match = regex.IsMatch(uriString); + if(match) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); } break; case Enums.UriMatchType.Never: @@ -216,7 +217,7 @@ namespace Bit.App.Services break; } - if(added) + if(match) { break; } @@ -436,21 +437,21 @@ namespace Bit.App.Services { if(Array.IndexOf(matchingDomainsArray, loginUriString) >= 0) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); return true; } else if(mobileApp && Array.IndexOf(matchingFuzzyDomainsArray, loginUriString) >= 0) { - matchingFuzzyLogins.Add(cipher); - return true; + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; } else if(!mobileApp) { var info = InfoFromMobileAppUri(loginUriString); if(info?.Item1 != null && Array.IndexOf(matchingDomainsArray, info.Item1) >= 0) { - matchingFuzzyLogins.Add(cipher); - return true; + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; } } @@ -462,13 +463,13 @@ namespace Bit.App.Services if(Array.IndexOf(matchingDomainsArray, loginDomainName) >= 0) { - matchingLogins.Add(cipher); + AddMatchingLogin(cipher, matchingLogins, matchingFuzzyLogins); return true; } else if(mobileApp && Array.IndexOf(matchingFuzzyDomainsArray, loginDomainName) >= 0) { - matchingFuzzyLogins.Add(cipher); - return true; + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; } } @@ -491,13 +492,31 @@ namespace Bit.App.Services if(addedFromSearchTerm) { - matchingFuzzyLogins.Add(cipher); - return true; + AddMatchingFuzzyLogin(cipher, matchingLogins, matchingFuzzyLogins); + return false; } } } return false; } + + private void AddMatchingLogin(Cipher cipher, List matchingLogins, List matchingFuzzyLogins) + { + if(matchingFuzzyLogins.Contains(cipher)) + { + matchingFuzzyLogins.Remove(cipher); + } + + matchingLogins.Add(cipher); + } + + private void AddMatchingFuzzyLogin(Cipher cipher, List matchingLogins, List matchingFuzzyLogins) + { + if(!matchingFuzzyLogins.Contains(cipher) && !matchingLogins.Contains(cipher)) + { + matchingFuzzyLogins.Add(cipher); + } + } } } diff --git a/src/App/Utilities/Helpers.cs b/src/App/Utilities/Helpers.cs index 51a23f237..d310a40a8 100644 --- a/src/App/Utilities/Helpers.cs +++ b/src/App/Utilities/Helpers.cs @@ -521,5 +521,11 @@ namespace Bit.App.Utilities return host; } + + public static void AlertNoConnection(Page page) + { + page.DisplayAlert(AppResources.InternetConnectionRequiredTitle, + AppResources.InternetConnectionRequiredMessage, AppResources.Ok); + } } } diff --git a/src/UWP/Services/DeviceActionService.cs b/src/UWP/Services/DeviceActionService.cs index 8f1a57c19..3fdb70374 100644 --- a/src/UWP/Services/DeviceActionService.cs +++ b/src/UWP/Services/DeviceActionService.cs @@ -182,5 +182,22 @@ namespace Bit.UWP.Services return result.Ok ? result.Value ?? string.Empty : null; } + + public async Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) + { + if(!string.IsNullOrWhiteSpace(message)) + { + if(string.IsNullOrWhiteSpace(title)) + { + title = message; + } + else + { + title = $"{title}: {message}"; + } + } + + return await _userDialogs.ActionSheetAsync(title, cancel, null, null, buttons.ToArray()); + } } } diff --git a/src/iOS.Core/Services/NoopDeviceActionService.cs b/src/iOS.Core/Services/NoopDeviceActionService.cs index 8236c7003..36f35b20c 100644 --- a/src/iOS.Core/Services/NoopDeviceActionService.cs +++ b/src/iOS.Core/Services/NoopDeviceActionService.cs @@ -43,6 +43,11 @@ namespace Bit.iOS.Core.Services // do nothing } + public Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) + { + return Task.FromResult(null); + } + public Task DisplayPromptAync(string title = null, string description = null, string text = null) { return Task.FromResult(null); diff --git a/src/iOS/Services/DeviceActionService.cs b/src/iOS/Services/DeviceActionService.cs index 1967ec81b..720b1218e 100644 --- a/src/iOS/Services/DeviceActionService.cs +++ b/src/iOS/Services/DeviceActionService.cs @@ -351,6 +351,32 @@ namespace Bit.iOS.Services return result.Task; } + public Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) + { + var result = new TaskCompletionSource(); + var alert = UIAlertController.Create(title ?? string.Empty, message, UIAlertControllerStyle.Alert); + + if(!string.IsNullOrWhiteSpace(cancel)) + { + alert.AddAction(UIAlertAction.Create(cancel, UIAlertActionStyle.Cancel, x => + { + result.TrySetResult(cancel); + })); + } + + foreach(var button in buttons) + { + alert.AddAction(UIAlertAction.Create(button, UIAlertActionStyle.Default, x => + { + result.TrySetResult(button); + })); + } + + var vc = GetPresentedViewController(); + vc?.PresentViewController(alert, true, null); + return result.Task; + } + private UIViewController GetPresentedViewController() { var window = UIApplication.SharedApplication.KeyWindow;