[SG-166] Two Step Login - Feature Branch (#2157)

* [SG-166] Update fonts to have necessary icons

* [SG-166] Add new custom view to hold a button with a font icon and a label.

* [SG-166] Two Step login flow - Mobile (#2153)

* [SG-166] Add UI elements to Home and Login pages. Change VMs to function with new UI. Add new string resources.

* [SG-166] Pass email parameter from Home to Login page.

* [SG-166] Pass email to password hint page.

* [SG-166] Remove remembered email from account switching.

* [SG-166] Add GetKnownDevice endpoint to ApiService

* [SG-166] Fix GetKnownDevice string uri

* [SG-166] Add Renderer for IconLabel control. Add RemoveFontPadding bool property.

* [SG-166] include IconLabelRenderer in Android csproj file

* [SG-166] Add new control. Add styles for the control.

* [SG-166] Add verification to start login if email is remembered

* [SG-166] Pass default email to hint page

* [SG-166] Login with device button only shows if it is a known device.

* [SG-166] Change Remember Email to Remember me. Change Check to Switch control.

* [SG-166] Add command to button for SSO Login

* Revert "[SG-166] Update fonts to have necessary icons"

This reverts commit 472b541cef.

* [SG-166] Remove IconLabel Android renderer. Add RemoveFontPadding effect.

* [SG-166] Update font with new device and suitcase icon

* [SG-166] Fix RemoveFontPadding effect

* [SG-166] Remove unused property in IconLabel

* [SG-166] Fix formatting on IconLabelButton.xaml

* [SG-166] Update padding effect to IconLabel

* [SG-166] Add control variable to run code once on create

* [SG-166] Add email validation before continue

* [SG-166] Refactor icons

* [SG-166] Update iOS Extension font

* [SG-166] Remove HomePage login btn step

* [SG-166] Make clickable area smaller

* [SG-166] Fix hint filled by default

* [SG-166] Fix IconButton font issue

* [SG-166] Fix iOS extension

* [SG-166] Move style to Base instead of platforms

* [SG-166] Fix layout for IconLabelButton

* [SG-166] Switched EventHandler for Command

* [SG-166] Removed event handler

* [SG-166] Fix LoginPage layout options

* [SG-166] Fix extensions Login null email

* [SG-166] Move remembered email logic to viewmodel

* [SG-166] Protect method and show dialog in case of error

* [SG-166] Rename of GetKnownDevice api method

* [SG-166] rename text resource key name

* [SG-166] Add close button to iOS extension

* [SG-166] Switch event handlers for commands

* [SG-166] Change commands UI thread invocation.

* [SG-166] Remove Login with device button from the UI

* [SG-166] Fixed appOptions and close button on iOS Extensions
This commit is contained in:
André Bispo 2022-10-28 23:10:41 +01:00 committed by GitHub
parent 89adab6784
commit b9b9c2e5ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 611 additions and 122 deletions

View file

@ -156,6 +156,7 @@
<Compile Include="Services\FileService.cs" />
<Compile Include="Services\AutofillHandler.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />

Binary file not shown.

View file

@ -0,0 +1,23 @@
using Android.Widget;
using Bit.Droid.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportEffect(typeof(RemoveFontPaddingEffect), nameof(RemoveFontPaddingEffect))]
namespace Bit.Droid.Effects
{
public class RemoveFontPaddingEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is TextView textView)
{
textView.SetIncludeFontPadding(false);
}
}
protected override void OnDetached()
{
}
}
}

View file

@ -139,6 +139,7 @@
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" />
</ItemGroup>
<ItemGroup>
@ -430,5 +431,6 @@
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />
<None Remove="Controls\IconLabelButton\" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@ -16,6 +17,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View file

