1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-06 10:34:07 +00:00

[PM-2320] Improve Android block Auto-fill URIs (#2616)

* PM-2320 Added new view for block autofill URIs on Android

* PM-2320 Fix formatting

* PM-2320 Improved validations on block autofill uris

* PM-2320 Improved autofill block uris placeholder colors on different themes
This commit is contained in:
Federico Maccaroni
2023-07-18 11:25:38 -03:00
committed by GitHub
parent c678c17ebc
commit dd52ff0dcc
21 changed files with 783 additions and 100 deletions

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.BlockAutofillUrisPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:BlockAutofillUrisPageViewModel"
Title="{u:I18n BlockAutoFill}">
<ContentPage.BindingContext>
<pages:BlockAutofillUrisPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout Orientation="Vertical">
<Image
x:Name="_emptyUrisPlaceholder"
HorizontalOptions="Center"
WidthRequest="120"
HeightRequest="120"
Margin="0,100,0,0"
IsVisible="{Binding ShowList, Converter={StaticResource inverseBool}}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ThereAreNoBlockedURIs}" />
<controls:CustomLabel
StyleClass="box-label-regular"
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
FontWeight="500"
HorizontalTextAlignment="Center"
Margin="14,10,14,0"/>
<controls:ExtendedCollectionView
ItemsSource="{Binding BlockedUris}"
IsVisible="{Binding ShowList}"
VerticalOptions="FillAndExpand"
Margin="0,5,0,0"
SelectionMode="None"
StyleClass="list, list-platform"
ExtraDataForLogging="Blocked Autofill Uris"
AutomationId="BlockedUrisCellList">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="pages:BlockAutofillUriItemViewModel">
<StackLayout
Orientation="Vertical"
AutomationId="BlockedUriCell">
<StackLayout
Orientation="Horizontal">
<controls:CustomLabel
VerticalOptions="Center"
StyleClass="box-label-regular"
Text="{Binding Uri}"
MaxLines="2"
LineBreakMode="TailTruncation"
FontWeight="500"
Margin="15,0,0,0"
HorizontalOptions="StartAndExpand"/>
<controls:IconButton
StyleClass="box-row-button-muted, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.PencilSquare}}"
Command="{Binding EditUriCommand}"
Margin="5,0,15,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n EditURI}"
AutomationId="EditUriButton" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</controls:ExtendedCollectionView>
<Button
Text="{u:I18n NewBlockedURI}"
Command="{Binding AddUriCommand}"
VerticalOptions="End"
HeightRequest="40"
Opacity="0.8"
Margin="14,5,14,10"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NewBlockedURI}"
AutomationId="NewBlockedUriButton" />
</StackLayout>
</pages:BaseContentPage>

View File

@@ -0,0 +1,44 @@
using System.Threading.Tasks;
using Bit.App.Styles;
using Bit.App.Utilities;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class BlockAutofillUrisPage : BaseContentPage, IThemeDirtablePage
{
private readonly BlockAutofillUrisPageViewModel _vm;
public BlockAutofillUrisPage()
{
InitializeComponent();
_vm = BindingContext as BlockAutofillUrisPageViewModel;
_vm.Page = this;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.InitAsync().FireAndForget(_ => Navigation.PopAsync());
UpdatePlaceholder();
}
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();
UpdatePlaceholder();
}
private void UpdatePlaceholder()
{
MainThread.BeginInvokeOnMainThread(() =>
_emptyUrisPlaceholder.Source = ImageSource.FromFile(ThemeManager.UsingLightTheme ? "empty_uris_placeholder" : "empty_uris_placeholder_dark"));
}
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class BlockAutofillUrisPageViewModel : BaseViewModel
{
private const char URI_SEPARARTOR = ',';
private const string URI_FORMAT = "https://domain.com";
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
public BlockAutofillUrisPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
AddUriCommand = new AsyncCommand(AddUriAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
EditUriCommand = new AsyncCommand<BlockAutofillUriItemViewModel>(EditUriAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
}
public ObservableRangeCollection<BlockAutofillUriItemViewModel> BlockedUris { get; set; } = new ObservableRangeCollection<BlockAutofillUriItemViewModel>();
public bool ShowList => BlockedUris.Any();
public ICommand AddUriCommand { get; }
public ICommand EditUriCommand { get; }
public async Task InitAsync()
{
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
if (blockedUrisList?.Any() != true)
{
return;
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.AddRange(blockedUrisList.OrderBy(uri => uri).Select(u => new BlockAutofillUriItemViewModel(u, EditUriCommand)).ToList());
TriggerPropertyChanged(nameof(ShowList));
});
}
private async Task AddUriAsync()
{
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
{
Title = AppResources.NewUri,
Subtitle = AppResources.EnterURI,
ValueSubInfo = string.Format(AppResources.FormatXSeparateMultipleURIsWithAComma, URI_FORMAT),
OkButtonText = AppResources.Save,
ValidateText = text => ValidateUris(text, true)
});
if (response?.Text is null)
{
return;
}
await MainThread.InvokeOnMainThreadAsync(() =>
{
foreach (var uri in response.Value.Text.Split(URI_SEPARARTOR).Where(s => !string.IsNullOrEmpty(s)))
{
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
}
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
TriggerPropertyChanged(nameof(BlockedUris));
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URISaved);
}
private async Task EditUriAsync(BlockAutofillUriItemViewModel uriItemViewModel)
{
var response = await _deviceActionService.DisplayValidatablePromptAsync(new Utilities.Prompts.ValidatablePromptConfig
{
Title = AppResources.EditURI,
Subtitle = AppResources.EnterURI,
Text = uriItemViewModel.Uri,
ValueSubInfo = string.Format(AppResources.FormatX, URI_FORMAT),
OkButtonText = AppResources.Save,
ThirdButtonText = AppResources.Remove,
ValidateText = text => ValidateUris(text, false)
});
if (response is null)
{
return;
}
if (response.Value.ExecuteThirdAction)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.Remove(uriItemViewModel);
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URIRemoved);
return;
}
var cleanedUri = response.Value.Text.Replace(Environment.NewLine, string.Empty).Trim();
await MainThread.InvokeOnMainThreadAsync(() =>
{
BlockedUris.Remove(uriItemViewModel);
BlockedUris.Add(new BlockAutofillUriItemViewModel(cleanedUri, EditUriCommand));
BlockedUris = new ObservableRangeCollection<BlockAutofillUriItemViewModel>(BlockedUris.OrderBy(b => b.Uri));
TriggerPropertyChanged(nameof(BlockedUris));
TriggerPropertyChanged(nameof(ShowList));
});
await UpdateAutofillBlacklistedUrisAsync();
_deviceActionService.Toast(AppResources.URISaved);
}
private string ValidateUris(string uris, bool allowMultipleUris)
{
if (string.IsNullOrWhiteSpace(uris))
{
return string.Format(AppResources.FormatX, URI_FORMAT);
}
if (!allowMultipleUris && uris.Contains(URI_SEPARARTOR))
{
return AppResources.CannotEditMultipleURIsAtOnce;
}
foreach (var uri in uris.Split(URI_SEPARARTOR).Where(u => !string.IsNullOrWhiteSpace(u)))
{
var cleanedUri = uri.Replace(Environment.NewLine, string.Empty).Trim();
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
{
return AppResources.InvalidFormatUseHttpsHttpOrAndroidApp;
}
if (!Uri.TryCreate(cleanedUri, UriKind.Absolute, out var _))
{
return AppResources.InvalidURI;
}
if (BlockedUris.Any(uriItem => uriItem.Uri == cleanedUri))
{
return string.Format(AppResources.TheURIXIsAlreadyBlocked, cleanedUri);
}
}
return null;
}
private async Task UpdateAutofillBlacklistedUrisAsync()
{
await _stateService.SetAutofillBlacklistedUrisAsync(BlockedUris.Any() ? BlockedUris.Select(bu => bu.Uri).ToList() : null);
}
}
public class BlockAutofillUriItemViewModel : ExtendedViewModel
{
public BlockAutofillUriItemViewModel(string uri, ICommand editUriCommand)
{
Uri = uri;
EditUriCommand = new Command(() => editUriCommand.Execute(this));
}
public string Uri { get; }
public ICommand EditUriCommand { get; }
}
}

