1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-09-29 04:07:37 +02:00

add other cipher type support to vault listings

This commit is contained in:
Kyle Spearrin 2017-10-18 21:35:33 -04:00
parent 0020bd0fb7
commit 74ac9cbbbe
10 changed files with 267 additions and 138 deletions

View File

@ -104,7 +104,7 @@ namespace Bit.Android
OpenAccessibilitySettings(); OpenAccessibilitySettings();
}); });
MessagingCenter.Subscribe<Xamarin.Forms.Application, VaultListPageModel.Login>( MessagingCenter.Subscribe<Xamarin.Forms.Application, VaultListPageModel.Cipher>(
Xamarin.Forms.Application.Current, "Autofill", (sender, args) => Xamarin.Forms.Application.Current, "Autofill", (sender, args) =>
{ {
ReturnCredentials(args); ReturnCredentials(args);
@ -128,10 +128,10 @@ namespace Bit.Android
}); });
} }
private void ReturnCredentials(VaultListPageModel.Login login) private void ReturnCredentials(VaultListPageModel.Cipher cipher)
{ {
Intent data = new Intent(); Intent data = new Intent();
if(login == null) if(cipher == null)
{ {
data.PutExtra("canceled", "true"); data.PutExtra("canceled", "true");
} }
@ -139,14 +139,14 @@ namespace Bit.Android
{ {
var isPremium = Resolver.Resolve<ITokenService>()?.TokenPremium ?? false; var isPremium = Resolver.Resolve<ITokenService>()?.TokenPremium ?? false;
var autoCopyEnabled = !_settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false); var autoCopyEnabled = !_settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false);
if(isPremium && autoCopyEnabled && _deviceActionService != null && login.Totp.Value != null) if(isPremium && autoCopyEnabled && _deviceActionService != null && cipher.Totp.Value != null)
{ {
_deviceActionService.CopyToClipboard(App.Utilities.Crypto.Totp(login.Totp.Value)); _deviceActionService.CopyToClipboard(App.Utilities.Crypto.Totp(cipher.Totp.Value));
} }
data.PutExtra("uri", login.Uri.Value); data.PutExtra("uri", cipher.Uri.Value);
data.PutExtra("username", login.Username); data.PutExtra("username", cipher.Username);
data.PutExtra("password", login.Password.Value); data.PutExtra("password", cipher.Password.Value);
} }
if(Parent == null) if(Parent == null)

View File

@ -68,7 +68,7 @@ namespace Bit.App
if(authService.IsAuthenticated && _uri != null) if(authService.IsAuthenticated && _uri != null)
{ {
MainPage = new ExtendedNavigationPage(new VaultAutofillListLoginsPage(_uri)); MainPage = new ExtendedNavigationPage(new VaultAutofillListCiphersPage(_uri));
} }
else if(authService.IsAuthenticated) else if(authService.IsAuthenticated)
{ {

View File

@ -180,7 +180,7 @@
<Compile Include="Pages\Settings\SettingsPage.cs" /> <Compile Include="Pages\Settings\SettingsPage.cs" />
<Compile Include="Pages\Settings\SettingsListFoldersPage.cs" /> <Compile Include="Pages\Settings\SettingsListFoldersPage.cs" />
<Compile Include="Pages\Vault\VaultCustomFieldsPage.cs" /> <Compile Include="Pages\Vault\VaultCustomFieldsPage.cs" />
<Compile Include="Pages\Vault\VaultAutofillListLoginsPage.cs" /> <Compile Include="Pages\Vault\VaultAutofillListCiphersPage.cs" />
<Compile Include="Pages\Vault\VaultAttachmentsPage.cs" /> <Compile Include="Pages\Vault\VaultAttachmentsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ICipherRepository.cs" /> <Compile Include="Abstractions\Repositories\ICipherRepository.cs" />
@ -357,7 +357,7 @@
<Compile Include="Pages\Vault\VaultAddLoginPage.cs" /> <Compile Include="Pages\Vault\VaultAddLoginPage.cs" />
<Compile Include="Pages\Vault\VaultViewLoginPage.cs" /> <Compile Include="Pages\Vault\VaultViewLoginPage.cs" />
<Compile Include="Pages\Vault\VaultEditLoginPage.cs" /> <Compile Include="Pages\Vault\VaultEditLoginPage.cs" />
<Compile Include="Pages\Vault\VaultListLoginsPage.cs" /> <Compile Include="Pages\Vault\VaultListCiphersPage.cs" />
<Compile Include="Services\PasswordGenerationService.cs" /> <Compile Include="Services\PasswordGenerationService.cs" />
<Compile Include="Utilities\Base32.cs" /> <Compile Include="Utilities\Base32.cs" />
<Compile Include="Utilities\Crypto.cs" /> <Compile Include="Utilities\Crypto.cs" />

View File

@ -7,15 +7,15 @@ namespace Bit.App.Controls
public class VaultListViewCell : LabeledDetailCell public class VaultListViewCell : LabeledDetailCell
{ {
public static readonly BindableProperty LoginParameterProperty = BindableProperty.Create(nameof(LoginParameter), public static readonly BindableProperty LoginParameterProperty = BindableProperty.Create(nameof(LoginParameter),
typeof(VaultListPageModel.Login), typeof(VaultListViewCell), null); typeof(VaultListPageModel.Cipher), typeof(VaultListViewCell), null);
public VaultListViewCell(Action<VaultListPageModel.Login> moreClickedAction) public VaultListViewCell(Action<VaultListPageModel.Cipher> moreClickedAction)
{ {
SetBinding(LoginParameterProperty, new Binding(".")); SetBinding(LoginParameterProperty, new Binding("."));
Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Login.Name)); Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Cipher.Name));
Detail.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Login.Username)); Detail.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Cipher.Subtitle));
LabelIcon.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Login.Shared)); LabelIcon.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Cipher.Shared));
LabelIcon2.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Login.HasAttachments)); LabelIcon2.SetBinding(VisualElement.IsVisibleProperty, nameof(VaultListPageModel.Cipher.HasAttachments));
Button.Image = "more"; Button.Image = "more";
Button.Command = new Command(() => moreClickedAction?.Invoke(LoginParameter)); Button.Command = new Command(() => moreClickedAction?.Invoke(LoginParameter));
@ -27,9 +27,9 @@ namespace Bit.App.Controls
BackgroundColor = Color.White; BackgroundColor = Color.White;
} }
public VaultListPageModel.Login LoginParameter public VaultListPageModel.Cipher LoginParameter
{ {
get { return GetValue(LoginParameterProperty) as VaultListPageModel.Login; } get { return GetValue(LoginParameterProperty) as VaultListPageModel.Cipher; }
set { SetValue(LoginParameterProperty, value); } set { SetValue(LoginParameterProperty, value); }
} }
} }