@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@ -17,6 +18,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.IconLabelButton"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:Name="_iconLabelButton"
HeightRequest="45"
Padding="1"
StyleClass="btn-icon-secondary"
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
HasShadow="False">
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
</Frame.GestureRecognizers>
<Frame
Margin="0"
Padding="0"
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
IsClippedToBounds="True"
HasShadow="False">
<StackLayout
Orientation="Horizontal"
HorizontalOptions="Center">
<controls:IconLabel
VerticalOptions="Center"
HorizontalTextAlignment="Center"
FontSize="Large"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
</controls:IconLabel>
<Label
VerticalOptions="Center"
HorizontalTextAlignment="Center"
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
FontSize="Medium"
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
</StackLayout>
</Frame>
</Frame>

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.Core.Models.Domain;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Bit.App.Controls
{
public partial class IconLabelButton : Frame
{
public static readonly BindableProperty IconProperty = BindableProperty.Create(
nameof(Icon), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
nameof(Label), typeof(string), typeof(IconLabelButton));
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(Command), typeof(IconLabelButton));
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Color.White);
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Color.White);
public IconLabelButton()
{
InitializeComponent();
}
public string Icon
{
get => GetValue(IconProperty) as string;
set => SetValue(IconProperty, value);
}
public string Label
{
get => GetValue(LabelProperty) as string;
set => SetValue(LabelProperty, value);
}
public ICommand ButtonCommand
{
get => GetValue(ButtonCommandProperty) as ICommand;
set => SetValue(ButtonCommandProperty, value);
}
public Color IconLabelColor
{
get { return (Color)GetValue(IconLabelColorProperty); }
set { SetValue(IconLabelColorProperty, value); }
}
public Color IconLabelBackgroundColor
{
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
set { SetValue(IconLabelBackgroundColorProperty, value); }
}
public Color IconLabelBorderColor
{
get { return (Color)GetValue(IconLabelBorderColorProperty); }
set { SetValue(IconLabelBorderColorProperty, value); }
}
}
}

View file

@ -0,0 +1,13 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Effects
{
public class RemoveFontPaddingEffect : RoutingEffect
{
public RemoveFontPaddingEffect()
: base("Bitwarden.RemoveFontPaddingEffect")
{ }
}
}

View file

@ -6,11 +6,12 @@ namespace Bit.App.Pages
{
private HintPageViewModel _vm;
public HintPage()
public HintPage(string email = null)
{
InitializeComponent();
_vm = BindingContext as HintPageViewModel;
_vm.Page = this;
_vm.Email = email;
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);

View file

@ -15,6 +15,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
private string _email;
public HintPageViewModel()
{
@ -34,7 +35,12 @@ namespace Bit.App.Pages
}
public ICommand SubmitCommand { get; }
public string Email { get; set; }
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public async Task SubmitAsync()
{

View file

@ -24,6 +24,7 @@
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
<ToolbarItem
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
@ -32,30 +33,66 @@
<ContentPage.Resources>
<ResourceDictionary>
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="0" Padding="10, 5">
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center">
</Label>
<StackLayout Spacing="5">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n CreateAccount}"
Clicked="Register_Clicked" />
<Button Text="{u:I18n LogInSso}"
Clicked="LogInSso_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Margin="0,10,0,0"
Clicked="Cancel_Clicked" />
<u:InverseBoolConverter x:Key="inverseBool" />
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"/>
<StackLayout
StyleClass="box-row">
<Label
Text="{u:I18n EmailAddress}"
StyleClass="box-label" />
<Entry
x:Name="_email"
Text="{Binding Email}"
Keyboard="Email"
StyleClass="box-value">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Entry>
<StackLayout
Orientation="Horizontal"
Margin="0, 16, 0 ,0">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding RememberEmailCommand}" />
</StackLayout.GestureRecognizers>
<Label
Text="{u:I18n RememberMe}"
StyleClass="text-sm"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center"
VerticalTextAlignment="Center"/>
<Switch
Scale="0.8"
IsToggled="{Binding RememberEmail}"
VerticalOptions="Center"/>
</StackLayout>
</StackLayout>
<Button Text="{u:I18n Continue}"
StyleClass="btn-primary"
IsEnabled="{Binding CanContinue}"
Command="{Binding ContinueCommand}" />
<Label FormattedText="{Binding CreateAccountText}"
Margin="0, 10"
StyleClass="box-footer-label">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ResourceDictionary>
</ContentPage.Resources>

View file