View File

@@ -153,22 +153,14 @@
StyleClass="box-footer-label, box-footer-label-switch" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n AutofillBlockedUris}"
StyleClass="box-label" />
<Editor
x:Name="_autofillBlockedUrisEditor"
Text="{Binding AutofillBlockedUris}"
StyleClass="box-value"
AutoSize="TextChanges"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
Keyboard="Url"
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
</StackLayout>
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding GoToBlockAutofillUrisCommand}" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n AutofillBlockedUrisDescription}"
Text="{u:I18n BlockAutoFill}"
StyleClass="box-label-regular" />
<Label
Text="{u:I18n AutoFillWillNotBeOfferedForTheseURIs}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>

View File

@@ -1,6 +1,4 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
@@ -44,17 +42,6 @@ namespace Bit.App.Pages
await _vm.InitAsync();
}
protected async override void OnDisappearing()
{
base.OnDisappearing();
await _vm.UpdateAutofillBlockedUris();
}
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
{
await _vm.UpdateAutofillBlockedUris();
}
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())

View File

@@ -1,12 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -19,7 +20,6 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private bool _autofillSavePrompt;
private string _autofillBlockedUris;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
@@ -84,6 +84,10 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>(null, AppResources.DefaultSystem)
};
LocalesOptions.AddRange(_i18nService.LocaleNames.ToList());
GoToBlockAutofillUrisCommand = new AsyncCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
}
public List<KeyValuePair<int?, string>> ClearClipboardOptions { get; set; }
@@ -192,25 +196,18 @@ namespace Bit.App.Pages
}
}
public string AutofillBlockedUris
{
get => _autofillBlockedUris;
set => SetProperty(ref _autofillBlockedUris, value);
}
public bool ShowAndroidAutofillSettings
{
get => _showAndroidAutofillSettings;
set => SetProperty(ref _showAndroidAutofillSettings, value);
}
public ICommand GoToBlockAutofillUrisCommand { get; }
public async Task InitAsync()
{
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
@@ -288,41 +285,6 @@ namespace Bit.App.Pages
}
}
public async Task UpdateAutofillBlockedUris()
{
if (_inited)
{
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
{
await _stateService.SetAutofillBlacklistedUrisAsync(null);
AutofillBlockedUris = null;
return;
}
try
{
var csv = AutofillBlockedUris;
var urisList = new List<string>();
foreach (var uri in csv.Split(','))
{
if (string.IsNullOrWhiteSpace(uri))
{
continue;
}
var cleanedUri = uri.Replace(System.Environment.NewLine, string.Empty).Trim();
if (!cleanedUri.StartsWith("http://") && !cleanedUri.StartsWith("https://") &&
!cleanedUri.StartsWith(Constants.AndroidAppProtocol))
{
continue;
}
urisList.Add(cleanedUri);
}
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
AutofillBlockedUris = string.Join(", ", urisList);
}
catch { }
}
}
private async Task UpdateCurrentLocaleAsync()
{
if (!_inited)