Merge pull request #52 from github/haacked/two-factor-improvement

Two factor dialog improvements
This commit is contained in:
Andreia Gaita 2015-03-02 11:10:12 -08:00
Родитель 5de7a9e3d4 d74db93d66
Коммит e3364936e3
17 изменённых файлов: 423 добавлений и 175 удалений

Просмотреть файл

@ -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>