@ -10,28 +10,35 @@ namespace Bit.App.Pages
{
public partial class HomePage : BaseContentPage
{
private bool _checkRememberedEmail;
private readonly HomeViewModel _vm;
private readonly AppOptions _appOptions;
private IBroadcasterService _broadcasterService;
public HomePage(AppOptions appOptions = null)
public HomePage(AppOptions appOptions = null, bool checkRememberedEmail = true)
{
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as HomeViewModel;
_vm.Page = this;
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
_vm.CheckHasRememberedEmail = checkRememberedEmail;
_vm.ShowCancelButton = _appOptions?.IosExtension ?? false;
_vm.StartLoginAction = async () => await StartLoginAsync();
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
_vm.CloseAction = async () =>
{
await _accountListOverlay.HideAsync();
await Navigation.PopModalAsync();
};
UpdateLogo();
if (_appOptions?.IosExtension ?? false)
if (!_vm.ShowCancelButton)
{
_vm.ShowCancelButton = true;
ToolbarItems.Remove(_closeButton);
}
if (_appOptions?.HideAccountSwitcher ?? false)
{
ToolbarItems.Remove(_accountAvatar);
@ -64,6 +71,8 @@ namespace Bit.App.Pages
});
}
});
_vm.CheckNavigateLoginStep();
}
protected override bool OnBackButtonPressed()
@ -96,28 +105,12 @@ namespace Bit.App.Pages
}
}
private void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartLoginAction();
}
}
private async Task StartLoginAsync()
{
var page = new LoginPage(null, _appOptions);
var page = new LoginPage(_vm.Email, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Register_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartRegisterAction();
}
}
private async Task StartRegisterAsync()
{
var page = new RegisterPage(this);

View file

@ -1,8 +1,14 @@
using System;
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@ -12,19 +18,33 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private bool _showCancelButton;
private bool _rememberEmail;
private string _email;
private bool _isEmailEnabled;
private bool _canLogin;
private IPlatformUtilsService _platformUtilsService;
private ILogger _logger;
public HomeViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
var logger = ServiceContainer.Resolve<ILogger>("logger");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_logger = ServiceContainer.Resolve<ILogger>("logger");
PageTitle = AppResources.Bitwarden;
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, logger)
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
AllowActiveAccountSelection = true
};
RememberEmailCommand = new Command(() => RememberEmail = !RememberEmail);
ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false);
CreateAccountCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(StartRegisterAction),
onException: _logger.Exception, allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
onException: _logger.Exception, allowsMultipleExecutions: false);
InitAsync().FireAndForget();
}
public bool ShowCancelButton
@ -33,11 +53,92 @@ namespace Bit.App.Pages
set => SetProperty(ref _showCancelButton, value);
}
public bool RememberEmail
{
get => _rememberEmail;
set => SetProperty(ref _rememberEmail, value);
}
public string Email
{
get => _email;
set => SetProperty(ref _email, value,
additionalPropertyNames: new[] { nameof(CanContinue) });
}
public bool CanContinue => !string.IsNullOrEmpty(Email);
public bool CheckHasRememberedEmail { get; set; }
public FormattedString CreateAccountText
{
get
{
var fs = new FormattedString();
fs.Spans.Add(new Span
{
Text = $"{AppResources.NewAroundHere} "
});
fs.Spans.Add(new Span
{
Text = AppResources.CreateAccount,
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
});
return fs;
}
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Action StartLoginAction { get; set; }
public Action StartRegisterAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action StartEnvironmentAction { get; set; }
public Action CloseAction { get; set; }
public Command RememberEmailCommand { get; set; }
public AsyncCommand ContinueCommand { get; }
public AsyncCommand CloseCommand { get; }
public AsyncCommand CreateAccountCommand { get; }
public async Task InitAsync()
{
Email = await _stateService.GetRememberedEmailAsync();
RememberEmail = !string.IsNullOrEmpty(Email);
}
public void CheckNavigateLoginStep()
{
if (CheckHasRememberedEmail && RememberEmail)
{
StartLoginAction();
}
CheckHasRememberedEmail = false;
}
public async Task ContinueToLoginStepAsync()
{
try
{
if (string.IsNullOrWhiteSpace(Email))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
AppResources.AnErrorHasOccurred, AppResources.Ok);
return;
}
if (!Email.Contains("@"))
{
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _stateService.SetRememberedEmailAsync(RememberEmail ? Email : null);
StartLoginAction();
}
catch (Exception ex)
{
_logger.Exception(ex);
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
}
}
}
}

View file