View File

@ -2,24 +2,51 @@
using System.Collections.Generic; using System.Collections.Generic;
using Bit.App.Resources; using Bit.App.Resources;
using System.Linq; using System.Linq;
using Bit.App.Enums;
namespace Bit.App.Models.Page namespace Bit.App.Models.Page
{ {
public class VaultListPageModel public class VaultListPageModel
{ {
public class Login public class Cipher
{ {
public Login(Models.Cipher cipher) public Cipher(Models.Cipher cipher)
{ {
Id = cipher.Id; Id = cipher.Id;
Shared = !string.IsNullOrWhiteSpace(cipher.OrganizationId); Shared = !string.IsNullOrWhiteSpace(cipher.OrganizationId);
HasAttachments = cipher.Attachments?.Any() ?? false; HasAttachments = cipher.Attachments?.Any() ?? false;
FolderId = cipher.FolderId; FolderId = cipher.FolderId;
Name = cipher.Name?.Decrypt(cipher.OrganizationId); Name = cipher.Name?.Decrypt(cipher.OrganizationId);
Username = cipher.Login?.Username?.Decrypt(cipher.OrganizationId) ?? " "; Type = cipher.Type;
Password = new Lazy<string>(() => cipher.Login?.Password?.Decrypt(cipher.OrganizationId));
Uri = new Lazy<string>(() => cipher.Login?.Uri?.Decrypt(cipher.OrganizationId)); switch(cipher.Type)
Totp = new Lazy<string>(() => cipher.Login?.Totp?.Decrypt(cipher.OrganizationId)); {
case CipherType.Login:
Username = cipher.Login?.Username?.Decrypt(cipher.OrganizationId) ?? " ";
Password = new Lazy<string>(() => cipher.Login?.Password?.Decrypt(cipher.OrganizationId));
Uri = new Lazy<string>(() => cipher.Login?.Uri?.Decrypt(cipher.OrganizationId));
Totp = new Lazy<string>(() => cipher.Login?.Totp?.Decrypt(cipher.OrganizationId));
Subtitle = Username;
break;
case CipherType.SecureNote:
Subtitle = " ";
break;
case CipherType.Card:
var cardNumber = cipher.Card?.Number?.Decrypt(cipher.OrganizationId) ?? " ";
var cardBrand = cipher.Card?.Brand?.Decrypt(cipher.OrganizationId) ?? " ";
CardCode = new Lazy<string>(() => cipher.Card?.Code?.Decrypt(cipher.OrganizationId));
Subtitle = $"{cardBrand}, *{cardNumber}";
break;
case CipherType.Identity:
var firstName = cipher.Identity?.FirstName?.Decrypt(cipher.OrganizationId) ?? " ";
var lastName = cipher.Identity?.LastName?.Decrypt(cipher.OrganizationId) ?? " ";
Subtitle = $"{firstName} {lastName}";
break;
default:
break;
}
} }
public string Id { get; set; } public string Id { get; set; }
@ -27,16 +54,24 @@ namespace Bit.App.Models.Page
public bool HasAttachments { get; set; } public bool HasAttachments { get; set; }
public string FolderId { get; set; } public string FolderId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Subtitle { get; set; }
public CipherType Type { get; set; }
// Login metadata
public string Username { get; set; } public string Username { get; set; }
public Lazy<string> Password { get; set; } public Lazy<string> Password { get; set; }
public Lazy<string> Uri { get; set; } public Lazy<string> Uri { get; set; }
public Lazy<string> Totp { get; set; } public Lazy<string> Totp { get; set; }
// Login metadata
public string CardNumber { get; set; }
public Lazy<string> CardCode { get; set; }
} }
public class AutofillLogin : Login public class AutofillCipher : Cipher
{ {
public AutofillLogin(Models.Cipher login, bool fuzzy = false) public AutofillCipher(Models.Cipher cipher, bool fuzzy = false)
: base(login) : base(cipher)
{ {
Fuzzy = fuzzy; Fuzzy = fuzzy;
} }
@ -44,7 +79,7 @@ namespace Bit.App.Models.Page
public bool Fuzzy { get; set; } public bool Fuzzy { get; set; }
} }
public class Folder : List<Login> public class Folder : List<Cipher>
{ {
public Folder(Models.Folder folder) public Folder(Models.Folder folder)
{ {
@ -52,18 +87,18 @@ namespace Bit.App.Models.Page
Name = folder.Name?.Decrypt(); Name = folder.Name?.Decrypt();
} }
public Folder(List<Login> logins) public Folder(List<Cipher> ciphers)
{ {
AddRange(logins); AddRange(ciphers);
} }
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } = AppResources.FolderNone; public string Name { get; set; } = AppResources.FolderNone;
} }
public class AutofillGrouping : List<AutofillLogin> public class AutofillGrouping : List<AutofillCipher>
{ {
public AutofillGrouping(List<AutofillLogin> logins, string name) public AutofillGrouping(List<AutofillCipher> logins, string name)
{ {
AddRange(logins); AddRange(logins);
Name = name; Name = name;

View File

@ -12,8 +12,8 @@ namespace Bit.App.Pages
TintColor = Color.FromHex("3c8dbc"); TintColor = Color.FromHex("3c8dbc");
var settingsNavigation = new ExtendedNavigationPage(new SettingsPage()); var settingsNavigation = new ExtendedNavigationPage(new SettingsPage());
var favoritesNavigation = new ExtendedNavigationPage(new VaultListLoginsPage(true, uri)); var favoritesNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(true, uri));
var vaultNavigation = new ExtendedNavigationPage(new VaultListLoginsPage(false, uri)); var vaultNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(false, uri));
var toolsNavigation = new ExtendedNavigationPage(new ToolsPage()); var toolsNavigation = new ExtendedNavigationPage(new ToolsPage());
favoritesNavigation.Title = AppResources.Favorites; favoritesNavigation.Title = AppResources.Favorites;

View File

@ -15,7 +15,7 @@ using System.Collections.Generic;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class VaultAutofillListLoginsPage : ExtendedContentPage public class VaultAutofillListCiphersPage : ExtendedContentPage
{ {
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IDeviceInfoService _deviceInfoService; private readonly IDeviceInfoService _deviceInfoService;
@ -24,7 +24,7 @@ namespace Bit.App.Pages
private CancellationTokenSource _filterResultsCancellationTokenSource; private CancellationTokenSource _filterResultsCancellationTokenSource;
private readonly string _name; private readonly string _name;
public VaultAutofillListLoginsPage(string uriString) public VaultAutofillListCiphersPage(string uriString)
: base(true) : base(true)
{ {
Uri = uriString; Uri = uriString;
@ -50,13 +50,13 @@ namespace Bit.App.Pages
Init(); Init();
} }
public ExtendedObservableCollection<VaultListPageModel.AutofillGrouping> PresentationLoginsGroup { get; private set; } public ExtendedObservableCollection<VaultListPageModel.AutofillGrouping> PresentationCiphersGroup { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.AutofillGrouping>(); = new ExtendedObservableCollection<VaultListPageModel.AutofillGrouping>();
public StackLayout NoDataStackLayout { get; set; } public StackLayout NoDataStackLayout { get; set; }
public ListView ListView { get; set; } public ListView ListView { get; set; }
public ActivityIndicator LoadingIndicator { get; set; } public ActivityIndicator LoadingIndicator { get; set; }
private SearchToolBarItem SearchItem { get; set; } private SearchToolBarItem SearchItem { get; set; }
private AddLoginToolBarItem AddLoginItem { get; set; } private AddCipherToolBarItem AddCipherItem { get; set; }
private IGoogleAnalyticsService GoogleAnalyticsService { get; set; } private IGoogleAnalyticsService GoogleAnalyticsService { get; set; }
private IUserDialogs UserDialogs { get; set; } private IUserDialogs UserDialogs { get; set; }
private string Uri { get; set; } private string Uri { get; set; }
@ -71,34 +71,34 @@ namespace Bit.App.Pages
Style = (Style)Application.Current.Resources["text-muted"] Style = (Style)Application.Current.Resources["text-muted"]
}; };
var addLoginButton = new ExtendedButton var addCipherButton = new ExtendedButton
{ {
Text = AppResources.AddALogin, Text = AppResources.AddALogin,
Command = new Command(() => AddLoginAsync()), Command = new Command(() => AddCipherAsync()),
Style = (Style)Application.Current.Resources["btn-primaryAccent"] Style = (Style)Application.Current.Resources["btn-primaryAccent"]
}; };
NoDataStackLayout = new StackLayout NoDataStackLayout = new StackLayout
{ {
Children = { noDataLabel, addLoginButton }, Children = { noDataLabel, addCipherButton },
VerticalOptions = LayoutOptions.CenterAndExpand, VerticalOptions = LayoutOptions.CenterAndExpand,
Padding = new Thickness(20, 0), Padding = new Thickness(20, 0),
Spacing = 20 Spacing = 20
}; };
AddLoginItem = new AddLoginToolBarItem(this); AddCipherItem = new AddCipherToolBarItem(this);
ToolbarItems.Add(AddLoginItem); ToolbarItems.Add(AddCipherItem);
SearchItem = new SearchToolBarItem(this); SearchItem = new SearchToolBarItem(this);
ToolbarItems.Add(SearchItem); ToolbarItems.Add(SearchItem);
ListView = new ListView(ListViewCachingStrategy.RecycleElement) ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{ {
IsGroupingEnabled = true, IsGroupingEnabled = true,
ItemsSource = PresentationLoginsGroup, ItemsSource = PresentationCiphersGroup,
HasUnevenRows = true, HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new HeaderViewCell()), GroupHeaderTemplate = new DataTemplate(() => new HeaderViewCell()),
ItemTemplate = new DataTemplate(() => new VaultListViewCell( ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Login l) => MoreClickedAsync(l))) (VaultListPageModel.Cipher l) => MoreClickedAsync(l)))
}; };
if(Device.RuntimePlatform == Device.iOS) if(Device.RuntimePlatform == Device.iOS)
@ -121,8 +121,8 @@ namespace Bit.App.Pages
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
ListView.ItemSelected += LoginSelected; ListView.ItemSelected += CipherSelected;
AddLoginItem.InitEvents(); AddCipherItem.InitEvents();
SearchItem.InitEvents(); SearchItem.InitEvents();
_filterResultsCancellationTokenSource = FetchAndLoadVault(); _filterResultsCancellationTokenSource = FetchAndLoadVault();
} }
@ -130,21 +130,21 @@ namespace Bit.App.Pages
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
ListView.ItemSelected -= LoginSelected; ListView.ItemSelected -= CipherSelected;
AddLoginItem.Dispose(); AddCipherItem.Dispose();
SearchItem.Dispose(); SearchItem.Dispose();
} }
protected override bool OnBackButtonPressed() protected override bool OnBackButtonPressed()
{ {
GoogleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App"); GoogleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Login)null); MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Cipher)null);
return true; return true;
} }
private void AdjustContent() private void AdjustContent()
{ {
if(PresentationLoginsGroup.Count > 0) if(PresentationCiphersGroup.Count > 0)
{ {
Content = ListView; Content = ListView;
} }
@ -162,18 +162,18 @@ namespace Bit.App.Pages
Task.Run(async () => Task.Run(async () =>
{ {
var autofillGroupings = new List<VaultListPageModel.AutofillGrouping>(); var autofillGroupings = new List<VaultListPageModel.AutofillGrouping>();
var logins = await _cipherService.GetAllAsync(Uri); var ciphers = await _cipherService.GetAllAsync(Uri);
var normalLogins = logins?.Item1.Select(l => new VaultListPageModel.AutofillLogin(l, false)) var normalLogins = ciphers?.Item1.Select(l => new VaultListPageModel.AutofillCipher(l, false))
.OrderBy(s => s.Name) .OrderBy(s => s.Name)
.ThenBy(s => s.Username) .ThenBy(s => s.Subtitle)
.ToList(); .ToList();
if(normalLogins?.Any() ?? false) if(normalLogins?.Any() ?? false)
{ {
autofillGroupings.Add(new VaultListPageModel.AutofillGrouping(normalLogins, AppResources.MatchingLogins)); autofillGroupings.Add(new VaultListPageModel.AutofillGrouping(normalLogins, AppResources.MatchingLogins));
} }
var fuzzyLogins = logins?.Item2.Select(l => new VaultListPageModel.AutofillLogin(l, true)) var fuzzyLogins = ciphers?.Item2.Select(l => new VaultListPageModel.AutofillCipher(l, true))
.OrderBy(s => s.Name) .OrderBy(s => s.Name)
.ThenBy(s => s.Username) .ThenBy(s => s.Username)
.ToList(); .ToList();
@ -187,7 +187,7 @@ namespace Bit.App.Pages
{ {
if(autofillGroupings.Any()) if(autofillGroupings.Any())
{ {
PresentationLoginsGroup.ResetWithRange(autofillGroupings); PresentationCiphersGroup.ResetWithRange(autofillGroupings);
} }
AdjustContent(); AdjustContent();
@ -197,22 +197,22 @@ namespace Bit.App.Pages
return cts; return cts;
} }
private async void LoginSelected(object sender, SelectedItemChangedEventArgs e) private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
{ {
var login = e.SelectedItem as VaultListPageModel.AutofillLogin; var cipher = e.SelectedItem as VaultListPageModel.AutofillCipher;
if(login == null) if(cipher == null)
{ {
return; return;
} }
if(_deviceInfoService.Version < 21) if(_deviceInfoService.Version < 21)
{ {
MoreClickedAsync(login); MoreClickedAsync(cipher);
} }
else else
{ {
bool doAutofill = true; bool doAutofill = true;
if(login.Fuzzy) if(cipher.Fuzzy)
{ {
doAutofill = await UserDialogs.ConfirmAsync( doAutofill = await UserDialogs.ConfirmAsync(
string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, _name), string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, _name),
@ -222,50 +222,73 @@ namespace Bit.App.Pages
if(doAutofill) if(doAutofill)
{ {
GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App"); GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", login as VaultListPageModel.Login); MessagingCenter.Send(Application.Current, "Autofill", cipher as VaultListPageModel.Cipher);
} }
} }
((ListView)sender).SelectedItem = null; ((ListView)sender).SelectedItem = null;
} }
private async void AddLoginAsync() private async void AddCipherAsync()
{ {
var page = new VaultAddLoginPage(Uri, _name, true); var page = new VaultAddLoginPage(Uri, _name, true);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
private async void MoreClickedAsync(VaultListPageModel.Login login) private async void MoreClickedAsync(VaultListPageModel.Cipher cipher)
{ {
var buttons = new List<string> { AppResources.View, AppResources.Edit }; var buttons = new List<string> { AppResources.View, AppResources.Edit };
if(!string.IsNullOrWhiteSpace(login.Password.Value))
if(cipher.Type == Enums.CipherType.Login)
{ {
buttons.Add(AppResources.CopyPassword); if(!string.IsNullOrWhiteSpace(cipher.Password.Value))
{
buttons.Add(AppResources.CopyPassword);
}
if(!string.IsNullOrWhiteSpace(cipher.Username))
{
buttons.Add(AppResources.CopyUsername);
}
} }
if(!string.IsNullOrWhiteSpace(login.Username)) else if(cipher.Type == Enums.CipherType.Card)
{ {
buttons.Add(AppResources.CopyUsername); if(!string.IsNullOrWhiteSpace(cipher.CardNumber))
{
buttons.Add(AppResources.CopyNumber);
}
if(!string.IsNullOrWhiteSpace(cipher.CardCode.Value))
{
buttons.Add(AppResources.CopySecurityCode);
}
} }
var selection = await DisplayActionSheet(login.Name, AppResources.Cancel, null, buttons.ToArray()); var selection = await DisplayActionSheet(cipher.Name, AppResources.Cancel, null, buttons.ToArray());
if(selection == AppResources.View) if(selection == AppResources.View)
{ {
var page = new VaultViewLoginPage(login.Id); var page = new VaultViewLoginPage(cipher.Id);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
else if(selection == AppResources.Edit) else if(selection == AppResources.Edit)
{ {
var page = new VaultEditLoginPage(login.Id); var page = new VaultEditLoginPage(cipher.Id);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
else if(selection == AppResources.CopyPassword) else if(selection == AppResources.CopyPassword)
{ {
Copy(login.Password.Value, AppResources.Password); Copy(cipher.Password.Value, AppResources.Password);
} }
else if(selection == AppResources.CopyUsername) else if(selection == AppResources.CopyUsername)
{ {
Copy(login.Username, AppResources.Username); Copy(cipher.Username, AppResources.Username);
}
else if(selection == AppResources.CopyNumber)
{
Copy(cipher.CardNumber, AppResources.Number);
}
else if(selection == AppResources.CopySecurityCode)
{
Copy(cipher.CardCode.Value, AppResources.SecurityCode);
} }
} }
@ -275,26 +298,26 @@ namespace Bit.App.Pages
UserDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); UserDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
} }
private class AddLoginToolBarItem : ExtendedToolbarItem private class AddCipherToolBarItem : ExtendedToolbarItem
{ {
public AddLoginToolBarItem(VaultAutofillListLoginsPage page) public AddCipherToolBarItem(VaultAutofillListCiphersPage page)
: base(() => page.AddLoginAsync()) : base(() => page.AddCipherAsync())
{ {
Text = AppResources.Add; Text = AppResources.Add;
Icon = "plus"; Icon = "plus.png";
Priority = 2; Priority = 2;
} }
} }
private class SearchToolBarItem : ExtendedToolbarItem private class SearchToolBarItem : ExtendedToolbarItem
{ {
private readonly VaultAutofillListLoginsPage _page; private readonly VaultAutofillListCiphersPage _page;
public SearchToolBarItem(VaultAutofillListLoginsPage page) public SearchToolBarItem(VaultAutofillListCiphersPage page)
{ {
_page = page; _page = page;
Text = AppResources.Search; Text = AppResources.Search;
Icon = "search"; Icon = "search.png";
Priority = 1; Priority = 1;
ClickAction = () => DoClick(); ClickAction = () => DoClick();
} }

View File

@ -17,7 +17,7 @@ using FFImageLoading.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class VaultListLoginsPage : ExtendedContentPage public class VaultListCiphersPage : ExtendedContentPage
{ {
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
@ -32,7 +32,7 @@ namespace Bit.App.Pages
private readonly bool _favorites; private readonly bool _favorites;
private CancellationTokenSource _filterResultsCancellationTokenSource; private CancellationTokenSource _filterResultsCancellationTokenSource;
public VaultListLoginsPage(bool favorites, string uri = null) public VaultListCiphersPage(bool favorites, string uri = null)
: base(true) : base(true)
{ {
_favorites = favorites; _favorites = favorites;
@ -57,13 +57,13 @@ namespace Bit.App.Pages
public ExtendedObservableCollection<VaultListPageModel.Folder> PresentationFolders { get; private set; } public ExtendedObservableCollection<VaultListPageModel.Folder> PresentationFolders { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.Folder>(); = new ExtendedObservableCollection<VaultListPageModel.Folder>();
public ListView ListView { get; set; } public ListView ListView { get; set; }
public VaultListPageModel.Login[] Logins { get; set; } = new VaultListPageModel.Login[] { }; public VaultListPageModel.Cipher[] Ciphers { get; set; } = new VaultListPageModel.Cipher[] { };
public VaultListPageModel.Folder[] Folders { get; set; } = new VaultListPageModel.Folder[] { }; public VaultListPageModel.Folder[] Folders { get; set; } = new VaultListPageModel.Folder[] { };
public SearchBar Search { get; set; } public SearchBar Search { get; set; }
public StackLayout NoDataStackLayout { get; set; } public StackLayout NoDataStackLayout { get; set; }
public StackLayout ResultsStackLayout { get; set; } public StackLayout ResultsStackLayout { get; set; }
public ActivityIndicator LoadingIndicator { get; set; } public ActivityIndicator LoadingIndicator { get; set; }
private AddLoginToolBarItem AddLoginItem { get; set; } private AddCipherToolBarItem AddCipherItem { get; set; }
public string Uri { get; set; } public string Uri { get; set; }
private void Init() private void Init()
@ -78,8 +78,8 @@ namespace Bit.App.Pages
if(!_favorites) if(!_favorites)
{ {
AddLoginItem = new AddLoginToolBarItem(this); AddCipherItem = new AddCipherToolBarItem(this);
ToolbarItems.Add(AddLoginItem); ToolbarItems.Add(AddCipherItem);
} }
ListView = new ListView(ListViewCachingStrategy.RecycleElement) ListView = new ListView(ListViewCachingStrategy.RecycleElement)
@ -89,7 +89,7 @@ namespace Bit.App.Pages
HasUnevenRows = true, HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)), GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)),
ItemTemplate = new DataTemplate(() => new VaultListViewCell( ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Login l) => MoreClickedAsync(l))) (VaultListPageModel.Cipher c) => MoreClickedAsync(c)))
}; };
if(Device.RuntimePlatform == Device.iOS) if(Device.RuntimePlatform == Device.iOS)
@ -135,14 +135,14 @@ namespace Bit.App.Pages
if(!_favorites) if(!_favorites)
{ {
var addLoginButton = new ExtendedButton var addCipherButton = new ExtendedButton
{ {
Text = AppResources.AddALogin, Text = AppResources.AddALogin,
Command = new Command(() => AddLogin()), Command = new Command(() => AddCipher()),
Style = (Style)Application.Current.Resources["btn-primaryAccent"] Style = (Style)Application.Current.Resources["btn-primaryAccent"]
}; };
NoDataStackLayout.Children.Add(addLoginButton); NoDataStackLayout.Children.Add(addCipherButton);
} }
LoadingIndicator = new ActivityIndicator LoadingIndicator = new ActivityIndicator
@ -208,28 +208,28 @@ namespace Bit.App.Pages
if(string.IsNullOrWhiteSpace(searchFilter)) if(string.IsNullOrWhiteSpace(searchFilter))
{ {
LoadFolders(Logins, ct); LoadFolders(Ciphers, ct);
} }
else else
{ {
searchFilter = searchFilter.ToLower(); searchFilter = searchFilter.ToLower();
var filteredLogins = Logins var filteredCiphers = Ciphers
.Where(s => s.Name.ToLower().Contains(searchFilter) || s.Username.ToLower().Contains(searchFilter)) .Where(s => s.Name.ToLower().Contains(searchFilter) || s.Subtitle.ToLower().Contains(searchFilter))
.TakeWhile(s => !ct.IsCancellationRequested) .TakeWhile(s => !ct.IsCancellationRequested)
.ToArray(); .ToArray();
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
LoadFolders(filteredLogins, ct); LoadFolders(filteredCiphers, ct);
} }
} }
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
ListView.ItemSelected += LoginSelected; ListView.ItemSelected += CipherSelected;
Search.TextChanged += SearchBar_TextChanged; Search.TextChanged += SearchBar_TextChanged;
Search.SearchButtonPressed += SearchBar_SearchButtonPressed; Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
AddLoginItem?.InitEvents(); AddCipherItem?.InitEvents();
_filterResultsCancellationTokenSource = FetchAndLoadVault(); _filterResultsCancellationTokenSource = FetchAndLoadVault();
@ -267,10 +267,10 @@ namespace Bit.App.Pages
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
ListView.ItemSelected -= LoginSelected; ListView.ItemSelected -= CipherSelected;
Search.TextChanged -= SearchBar_TextChanged; Search.TextChanged -= SearchBar_TextChanged;
Search.SearchButtonPressed -= SearchBar_SearchButtonPressed; Search.SearchButtonPressed -= SearchBar_SearchButtonPressed;
AddLoginItem?.Dispose(); AddCipherItem?.Dispose();
} }
protected override bool OnBackButtonPressed() protected override bool OnBackButtonPressed()
@ -281,7 +281,7 @@ namespace Bit.App.Pages
} }
_googleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App"); _googleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Login)null); MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Cipher)null);
return true; return true;
} }
@ -310,21 +310,21 @@ namespace Bit.App.Pages
Task.Run(async () => Task.Run(async () =>
{ {
var foldersTask = _folderService.GetAllAsync(); var foldersTask = _folderService.GetAllAsync();
var loginsTask = _favorites ? _cipherService.GetAllAsync(true) : _cipherService.GetAllAsync(); var ciphersTask = _favorites ? _cipherService.GetAllAsync(true) : _cipherService.GetAllAsync();
await Task.WhenAll(foldersTask, loginsTask); await Task.WhenAll(foldersTask, ciphersTask);
var folders = await foldersTask; var folders = await foldersTask;
var logins = await loginsTask; var ciphers = await ciphersTask;
Folders = folders Folders = folders
.Select(f => new VaultListPageModel.Folder(f)) .Select(f => new VaultListPageModel.Folder(f))
.OrderBy(s => s.Name) .OrderBy(s => s.Name)
.ToArray(); .ToArray();
Logins = logins Ciphers = ciphers
.Select(s => new VaultListPageModel.Login(s)) .Select(s => new VaultListPageModel.Cipher(s))
.OrderBy(s => s.Name) .OrderBy(s => s.Name)
.ThenBy(s => s.Username) .ThenBy(s => s.Subtitle)
.ToArray(); .ToArray();
try try
@ -337,7 +337,7 @@ namespace Bit.App.Pages
return cts; return cts;
} }
private void LoadFolders(VaultListPageModel.Login[] logins, CancellationToken ct) private void LoadFolders(VaultListPageModel.Cipher[] ciphers, CancellationToken ct)
{ {
var folders = new List<VaultListPageModel.Folder>(Folders); var folders = new List<VaultListPageModel.Folder>(Folders);
@ -348,16 +348,16 @@ namespace Bit.App.Pages
folder.Clear(); folder.Clear();
} }
var loginsToAdd = logins var ciphersToAdd = ciphers
.Where(s => s.FolderId == folder.Id) .Where(s => s.FolderId == folder.Id)
.TakeWhile(s => !ct.IsCancellationRequested) .TakeWhile(s => !ct.IsCancellationRequested)
.ToList(); .ToList();
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
folder.AddRange(loginsToAdd); folder.AddRange(ciphersToAdd);
} }
var noneToAdd = logins var noneToAdd = ciphers
.Where(s => s.FolderId == null) .Where(s => s.FolderId == null)
.TakeWhile(s => !ct.IsCancellationRequested) .TakeWhile(s => !ct.IsCancellationRequested)
.ToList(); .ToList();
@ -380,10 +380,10 @@ namespace Bit.App.Pages
}); });
} }
private async void LoginSelected(object sender, SelectedItemChangedEventArgs e) private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
{ {
var login = e.SelectedItem as VaultListPageModel.Login; var cipher = e.SelectedItem as VaultListPageModel.Cipher;
if(login == null) if(cipher == null)
{ {
return; return;
} }
@ -397,65 +397,88 @@ namespace Bit.App.Pages
if(selection == AppResources.View || string.IsNullOrWhiteSpace(Uri)) if(selection == AppResources.View || string.IsNullOrWhiteSpace(Uri))
{ {
var page = new VaultViewLoginPage(login.Id); var page = new VaultViewLoginPage(cipher.Id);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
else if(selection == AppResources.Autofill) else if(selection == AppResources.Autofill)
{ {
if(_deviceInfoService.Version < 21) if(_deviceInfoService.Version < 21)
{ {
MoreClickedAsync(login); MoreClickedAsync(cipher);
} }
else else
{ {
_googleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App"); _googleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", login); MessagingCenter.Send(Application.Current, "Autofill", cipher);
} }
} }
((ListView)sender).SelectedItem = null; ((ListView)sender).SelectedItem = null;
} }
private async void MoreClickedAsync(VaultListPageModel.Login login) private async void MoreClickedAsync(VaultListPageModel.Cipher cipher)
{ {
var buttons = new List<string> { AppResources.View, AppResources.Edit }; var buttons = new List<string> { AppResources.View, AppResources.Edit };
if(!string.IsNullOrWhiteSpace(login.Password.Value))
if(cipher.Type == Enums.CipherType.Login)
{ {
buttons.Add(AppResources.CopyPassword); if(!string.IsNullOrWhiteSpace(cipher.Password.Value))
{
buttons.Add(AppResources.CopyPassword);
}
if(!string.IsNullOrWhiteSpace(cipher.Username))
{
buttons.Add(AppResources.CopyUsername);
}
if(!string.IsNullOrWhiteSpace(cipher.Uri.Value) && (cipher.Uri.Value.StartsWith("http://")
|| cipher.Uri.Value.StartsWith("https://")))
{
buttons.Add(AppResources.GoToWebsite);
}
} }
if(!string.IsNullOrWhiteSpace(login.Username)) else if(cipher.Type == Enums.CipherType.Card)
{ {
buttons.Add(AppResources.CopyUsername); if(!string.IsNullOrWhiteSpace(cipher.CardNumber))
} {
if(!string.IsNullOrWhiteSpace(login.Uri.Value) && (login.Uri.Value.StartsWith("http://") buttons.Add(AppResources.CopyNumber);
|| login.Uri.Value.StartsWith("https://"))) }
{ if(!string.IsNullOrWhiteSpace(cipher.CardCode.Value))
buttons.Add(AppResources.GoToWebsite); {
buttons.Add(AppResources.CopySecurityCode);
}
} }
var selection = await DisplayActionSheet(login.Name, AppResources.Cancel, null, buttons.ToArray()); var selection = await DisplayActionSheet(cipher.Name, AppResources.Cancel, null, buttons.ToArray());
if(selection == AppResources.View) if(selection == AppResources.View)
{ {
var page = new VaultViewLoginPage(login.Id); var page = new VaultViewLoginPage(cipher.Id);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
else if(selection == AppResources.Edit) else if(selection == AppResources.Edit)
{ {
var page = new VaultEditLoginPage(login.Id); var page = new VaultEditLoginPage(cipher.Id);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
else if(selection == AppResources.CopyPassword) else if(selection == AppResources.CopyPassword)
{ {
Copy(login.Password.Value, AppResources.Password); Copy(cipher.Password.Value, AppResources.Password);
} }
else if(selection == AppResources.CopyUsername) else if(selection == AppResources.CopyUsername)
{ {
Copy(login.Username, AppResources.Username); Copy(cipher.Username, AppResources.Username);
} }
else if(selection == AppResources.GoToWebsite) else if(selection == AppResources.GoToWebsite)
{ {
Device.OpenUri(new Uri(login.Uri.Value)); Device.OpenUri(new Uri(cipher.Uri.Value));
}
else if(selection == AppResources.CopyNumber)
{
Copy(cipher.CardNumber, AppResources.Number);
}
else if(selection == AppResources.CopySecurityCode)
{
Copy(cipher.CardCode.Value, AppResources.SecurityCode);
} }
} }
@ -465,18 +488,18 @@ namespace Bit.App.Pages
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); _userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
} }
private async void AddLogin() private async void AddCipher()
{ {
var page = new VaultAddLoginPage(Uri); var page = new VaultAddLoginPage(Uri);
await Navigation.PushForDeviceAsync(page); await Navigation.PushForDeviceAsync(page);
} }
private class AddLoginToolBarItem : ExtendedToolbarItem private class AddCipherToolBarItem : ExtendedToolbarItem
{ {
private readonly VaultListLoginsPage _page; private readonly VaultListCiphersPage _page;
public AddLoginToolBarItem(VaultListLoginsPage page) public AddCipherToolBarItem(VaultListCiphersPage page)
: base(() => page.AddLogin()) : base(() => page.AddCipher())
{ {
_page = page; _page = page;
Text = AppResources.Add; Text = AppResources.Add;
@ -486,7 +509,7 @@ namespace Bit.App.Pages
private class VaultListHeaderViewCell : ExtendedViewCell private class VaultListHeaderViewCell : ExtendedViewCell
{ {
public VaultListHeaderViewCell(VaultListLoginsPage page) public VaultListHeaderViewCell(VaultListCiphersPage page)
{ {
var image = new CachedImage var image = new CachedImage
{ {

View File

@ -646,6 +646,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Copy Number.
/// </summary>
public static string CopyNumber {
get {
return ResourceManager.GetString("CopyNumber", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy Password. /// Looks up a localized string similar to Copy Password.
/// </summary> /// </summary>
@ -655,6 +664,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Copy Security Code.
/// </summary>
public static string CopySecurityCode {
get {
return ResourceManager.GetString("CopySecurityCode", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy TOTP. /// Looks up a localized string similar to Copy TOTP.
/// </summary> /// </summary>
@ -1816,6 +1834,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Number.
/// </summary>
public static string Number {
get {
return ResourceManager.GetString("Number", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Ok. /// Looks up a localized string similar to Ok.
/// </summary> /// </summary>
@ -2077,6 +2104,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Security Code.
/// </summary>
public static string SecurityCode {
get {
return ResourceManager.GetString("SecurityCode", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to See Development Progress. /// Looks up a localized string similar to See Development Progress.
/// </summary> /// </summary>

View File

@ -1039,4 +1039,16 @@
<data name="NoCustomFields" xml:space="preserve"> <data name="NoCustomFields" xml:space="preserve">
<value>No custom fields. You can fully manage custom fields from the web vault or browser extension.</value> <value>No custom fields. You can fully manage custom fields from the web vault or browser extension.</value>
</data> </data>
<data name="CopyNumber" xml:space="preserve">
<value>Copy Number</value>
</data>
<data name="CopySecurityCode" xml:space="preserve">
<value>Copy Security Code</value>
</data>
<data name="Number" xml:space="preserve">
<value>Number</value>
</data>
<data name="SecurityCode" xml:space="preserve">
<value>Security Code</value>
</data>
</root> </root>