зеркало из https://github.com/github/VisualStudio.git
Merge pull request #52 from github/haacked/two-factor-improvement
Two factor dialog improvements
This commit is contained in:
Коммит
e3364936e3
|
@ -4,7 +4,7 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:DesignTimeStyleHelper"
|
||||
|
||||
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="350" Width="525">
|
||||
|
||||
|
@ -31,5 +31,9 @@
|
|||
<StackPanel x:Name="container">
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<Border Margin="20">
|
||||
<ui:TwoFactorInput />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using GitHub.VisualStudio.TeamExplorerConnect;
|
||||
using GitHub.VisualStudio;
|
||||
using GitHub.Services;
|
||||
using GitHub.UI;
|
||||
using System.ComponentModel.Composition;
|
||||
using GitHub.VisualStudio;
|
||||
|
||||
namespace DesignTimeStyleHelper
|
||||
{
|
||||
|
@ -29,7 +27,6 @@ namespace DesignTimeStyleHelper
|
|||
creation.Subscribe(_ => { }, _ => x.Close());
|
||||
x.Show();
|
||||
d.Value.Start();
|
||||
|
||||
}
|
||||
|
||||
private void createLink_Click(object sender, RoutedEventArgs e)
|
||||
|
@ -45,5 +42,4 @@ namespace DesignTimeStyleHelper
|
|||
d.Value.Start();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,25 +11,20 @@ namespace GitHub.Authentication
|
|||
public class TwoFactorChallengeHandler : ITwoFactorChallengeHandler
|
||||
{
|
||||
//readonly IServiceProvider serviceProvider;
|
||||
readonly Lazy<ITwoFactorViewModel> lazyTwoFactorDialog;
|
||||
readonly Lazy<ITwoFactorDialogViewModel> lazyTwoFactorDialog;
|
||||
|
||||
[ImportingConstructor]
|
||||
public TwoFactorChallengeHandler(Lazy<ITwoFactorViewModel> twoFactorDialog)
|
||||
public TwoFactorChallengeHandler(Lazy<ITwoFactorDialogViewModel> twoFactorDialog)
|
||||
{
|
||||
//this.serviceProvider = serviceProvider;
|
||||
this.lazyTwoFactorDialog = twoFactorDialog;
|
||||
lazyTwoFactorDialog = twoFactorDialog;
|
||||
}
|
||||
|
||||
public IObservable<TwoFactorChallengeResult> HandleTwoFactorException(TwoFactorRequiredException exception)
|
||||
{
|
||||
var twoFactorDialog = lazyTwoFactorDialog.Value as TwoFactorDialogViewModel;
|
||||
//var twoFactorView = (IViewFor<TwoFactorDialogViewModel>)serviceProvider.GetService(typeof(IViewFor<TwoFactorDialogViewModel>));
|
||||
var twoFactorDialog = lazyTwoFactorDialog.Value as ITwoFactorDialogViewModel;
|
||||
|
||||
return Observable.Start(() =>
|
||||
{
|
||||
//twoFactorView.ViewModel = twoFactorDialog;
|
||||
//((DialogWindow)twoFactorView).Show();
|
||||
|
||||
var userError = new TwoFactorRequiredUserError(exception);
|
||||
return twoFactorDialog.Show(userError)
|
||||
.SelectMany(x =>
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Windows.Controls;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Exports;
|
||||
using GitHub.Models;
|
||||
|
@ -12,7 +13,6 @@ using GitHub.UI;
|
|||
using GitHub.ViewModels;
|
||||
using ReactiveUI;
|
||||
using Stateless;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace GitHub.Controllers
|
||||
{
|
||||
|
@ -24,14 +24,13 @@ namespace GitHub.Controllers
|
|||
readonly ExportFactoryProvider factory;
|
||||
readonly IUIProvider uiProvider;
|
||||
|
||||
CompositeDisposable disposables = new CompositeDisposable();
|
||||
readonly CompositeDisposable disposables = new CompositeDisposable();
|
||||
readonly StateMachine<UIViewType, Trigger> machine;
|
||||
Subject<UserControl> transition;
|
||||
UIControllerFlow currentFlow;
|
||||
StateMachine<UIViewType, Trigger> machine;
|
||||
|
||||
[ImportingConstructor]
|
||||
public UIController(IUIProvider uiProvider, IRepositoryHosts hosts,
|
||||
ExportFactoryProvider factory)
|
||||
public UIController(IUIProvider uiProvider, IRepositoryHosts hosts, ExportFactoryProvider factory)
|
||||
{
|
||||
this.factory = factory;
|
||||
this.uiProvider = uiProvider;
|
||||
|
@ -119,7 +118,9 @@ namespace GitHub.Controllers
|
|||
{
|
||||
IViewModel viewModel;
|
||||
if (viewType == UIViewType.TwoFactor)
|
||||
{
|
||||
viewModel = uiProvider.GetService<ITwoFactorViewModel>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var dvm = factory.GetViewModel(viewType);
|
||||
|
|
|
@ -160,6 +160,7 @@
|
|||
<Compile Include="ViewModels\ILoginControlViewModel.cs" />
|
||||
<Compile Include="ViewModels\ILoginToGitHubForEnterpriseViewModel.cs" />
|
||||
<Compile Include="ViewModels\ILoginToGitHubViewModel.cs" />
|
||||
<Compile Include="ViewModels\ITwoFactorDialogViewModel.cs" />
|
||||
<Compile Include="ViewModels\LoginControlViewModel.cs" />
|
||||
<Compile Include="ViewModels\LoginTabViewModel.cs" />
|
||||
<Compile Include="ViewModels\LoginToGitHubForEnterpriseViewModel.cs" />
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Validation;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels
|
||||
{
|
||||
public interface ITwoFactorDialogViewModel : ITwoFactorViewModel
|
||||
{
|
||||
ReactiveCommand<object> OkCommand { get; }
|
||||
ReactiveCommand<RecoveryOptionResult> ShowHelpCommand { get; }
|
||||
ReactiveCommand<RecoveryOptionResult> ResendCodeCommand { get; }
|
||||
|
||||
IObservable<RecoveryOptionResult> Show(TwoFactorRequiredUserError error);
|
||||
|
||||
bool IsSms { get; }
|
||||
bool IsAuthenticationCodeSent { get; }
|
||||
string Description { get; }
|
||||
string AuthenticationCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validator instance used for validating the
|
||||
/// <see cref="AuthenticationCode"/> property
|
||||
/// </summary>
|
||||
ReactivePropertyValidator AuthenticationCodeValidator { get; }
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Authentication;
|
||||
using GitHub.Exports;
|
||||
using GitHub.Services;
|
||||
using GitHub.Validation;
|
||||
using NullGuard;
|
||||
|
@ -14,8 +11,9 @@ using ReactiveUI;
|
|||
namespace GitHub.ViewModels
|
||||
{
|
||||
[Export(typeof(ITwoFactorViewModel))]
|
||||
[Export(typeof(ITwoFactorDialogViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.Shared)]
|
||||
public class TwoFactorDialogViewModel : ReactiveValidatableObject, ITwoFactorViewModel
|
||||
public class TwoFactorDialogViewModel : ReactiveObject, ITwoFactorDialogViewModel
|
||||
{
|
||||
bool isAuthenticationCodeSent;
|
||||
string authenticationCode;
|
||||
|
@ -25,15 +23,14 @@ namespace GitHub.ViewModels
|
|||
readonly ObservableAsPropertyHelper<bool> isSms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public TwoFactorDialogViewModel(IBrowser browser, IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
public TwoFactorDialogViewModel(IBrowser browser)
|
||||
{
|
||||
OkCommand = ReactiveCommand.Create(this.WhenAny(
|
||||
x => x.IsValid,
|
||||
x => x.AuthenticationCodeValidator.ValidationResult.IsValid,
|
||||
x => x.AuthenticationCode,
|
||||
(valid, y) => valid.Value && (String.IsNullOrEmpty(y.Value) || (y.Value != null && y.Value.Length == 6))));
|
||||
CancelCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
ShowHelpCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
//ShowHelpCommand.Subscribe(x => browser.OpenUrl(twoFactorHelpUri));
|
||||
//TODO: ShowHelpCommand.Subscribe(x => browser.OpenUrl(twoFactorHelpUri));
|
||||
ResendCodeCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
|
||||
|
||||
description = this.WhenAny(x => x.TwoFactorType, x => x.Value)
|
||||
|
@ -62,6 +59,10 @@ namespace GitHub.ViewModels
|
|||
isSms = this.WhenAny(x => x.TwoFactorType, x => x.Value)
|
||||
.Select(factorType => factorType == TwoFactorType.Sms)
|
||||
.ToProperty(this, x => x.IsSms);
|
||||
|
||||
AuthenticationCodeValidator = ReactivePropertyValidator.For(this, x => x.AuthenticationCode)
|
||||
.IfNullOrEmpty("Please enter your authentication code")
|
||||
.IfNotMatch(@"^\d{6}$", "Authentication code must be exactly six digits");
|
||||
}
|
||||
|
||||
public TwoFactorType TwoFactorType
|
||||
|
@ -92,8 +93,6 @@ namespace GitHub.ViewModels
|
|||
get { return description.Value; }
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "Please enter your authentication code")]
|
||||
[RegularExpression(@"\d+", ErrorMessage = "Authentication code must only contain numbers")]
|
||||
[AllowNull]
|
||||
public string AuthenticationCode
|
||||
{
|
||||
|
@ -103,31 +102,25 @@ namespace GitHub.ViewModels
|
|||
}
|
||||
|
||||
public ReactiveCommand<object> OkCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> CancelCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> ShowHelpCommand { get; private set; }
|
||||
public ReactiveCommand<RecoveryOptionResult> ResendCodeCommand { get; private set; }
|
||||
|
||||
public ICommand OkCmd { get { return OkCommand; } }
|
||||
public ICommand CancelCmd { get { return CancelCommand; } }
|
||||
public ICommand ShowHelpCmd { get { return ShowHelpCommand; } }
|
||||
public ICommand ResendCodeCmd { get { return ResendCodeCommand; } }
|
||||
public ReactivePropertyValidator AuthenticationCodeValidator { get; private set; }
|
||||
|
||||
public IObservable<RecoveryOptionResult> Show(TwoFactorRequiredUserError error)
|
||||
{
|
||||
TwoFactorType = error.TwoFactorType;
|
||||
var ok = OkCommand
|
||||
.Where(x => Validate())
|
||||
.Select(_ => AuthenticationCode == null
|
||||
? RecoveryOptionResult.CancelOperation
|
||||
: RecoveryOptionResult.RetryOperation)
|
||||
.Do(_ => error.ChallengeResult = AuthenticationCode != null
|
||||
? new TwoFactorChallengeResult(AuthenticationCode)
|
||||
: null);
|
||||
var cancel = CancelCommand.Select(_ => RecoveryOptionResult.CancelOperation);
|
||||
var resend = ResendCodeCommand.Select(_ => RecoveryOptionResult.RetryOperation)
|
||||
.Do(_ => error.ChallengeResult = TwoFactorChallengeResult.RequestResendCode);
|
||||
|
||||
return Observable.Merge(ok, cancel, resend)
|
||||
return Observable.Merge(ok, resend)
|
||||
.Take(1)
|
||||
.Do(_ =>
|
||||
{
|
||||
|
|
|
@ -17,6 +17,4 @@ namespace GitHub.UI
|
|||
Create = 2,
|
||||
Clone = 3
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,18 +1,7 @@
|
|||
using System.Windows.Input;
|
||||
|
||||
namespace GitHub.ViewModels
|
||||
namespace GitHub.ViewModels
|
||||
{
|
||||
public interface ITwoFactorViewModel : IViewModel
|
||||
{
|
||||
ICommand OkCmd { get; }
|
||||
ICommand CancelCmd { get; }
|
||||
ICommand ShowHelpCmd { get; }
|
||||
ICommand ResendCodeCmd { get; }
|
||||
|
||||
bool IsShowing { get; }
|
||||
bool IsSms { get; }
|
||||
bool IsAuthenticationCodeSent { get; }
|
||||
string Description { get; }
|
||||
string AuthenticationCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<UserControl
|
||||
x:Class="GitHub.UI.TwoFactorInput"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
Width="262"
|
||||
d:DesignHeight="47"
|
||||
d:DesignWidth="262">
|
||||
<UserControl.Resources>
|
||||
<Style TargetType="{x:Type TextBox}">
|
||||
<Setter Property="Width" Value="37"/>
|
||||
<Setter Property="Height" Value="47"/>
|
||||
<Setter Property="Margin" Value="0,0,8,0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="20px" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox x:Name="one" />
|
||||
<TextBox x:Name="two" />
|
||||
<TextBox x:Name="three" />
|
||||
<TextBox x:Name="four" />
|
||||
<TextBox x:Name="five" />
|
||||
<TextBox x:Name="six" Margin="0" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using NullGuard;
|
||||
|
||||
namespace GitHub.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for TwoFactorInput.xaml
|
||||
/// </summary>
|
||||
public partial class TwoFactorInput : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register("Text", typeof(string), typeof(TwoFactorInput), new PropertyMetadata(""));
|
||||
|
||||
TextBox[] TextBoxes;
|
||||
|
||||
public TwoFactorInput()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
TextBoxes = new[]
|
||||
{
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
five,
|
||||
six
|
||||
};
|
||||
|
||||
foreach(var textBox in TextBoxes)
|
||||
{
|
||||
SetupTextBox(textBox);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPaste(object sender, DataObjectPastingEventArgs e)
|
||||
{
|
||||
var isText = e.SourceDataObject.GetDataPresent(DataFormats.Text, true);
|
||||
if (!isText) return;
|
||||
|
||||
var text = e.SourceDataObject.GetData(DataFormats.Text) as string;
|
||||
if (text == null) return;
|
||||
e.CancelCommand();
|
||||
SetText(text);
|
||||
}
|
||||
|
||||
void SetText(string text)
|
||||
{
|
||||
if (String.IsNullOrEmpty(text))
|
||||
{
|
||||
foreach (var textBox in TextBoxes)
|
||||
{
|
||||
textBox.Text = "";
|
||||
}
|
||||
SetValue(TextProperty, text);
|
||||
return;
|
||||
}
|
||||
var digits = text.Where(Char.IsDigit).ToList();
|
||||
for (int i = 0; i < Math.Min(6, digits.Count); i++)
|
||||
{
|
||||
TextBoxes[i].Text = digits[i].ToString();
|
||||
}
|
||||
SetValue(TextProperty, String.Join("", digits));
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get { return (string)GetValue(TextProperty); }
|
||||
set { SetText(value); }
|
||||
}
|
||||
|
||||
private void SetupTextBox(TextBox textBox)
|
||||
{
|
||||
DataObject.AddPastingHandler(textBox, new DataObjectPastingEventHandler(OnPaste));
|
||||
|
||||
textBox.GotFocus += (sender, args) => textBox.SelectAll();
|
||||
|
||||
textBox.PreviewKeyDown += (sender, args) =>
|
||||
{
|
||||
if (args.Key != Key.D0
|
||||
&& args.Key != Key.D1
|
||||
&& args.Key != Key.D2
|
||||
&& args.Key != Key.D3
|
||||
&& args.Key != Key.D4
|
||||
&& args.Key != Key.D5
|
||||
&& args.Key != Key.D6
|
||||
&& args.Key != Key.D7
|
||||
&& args.Key != Key.D8
|
||||
&& args.Key != Key.D9
|
||||
&& args.Key != Key.Tab
|
||||
&& args.Key != Key.Escape
|
||||
&& (!(args.Key == Key.V && args.KeyboardDevice.Modifiers == ModifierKeys.Control))
|
||||
&& (!(args.Key == Key.Insert && args.KeyboardDevice.Modifiers == ModifierKeys.Shift)))
|
||||
{
|
||||
args.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
textBox.SelectionChanged += (sender, args) =>
|
||||
{
|
||||
// Make sure we can't insert additional text into a textbox.
|
||||
// Each textbox should only allow one character.
|
||||
if (textBox.SelectionLength == 0 && textBox.Text.Any())
|
||||
{
|
||||
textBox.SelectAll();
|
||||
}
|
||||
};
|
||||
|
||||
textBox.TextChanged += (sender, args) =>
|
||||
{
|
||||
var tRequest = new TraversalRequest(FocusNavigationDirection.Next);
|
||||
var keyboardFocus = Keyboard.FocusedElement as UIElement;
|
||||
|
||||
SetValue(TextProperty, String.Join("", GetTwoFactorCode()));
|
||||
|
||||
if (keyboardFocus != null)
|
||||
{
|
||||
keyboardFocus.MoveFocus(tRequest);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetTextBoxValue(TextBox textBox)
|
||||
{
|
||||
return String.IsNullOrEmpty(textBox.Text) ? " " : textBox.Text;
|
||||
}
|
||||
|
||||
private string GetTwoFactorCode()
|
||||
{
|
||||
return String.Join("", TextBoxes.Select(textBox => textBox.Text));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -70,6 +70,9 @@
|
|||
<Compile Include="Controls\Octicons\OcticonPath.cs" />
|
||||
<Compile Include="Controls\Octicons\OcticonPaths.Designer.cs" />
|
||||
<Compile Include="Controls\TrimmedTextBlock.cs" />
|
||||
<Compile Include="Controls\TwoFactorInput.xaml.cs">
|
||||
<DependentUpon>TwoFactorInput.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Fakes\NullGuard.cs" />
|
||||
<Compile Include="Helpers\AccessKeysManagerScoping.cs" />
|
||||
<Compile Include="Controls\Buttons\OcticonButton.cs" />
|
||||
|
@ -157,6 +160,10 @@
|
|||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Controls\TwoFactorInput.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="SharedDictionary.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
|
|
|
@ -6,15 +6,14 @@
|
|||
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI"
|
||||
xmlns:uirx="clr-namespace:GitHub.UI;assembly=GitHub.UI.Reactive"
|
||||
xmlns:helpers="clr-namespace:GitHub.Helpers;assembly=GitHub.UI"
|
||||
mc:Ignorable="d"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="426"
|
||||
d:DesignHeight="517"
|
||||
MinWidth="426"
|
||||
MinHeight="517"
|
||||
Background="White">
|
||||
<Grid FocusManager.IsFocusScope="True" x:Name="LoginStackPanel" FocusVisualStyle="{x:Null}">
|
||||
|
||||
<Grid.Resources>
|
||||
<Border FocusManager.IsFocusScope="True" x:Name="LoginStackPanel" FocusVisualStyle="{x:Null}">
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type ui:PromptTextBox}" BasedOn="{StaticResource RoundedPromptTextBox}">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
|
@ -22,8 +21,7 @@
|
|||
<Style TargetType="{x:Type ui:SecurePasswordBox}" BasedOn="{StaticResource RoundedPromptTextBox}">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
|
||||
</Border.Resources>
|
||||
<StackPanel
|
||||
Margin="40,0,40,0"
|
||||
Background="{x:Null}"
|
||||
|
@ -66,12 +64,12 @@
|
|||
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<ui:PromptTextBox x:Name="dotComUserNameOrEmail" PromptText="Username or email" />
|
||||
<uirx:ValidationMessage x:Name="dotComUserNameOrEmailValidationMessage" ValidatesControl="{Binding ElementName=dotComUserNameOrEmail}"/>
|
||||
<uirx:ValidationMessage x:Name="dotComUserNameOrEmailValidationMessage" ValidatesControl="{Binding ElementName=dotComUserNameOrEmail}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<ui:SecurePasswordBox x:Name="dotComPassword" PromptText="Password" />
|
||||
<uirx:ValidationMessage x:Name="dotComPasswordValidationMessage" ValidatesControl="{Binding ElementName=dotComPassword}"/>
|
||||
<uirx:ValidationMessage x:Name="dotComPasswordValidationMessage" ValidatesControl="{Binding ElementName=dotComPassword}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
@ -134,5 +132,5 @@
|
|||
</TabItem>
|
||||
</TabControl>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
|
@ -10,105 +10,73 @@
|
|||
xmlns:sampleData="clr-namespace:GitHub.SampleData;assembly=GitHub.App"
|
||||
xmlns:GitHub="clr-namespace:GitHub.VisualStudio.Helpers"
|
||||
xmlns:pfui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.12.0"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="24" />
|
||||
<RowDefinition Height="1.5*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Rectangle Fill="{DynamicResource GitHubModalBackgroundFill}" IsHitTestVisible="False" />
|
||||
<Rectangle Grid.Row="1" Fill="{DynamicResource GitHubModalBackgroundFill}" />
|
||||
<Border Grid.Row="2" Background="{DynamicResource GitHubLightModalViewBackground}">
|
||||
<Grid Grid.Row="2" HorizontalAlignment="Center" MaxWidth="650" MinWidth="450">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ui:OcticonPath
|
||||
Stretch="Uniform"
|
||||
Fill="#90000000"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,71,0,0"
|
||||
Height="64"
|
||||
Icon="device_mobile"/>
|
||||
<StackPanel Grid.Column="1" HorizontalAlignment="Left" Margin="24,50,24,0">
|
||||
<WrapPanel Orientation="Horizontal" Margin="0,12,0,0" >
|
||||
<TextBlock
|
||||
Text="Two-factor authentication"
|
||||
Padding="0"
|
||||
Margin="0,0,12,0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
Foreground="{DynamicResource GitHubLightModalViewTextBrush}"/>
|
||||
<TextBlock
|
||||
Name="authenticationSentLabel"
|
||||
Text="authentication code sent!"
|
||||
Padding="0"
|
||||
Margin="0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
Foreground="{DynamicResource GitHubAccentBrush}" />
|
||||
</WrapPanel>
|
||||
<TextBlock
|
||||
Name="description"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,0,0,6"
|
||||
Text="Open the two-factor authentication app on your device to view your authentication code."
|
||||
Style="{DynamicResource GitHubDescriptionTextBlock}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="426"
|
||||
d:DesignHeight="517"
|
||||
MinWidth="426"
|
||||
MinHeight="517">
|
||||
<Border FocusManager.IsFocusScope="True" x:Name="LoginStackPanel" FocusVisualStyle="{x:Null}">
|
||||
<StackPanel
|
||||
Margin="40,0,40,0"
|
||||
Background="{x:Null}"
|
||||
FocusManager.IsFocusScope="True"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
helpers:AccessKeysManagerScoping.IsEnabled="True">
|
||||
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,12,0,12" >
|
||||
<TextBlock
|
||||
Text="Two-factor authentication"
|
||||
Padding="0"
|
||||
Margin="0,0,12,0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
Foreground="{DynamicResource GitHubLightModalViewTextBrush}"/>
|
||||
<ui:OcticonLinkButton
|
||||
x:Name="helpButton"
|
||||
Grid.Row="2"
|
||||
Margin="0,0,0,6"
|
||||
Icon="link_external"
|
||||
Foreground="{DynamicResource GitHubLightModalViewTextBrush}"
|
||||
ToolTip="Learn more about two-factor authentication on GitHub"
|
||||
Content="Read more" />
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="3">
|
||||
<ui:PromptTextBox
|
||||
Name="authenticationCode"
|
||||
MaxLength="6"
|
||||
TabIndex="1"
|
||||
PromptText="Authentication code"
|
||||
Margin="0"
|
||||
Style="{DynamicResource RoundedPromptTextBox}"
|
||||
VerticalAlignment="Stretch"
|
||||
d:LayoutOverrides="Height"
|
||||
Text="{Binding AuthenticationCode, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
|
||||
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}"
|
||||
MinWidth="164"
|
||||
MaxWidth="164" />
|
||||
<ui:OcticonLinkButton
|
||||
x:Name="resendCodeButton"
|
||||
ToolTip="Send the code to your registered SMS Device again"
|
||||
FontSize="12"
|
||||
Icon="sync"
|
||||
Margin="4,0,0,0"
|
||||
Content="_Resend" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="4"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
Margin="0,24,24,60">
|
||||
<ui:OcticonCircleButton
|
||||
x:Name="okButton"
|
||||
TabIndex="2"
|
||||
Icon="check"
|
||||
IsDefault="True"
|
||||
Content="Log in"
|
||||
Margin="0" />
|
||||
<ui:OcticonCircleButton
|
||||
x:Name="cancelButton"
|
||||
IsCancel="True"
|
||||
Margin="12,0,0,0"
|
||||
Icon="x"
|
||||
Content="Cancel" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Name="authenticationSentLabel"
|
||||
Text="authentication code sent!"
|
||||
Padding="0"
|
||||
Margin="0"
|
||||
Style="{DynamicResource GitHubH1TextBlock}"
|
||||
Foreground="{DynamicResource GitHubAccentBrush}" />
|
||||
</WrapPanel>
|
||||
|
||||
<ui:HorizontalShadowDivider Margin="0,0,0,20" />
|
||||
|
||||
<TextBlock
|
||||
Name="description"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="8,0,8,16"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Style="{DynamicResource GitHubDescriptionTextBlock}"
|
||||
Foreground="{DynamicResource GitHubLightModalViewTextBrush}">
|
||||
Open the two-factor authentication app on your device to view your authentication code.
|
||||
<Hyperlink NavigateUri="https://help.github.com/articles/about-two-factor-authentication" ToolTip="https://help.github.com/articles/about-two-factor-authentication">Read more</Hyperlink>
|
||||
</TextBlock>
|
||||
<ui:TwoFactorInput
|
||||
x:Name="authenticationCode"
|
||||
TabIndex="1" />
|
||||
<uirx:ValidationMessage
|
||||
x:Name="authenticationCodeValidationMessage"
|
||||
ValidatesControl="{Binding ElementName=authenticationCode}" />
|
||||
<StackPanel Grid.Row="4"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,24,24,60">
|
||||
<ui:OcticonCircleButton
|
||||
x:Name="okButton"
|
||||
TabIndex="2"
|
||||
Icon="check"
|
||||
IsDefault="True"
|
||||
Content="Verify" />
|
||||
<ui:OcticonLinkButton
|
||||
x:Name="resendCodeButton"
|
||||
ToolTip="Send the code to your registered SMS Device again"
|
||||
FontSize="12"
|
||||
Icon="sync"
|
||||
Margin="18,0,0,0"
|
||||
Content="_Resend" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Rectangle Grid.Row="3" Fill="{DynamicResource GitHubModalBackgroundFill}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
|
@ -4,9 +4,9 @@ using System.Windows;
|
|||
using System.Windows.Input;
|
||||
using GitHub.Exports;
|
||||
using GitHub.UI;
|
||||
using GitHub.UI.Helpers;
|
||||
using GitHub.ViewModels;
|
||||
using ReactiveUI;
|
||||
using GitHub.UI.Helpers;
|
||||
|
||||
namespace GitHub.VisualStudio.UI.Views.Controls
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ namespace GitHub.VisualStudio.UI.Views.Controls
|
|||
/// Interaction logic for PasswordView.xaml
|
||||
/// </summary>
|
||||
[ExportView(ViewType=UIViewType.TwoFactor)]
|
||||
public partial class TwoFactorControl : IViewFor<ITwoFactorViewModel>, IView
|
||||
public partial class TwoFactorControl : IViewFor<ITwoFactorDialogViewModel>, IView
|
||||
{
|
||||
public TwoFactorControl()
|
||||
{
|
||||
|
@ -23,54 +23,55 @@ namespace GitHub.VisualStudio.UI.Views.Controls
|
|||
Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
|
||||
|
||||
InitializeComponent();
|
||||
DataContextChanged += (s, e) => ViewModel = (ITwoFactorViewModel)e.NewValue;
|
||||
//IsVisibleChanged += (s, e) => authenticationCode.EnsureFocus();
|
||||
DataContextChanged += (s, e) => ViewModel = (ITwoFactorDialogViewModel)e.NewValue;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
d(this.BindCommand(ViewModel, vm => vm.OkCmd, view => view.okButton));
|
||||
d(this.BindCommand(ViewModel, vm => vm.CancelCmd, view => view.cancelButton));
|
||||
d(this.BindCommand(ViewModel, vm => vm.ShowHelpCmd, view => view.helpButton));
|
||||
d(this.BindCommand(ViewModel, vm => vm.ResendCodeCmd, view => view.resendCodeButton));
|
||||
authenticationCode.Focus();
|
||||
|
||||
d(this.BindCommand(ViewModel, vm => vm.OkCommand, view => view.okButton));
|
||||
d(this.BindCommand(ViewModel, vm => vm.ResendCodeCommand, view => view.resendCodeButton));
|
||||
|
||||
d(this.Bind(ViewModel, vm => vm.AuthenticationCode, view => view.authenticationCode.Text));
|
||||
d(this.OneWayBind(ViewModel, vm => vm.AuthenticationCodeValidator, v => v.authenticationCodeValidationMessage.ReactiveValidator));
|
||||
|
||||
d(this.OneWayBind(ViewModel, vm => vm.IsAuthenticationCodeSent,
|
||||
view => view.authenticationSentLabel.Visibility));
|
||||
d(this.OneWayBind(ViewModel, vm => vm.IsSms, view => view.resendCodeButton.Visibility));
|
||||
d(this.OneWayBind(ViewModel, vm => vm.Description, view => view.description.Text));
|
||||
d(MessageBus.Current.Listen<KeyEventArgs>()
|
||||
.Where(x => ViewModel.IsShowing && x.Key == Key.Escape && !x.Handled)
|
||||
.Subscribe(async key =>
|
||||
.Subscribe(key =>
|
||||
{
|
||||
key.Handled = true;
|
||||
await ((ReactiveCommand<object>)ViewModel.CancelCmd).ExecuteAsync();
|
||||
//TODO: Hide this dialog.
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
public ITwoFactorViewModel ViewModel
|
||||
public ITwoFactorDialogViewModel ViewModel
|
||||
{
|
||||
get { return (ITwoFactorViewModel)GetValue(ViewModelProperty); }
|
||||
get { return (ITwoFactorDialogViewModel)GetValue(ViewModelProperty); }
|
||||
set { SetValue(ViewModelProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(
|
||||
"ViewModel",
|
||||
typeof(ITwoFactorViewModel),
|
||||
typeof(ITwoFactorDialogViewModel),
|
||||
typeof(TwoFactorControl),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
object IViewFor.ViewModel
|
||||
{
|
||||
get { return ViewModel; }
|
||||
set { ViewModel = (ITwoFactorViewModel)value; }
|
||||
set { ViewModel = (ITwoFactorDialogViewModel)value; }
|
||||
}
|
||||
|
||||
object IView.ViewModel
|
||||
{
|
||||
get { return ViewModel; }
|
||||
set { ViewModel = (ITwoFactorViewModel)value; }
|
||||
set { ViewModel = (ITwoFactorDialogViewModel)value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using GitHub.UI;
|
||||
using Xunit;
|
||||
using Xunit.Extensions;
|
||||
|
||||
public class TwoFactorInputTests
|
||||
{
|
||||
public class TheTextProperty
|
||||
{
|
||||
[Fact]
|
||||
public void SetsTextBoxesToIndividualCharacters()
|
||||
{
|
||||
var twoFactorInput = new TwoFactorInput();
|
||||
var textBoxes = GetChildrenRecursive(twoFactorInput).OfType<TextBox>().ToList();
|
||||
|
||||
twoFactorInput.Text = "012345";
|
||||
|
||||
Assert.Equal("012345", twoFactorInput.Text);
|
||||
Assert.Equal("0", textBoxes[0].Text);
|
||||
Assert.Equal("1", textBoxes[1].Text);
|
||||
Assert.Equal("2", textBoxes[2].Text);
|
||||
Assert.Equal("3", textBoxes[3].Text);
|
||||
Assert.Equal("4", textBoxes[4].Text);
|
||||
Assert.Equal("5", textBoxes[5].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresNonDigitCharacters()
|
||||
{
|
||||
var twoFactorInput = new TwoFactorInput();
|
||||
var textBoxes = GetChildrenRecursive(twoFactorInput).OfType<TextBox>().ToList();
|
||||
|
||||
twoFactorInput.Text = "01xyz2345";
|
||||
|
||||
Assert.Equal("012345", twoFactorInput.Text);
|
||||
Assert.Equal("0", textBoxes[0].Text);
|
||||
Assert.Equal("1", textBoxes[1].Text);
|
||||
Assert.Equal("2", textBoxes[2].Text);
|
||||
Assert.Equal("3", textBoxes[3].Text);
|
||||
Assert.Equal("4", textBoxes[4].Text);
|
||||
Assert.Equal("5", textBoxes[5].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNotEnoughCharacters()
|
||||
{
|
||||
var twoFactorInput = new TwoFactorInput();
|
||||
var textBoxes = GetChildrenRecursive(twoFactorInput).OfType<TextBox>().ToList();
|
||||
|
||||
twoFactorInput.Text = "012";
|
||||
|
||||
Assert.Equal("012", twoFactorInput.Text);
|
||||
Assert.Equal("0", textBoxes[0].Text);
|
||||
Assert.Equal("1", textBoxes[1].Text);
|
||||
Assert.Equal("2", textBoxes[2].Text);
|
||||
Assert.Equal("", textBoxes[3].Text);
|
||||
Assert.Equal("", textBoxes[4].Text);
|
||||
Assert.Equal("", textBoxes[5].Text);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", "")]
|
||||
[InlineData("xxxx", "")]
|
||||
public void HandlesNullAndStringsWithNoDigits(string input, string expected)
|
||||
{
|
||||
var twoFactorInput = new TwoFactorInput();
|
||||
var textBoxes = GetChildrenRecursive(twoFactorInput).OfType<TextBox>().ToList();
|
||||
|
||||
twoFactorInput.Text = input;
|
||||
|
||||
Assert.Equal(expected, twoFactorInput.Text);
|
||||
Assert.Equal("", textBoxes[0].Text);
|
||||
Assert.Equal("", textBoxes[1].Text);
|
||||
Assert.Equal("", textBoxes[2].Text);
|
||||
Assert.Equal("", textBoxes[3].Text);
|
||||
Assert.Equal("", textBoxes[4].Text);
|
||||
Assert.Equal("", textBoxes[5].Text);
|
||||
}
|
||||
|
||||
static IEnumerable<FrameworkElement> GetChildrenRecursive(FrameworkElement element)
|
||||
{
|
||||
yield return element;
|
||||
foreach (var child in LogicalTreeHelper.GetChildren(element)
|
||||
.Cast<FrameworkElement>()
|
||||
.SelectMany(GetChildrenRecursive))
|
||||
{
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,6 +58,8 @@
|
|||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\NSubstitute.1.8.1.0\lib\net45\NSubstitute.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="ReactiveUI, Version=6.3.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\reactiveui-core.6.3.1\lib\Net45\ReactiveUI.dll</HintPath>
|
||||
|
@ -73,11 +75,13 @@
|
|||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="xunit">
|
||||
<HintPath>..\..\packages\xunit.1.9.2\lib\net20\xunit.dll</HintPath>
|
||||
</Reference>
|
||||
|
@ -88,6 +92,7 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="Args.cs" />
|
||||
<Compile Include="GitHub.App\ViewModels\LoginControlViewModelTests.cs" />
|
||||
<Compile Include="GitHub.UI\TwoFactorInputTests.cs" />
|
||||
<Compile Include="GitHubPackageTests.cs" />
|
||||
<Compile Include="Helpers\LazySubstitute.cs" />
|
||||
<Compile Include="TestDoubles\FakeMenuCommandService.cs" />
|
||||
|
@ -106,6 +111,10 @@
|
|||
<Project>{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}</Project>
|
||||
<Name>GitHub.UI.Reactive</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
|
||||
<Project>{346384dd-2445-4a28-af22-b45f3957bd89}</Project>
|
||||
<Name>GitHub.UI</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\GitHub.VisualStudio\GitHub.VisualStudio.csproj">
|
||||
<Project>{11569514-5ae5-4b5b-92a2-f10b0967de5f}</Project>
|
||||
<Name>GitHub.VisualStudio</Name>
|
||||
|
|
Загрузка…
Ссылка в новой задаче