@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPageViewModel"
@ -46,33 +47,13 @@
Order="Secondary" />
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
<StackLayout Spacing="20">
<StackLayout Spacing="0">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n EmailAddress}"
StyleClass="box-label" />
<Entry
x:Name="_email"
Text="{Binding Email}"
IsEnabled="{Binding IsEmailEnabled}"
Keyboard="Email"
StyleClass="box-value">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Entry>
</StackLayout>
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@ -81,6 +62,7 @@
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
Padding="0, 10, 0, 0"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
@ -98,21 +80,60 @@
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Row="1"
Grid.Column="1"
Grid.RowSpan="2"
Grid.RowSpan="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
<Label
Text="{u:I18n GetMasterPasswordwordHint}"
StyleClass="box-footer-label"
TextColor="{DynamicResource HyperlinkColor}"
Padding="0,5,0,0"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Hint_Clicked" />
</Label.GestureRecognizers>
</Label>
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}"
<StackLayout Padding="10, 10">
<Button x:Name="_loginWithMasterPassword"
Text="{u:I18n LogInWithMasterPassword}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Clicked="Cancel_Clicked" />
<controls:IconLabelButton
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
Label="{u:I18n LogInWithAnotherDevice}"
ButtonCommand="{Binding LogInCommand}"
IsVisible="False"/>
<controls:IconLabelButton
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand"
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
Label="{u:I18n LogInSso}">
<controls:IconLabelButton.GestureRecognizers>
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
</controls:IconLabelButton.GestureRecognizers>
</controls:IconLabelButton>
<Label
Text="{Binding LoggingInAsText}"
StyleClass="text-sm"
Margin="0,40,0,0"/>
<Label
Text="{u:I18n NotYou}"
StyleClass="text-md"
HorizontalOptions="Start"
TextColor="{DynamicResource HyperlinkColor}">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="Cancel_Clicked" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</ScrollView>

View file

@ -4,6 +4,7 @@ using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -25,6 +26,7 @@ namespace Bit.App.Pages
_vm.Page = this;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.CloseAction = async () =>
@ -42,9 +44,6 @@ namespace Bit.App.Pages
_vm.Email = email;
MasterPasswordEntry = _masterPassword;
_email.ReturnType = ReturnType.Next;
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_moreItem);
@ -85,7 +84,7 @@ namespace Bit.App.Pages
await _vm.InitAsync();
if (!_inputFocused)
{
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
RequestFocus(_masterPassword);
_inputFocused = true;
}
}
@ -115,11 +114,25 @@ namespace Bit.App.Pages
}
}
private void LogInSSO_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartSsoLoginAction();
}
}
private async Task StartSsoLoginAsync()
{
var page = new LoginSsoPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Hint_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new HintPage()));
_vm.ShowMasterPasswordHintAsync().FireAndForget();
}
}

View file

@ -3,11 +3,13 @@ using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
@ -25,12 +27,14 @@ namespace Bit.App.Pages
private readonly II18nService _i18nService;
private readonly IMessagingService _messagingService;
private readonly ILogger _logger;
private readonly IApiService _apiService;
private readonly IAppIdService _appIdService;
private bool _showPassword;
private bool _showCancelButton;
private string _email;
private string _masterPassword;
private bool _isEmailEnabled;
private bool _isKnownDevice;
public LoginPageViewModel()
{
@ -43,6 +47,8 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_apiService = ServiceContainer.Resolve<IApiService>();
_appIdService = ServiceContainer.Resolve<IAppIdService>();
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
@ -76,7 +82,11 @@ namespace Bit.App.Pages
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
set => SetProperty(ref _email, value,
additionalPropertyNames: new string[]
{
nameof(LoggingInAsText)
});
}
public string MasterPassword
@ -91,6 +101,12 @@ namespace Bit.App.Pages
set => SetProperty(ref _isEmailEnabled, value);
}
public bool IsKnownDevice
{
get => _isKnownDevice;
set => SetProperty(ref _isKnownDevice, value);
}
public bool IsIosExtension { get; set; }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
@ -100,9 +116,11 @@ namespace Bit.App.Pages
public ICommand MoreCommand { get; internal set; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action CloseAction { get; set; }
protected override II18nService i18nService => _i18nService;
@ -112,10 +130,14 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
if (string.IsNullOrWhiteSpace(Email))
{
Email = await _stateService.GetRememberedEmailAsync();
}
var deviceIdentifier = await _appIdService.GetAppIdAsync();
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, deviceIdentifier);
await _deviceActionService.HideLoadingAsync();
}
public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
@ -170,7 +192,6 @@ namespace Bit.App.Pages
}
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
await _stateService.SetRememberedEmailAsync(Email);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (response.CaptchaNeeded)
@ -223,12 +244,7 @@ namespace Bit.App.Pages
if (selection == AppResources.GetPasswordHint)
{
var hintNavigationPage = new NavigationPage(new HintPage());
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
await ShowMasterPasswordHintAsync();
}
else if (selection == AppResources.RemoveAccount)
{
@ -236,6 +252,16 @@ namespace Bit.App.Pages
}
}
public async Task ShowMasterPasswordHintAsync()
{
var hintNavigationPage = new NavigationPage(new HintPage(Email));
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;

View file

@ -2893,6 +2893,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Get master password hint.
/// </summary>
public static string GetMasterPasswordwordHint {
get {
return ResourceManager.GetString("GetMasterPasswordwordHint", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Get your master password hint.
/// </summary>
@ -3406,6 +3415,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Logging in as {0}.
/// </summary>
public static string LoggingInAsX {
get {
return ResourceManager.GetString("LoggingInAsX", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log In.
/// </summary>
@ -3434,10 +3452,11 @@ namespace Bit.App.Resources {
}
/// <summary>
/// Looks up a localized string similar to Login attempt from {0}. Do you want to switch to this account?.
/// Looks up a localized string similar to Login attempt from:
///{0}
///Do you want to switch to this account?.
/// </summary>
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount
{
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount {
get {
return ResourceManager.GetString("LoginAttemptFromXDoYouWantToSwitchToThisAccount", resourceCulture);
}
@ -3542,6 +3561,24 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Log In with another device.
/// </summary>
public static string LogInWithAnotherDevice {
get {
return ResourceManager.GetString("LogInWithAnotherDevice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log In with master password.
/// </summary>
public static string LogInWithMasterPassword {
get {
return ResourceManager.GetString("LogInWithMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Log out.
/// </summary>
@ -3947,6 +3984,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to New around here?.
/// </summary>
public static string NewAroundHere {
get {
return ResourceManager.GetString("NewAroundHere", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New custom field.
/// </summary>
@ -4181,6 +4227,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Not you?.
/// </summary>
public static string NotYou {
get {
return ResourceManager.GetString("NotYou", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This login does not have a username or password configured..
/// </summary>

View file

@ -2473,4 +2473,22 @@ select Add TOTP to store the key safely</value>
{0}
Do you want to switch to this account?</value>
</data>
<data name="NewAroundHere" xml:space="preserve">
<value>New around here?</value>
</data>
<data name="GetMasterPasswordwordHint" xml:space="preserve">
<value>Get master password hint</value>
</data>
<data name="LoggingInAsX" xml:space="preserve">
<value>Logging in as {0}</value>
</data>
<data name="NotYou" xml:space="preserve">
<value>Not you?</value>
</data>
<data name="LogInWithMasterPassword" xml:space="preserve">
<value>Log In with master password</value>
</data>
<data name="LogInWithAnotherDevice" xml:space="preserve">
<value>Log In with another device</value>
</data>
</root>

View file

@ -80,6 +80,7 @@
Value="{DynamicResource StepperForegroundColor}" />
</Style>
<Style TargetType="Frame"
ApplyToDerivedTypes="True"
Class="btn-icon-row">
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColor}" />

View file

@ -131,6 +131,35 @@
Value="{DynamicResource FabColor}" />
</Style>
<Style TargetType="controls:IconLabelButton"
ApplyToDerivedTypes="True"
Class="btn-icon-secondary">
<Setter Property="IconLabelColor"
Value="{DynamicResource ButtonTextColorOpacity}" />
<Setter Property="IconLabelBorderColor"
Value="{DynamicResource ButtonBorderColor}" />
<Setter Property="IconLabelBackgroundColor"
Value="{DynamicResource BackgroundColor}" />
<Setter Property="CornerRadius"
Value="5" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="IconLabelColor"
Value="{DynamicResource ButtonTextColorDisabled}" />
<Setter Property="IconLabelBackgroundColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
<Setter Property="IconLabelBorderColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- Title -->
<Style TargetType="Button"
Class="btn-title"
@ -518,4 +547,19 @@
<Setter Property="Radius"
Value="15" />
</Style>
<Style TargetType="Entry" Class="entry-visual-disabled">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{DynamicResource MutedColor}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>

View file

@ -93,6 +93,7 @@
Value="{DynamicResource StepperForegroundColor}" />
</Style>
<Style TargetType="Frame"
ApplyToDerivedTypes="True"
Class="btn-icon-row">
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColor}" />

View file

@ -86,5 +86,6 @@ namespace Bit.Core.Abstractions
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
}
}

View file

@ -112,5 +112,7 @@
public const string File = "\xe96e";
public const string Paste = "\xe96f";
public const string ViewCellMenu = "\xe5d3";
public const string Device = "\xe986";
public const string Suitcase = "\xe98c";
}
}

View file

@ -547,6 +547,11 @@ namespace Bit.Core.Services
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Put, $"/auth-requests/{id}", request, true, true);
}
public Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier)
{
return SendAsync<object, bool>(HttpMethod.Get, $"/devices/knowndevice/{email}/{deviceIdentifier}", null, false, true);
}
#endregion
#region Helpers

View file

@ -68,7 +68,6 @@ namespace Bit.Core.Services
_state.ActiveUserId = userId;
// Update pre-auth settings based on now-active user
await SetRememberedEmailAsync(await GetEmailAsync());
await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync());
await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync());
}

View file

@ -425,15 +425,16 @@ namespace Bit.iOS.Autofill
}
}
private void LaunchHomePage()
private void LaunchHomePage(bool checkRememberedEmail = true)
{
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var homePage = new HomePage(appOptions, checkRememberedEmail: checkRememberedEmail);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
@ -456,8 +457,8 @@ namespace Bit.iOS.Autofill
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(environmentPage);
@ -475,7 +476,7 @@ namespace Bit.iOS.Autofill
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(registerPage);
@ -495,8 +496,9 @@ namespace Bit.iOS.Autofill
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(loginPage);

View file

@ -446,15 +446,16 @@ namespace Bit.iOS.Extension
});
}
private void LaunchHomePage()
private void LaunchHomePage(bool checkRememberedEmail = true)
{
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var homePage = new HomePage(appOptions, checkRememberedEmail: checkRememberedEmail);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
@ -477,8 +478,8 @@ namespace Bit.iOS.Extension
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(environmentPage);
@ -496,7 +497,7 @@ namespace Bit.iOS.Extension
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(registerPage);
@ -516,8 +517,9 @@ namespace Bit.iOS.Extension
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => CompleteRequest(null, null);
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(checkRememberedEmail: false));
}
var navigationPage = new NavigationPage(loginPage);

View file

@ -287,13 +287,13 @@ namespace Bit.iOS.ShareExtension
return _app;
}
private void LaunchHomePage()
private void LaunchHomePage(bool checkRememberedEmail = true)
{
var homePage = new HomePage();
var homePage = new HomePage(_appOptions.Value, checkRememberedEmail: checkRememberedEmail);
SetupAppAndApplyResources(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow());
vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow());
@ -311,8 +311,8 @@ namespace Bit.iOS.ShareExtension
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
}
NavigateToPage(environmentPage);
@ -325,7 +325,7 @@ namespace Bit.iOS.ShareExtension
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
}
NavigateToPage(registerPage);
}
@ -338,11 +338,12 @@ namespace Bit.iOS.ShareExtension
{
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
vm.LogInSuccessAction = () =>
{
DismissLockAndContinue();
};
vm.CloseAction = () => CompleteRequest();
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(checkRememberedEmail: false));
}
NavigateToPage(loginPage);
@ -426,7 +427,7 @@ namespace Bit.iOS.ShareExtension
switch (navTarget)
{
case NavigationTarget.HomeLogin:
ExecuteLaunch(LaunchHomePage);
ExecuteLaunch(() => LaunchHomePage());
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)

Binary file not shown.