Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 7429224

Browse files
committed
Implemented log out/back in flow.
1 parent a9f01eb commit 7429224

File tree

15 files changed

+227
-39
lines changed

15 files changed

+227
-39
lines changed

src/GitHub.Api/LoginManager.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ Uri GetLoginUrl(IOauthClient client, string state)
373373

374374
request.State = state;
375375

376-
foreach (var scope in minimumScopes)
376+
foreach (var scope in requestedScopes)
377377
{
378378
request.Scopes.Add(scope);
379379
}

src/GitHub.App/Resources.Designer.cs

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/GitHub.App/Resources.resx

+3
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,7 @@ https://git-scm.com/download/win</value>
330330
<data name="DestinationAlreadyExists" xml:space="preserve">
331331
<value>The destination already exists.</value>
332332
</data>
333+
<data name="LogoutRequired" xml:space="preserve">
334+
<value>Logout Required</value>
335+
</data>
333336
</root>

src/GitHub.App/Services/DialogService.cs

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.ComponentModel.Composition;
33
using System.Threading.Tasks;
4+
using GitHub.Api;
45
using GitHub.Extensions;
56
using GitHub.Factories;
67
using GitHub.Models;
@@ -30,17 +31,15 @@ public DialogService(
3031

3132
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection)
3233
{
34+
Guard.ArgumentNotNull(connection, nameof(connection));
35+
3336
var viewModel = factory.CreateViewModel<IRepositoryCloneViewModel>();
3437

35-
if (connection != null)
36-
{
37-
await viewModel.InitializeAsync(connection);
38-
return (CloneDialogResult)await showDialog.Show(viewModel);
39-
}
40-
else
41-
{
42-
return (CloneDialogResult)await showDialog.ShowWithFirstConnection(viewModel);
43-
}
38+
return (CloneDialogResult)await showDialog.Show(
39+
viewModel,
40+
connection,
41+
ApiClientConfiguration.RequestedScopes)
42+
.ConfigureAwait(false);
4443
}
4544

4645
public async Task<string> ShowReCloneDialog(IRepositoryModel repository)

src/GitHub.App/ViewModels/Dialog/GitHubDialogWindowViewModel.cs

+31-23
Original file line numberDiff line numberDiff line change
@@ -62,40 +62,48 @@ public void Start(IDialogContentViewModel viewModel)
6262
public async Task StartWithConnection<T>(T viewModel)
6363
where T : IDialogContentViewModel, IConnectionInitializedViewModel
6464
{
65-
var connections = await connectionManager.Value.GetLoadedConnections();
65+
var connections = await connectionManager.Value.GetLoadedConnections().ConfigureAwait(true);
6666
var connection = connections.FirstOrDefault(x => x.IsLoggedIn);
6767

6868
if (connection == null)
6969
{
70-
var login = CreateLoginViewModel();
71-
72-
subscription = login.Done.Take(1).Subscribe(async x =>
73-
{
74-
var newConnection = (IConnection)x;
75-
76-
if (newConnection != null)
77-
{
78-
await viewModel.InitializeAsync(newConnection);
79-
Start(viewModel);
80-
}
81-
else
82-
{
83-
done.OnNext(null);
84-
}
85-
});
86-
87-
Content = login;
70+
connection = await ShowLogin().ConfigureAwait(true);
8871
}
89-
else
72+
73+
if (connection != null)
9074
{
91-
await viewModel.InitializeAsync(connection);
75+
await viewModel.InitializeAsync(connection).ConfigureAwait(true);
9276
Start(viewModel);
9377
}
9478
}
9579

96-
ILoginViewModel CreateLoginViewModel()
80+
public async Task StartWithLogout<T>(T viewModel, IConnection connection)
81+
where T : IDialogContentViewModel, IConnectionInitializedViewModel
82+
{
83+
var logout = factory.CreateViewModel<ILogOutRequiredViewModel>();
84+
85+
subscription?.Dispose();
86+
subscription = logout.Done.Take(1).Subscribe(async _ =>
87+
{
88+
await connectionManager.Value.LogOut(connection.HostAddress).ConfigureAwait(true);
89+
90+
connection = await ShowLogin().ConfigureAwait(true);
91+
92+
if (connection != null)
93+
{
94+
await viewModel.InitializeAsync(connection).ConfigureAwait(true);
95+
Start(viewModel);
96+
}
97+
});
98+
99+
Content = logout;
100+
}
101+
102+
async Task<IConnection> ShowLogin()
97103
{
98-
return factory.CreateViewModel<ILoginViewModel>();
104+
var login = factory.CreateViewModel<ILoginViewModel>();
105+
Content = login;
106+
return (IConnection)await login.Done.Take(1);
99107
}
100108
}
101109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.ComponentModel.Composition;
3+
using GitHub.ViewModels;
4+
using GitHub.ViewModels.Dialog;
5+
using ReactiveUI;
6+
7+
namespace GitHub.App.ViewModels.Dialog
8+
{
9+
/// <summary>
10+
/// The "Logout required" dialog page.
11+
/// </summary>
12+
[Export(typeof(ILogOutRequiredViewModel))]
13+
[PartCreationPolicy(CreationPolicy.NonShared)]
14+
public class LogOutRequiredViewModel : ViewModelBase, ILogOutRequiredViewModel
15+
{
16+
/// <inheritdoc/>
17+
public ReactiveCommand<object> LogOut { get; } = ReactiveCommand.Create();
18+
19+
/// <inheritdoc/>
20+
public string Title => Resources.LogoutRequired;
21+
22+
/// <inheritdoc/>
23+
public IObservable<object> Done => LogOut;
24+
}
25+
}

src/GitHub.Exports.Reactive/Services/IShowDialogService.cs

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Threading.Tasks;
4+
using GitHub.Api;
5+
using GitHub.Models;
36
using GitHub.Primitives;
47
using GitHub.ViewModels;
58
using GitHub.ViewModels.Dialog;
@@ -27,6 +30,22 @@ public interface IShowDialogService
2730
/// </returns>
2831
Task<object> Show(IDialogContentViewModel viewModel);
2932

33+
/// <summary>
34+
/// Shows a view model that requires a connection with specifiec scopes in the dialog.
35+
/// </summary>
36+
/// <param name="viewModel">The view model to show.</param>
37+
/// <param name="connection">The connection.</param>
38+
/// <param name="scopes">The required scopes.</param>
39+
/// <returns>
40+
/// If the connection does not have the requested scopes, the user will be invited to log
41+
/// out and back in.
42+
/// </returns>
43+
Task<object> Show<TViewModel>(
44+
TViewModel viewModel,
45+
IConnection connection,
46+
IEnumerable<string> scopes)
47+
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel;
48+
3049
/// <summary>
3150
/// Shows a view model that requires a connection in the dialog.
3251
/// </summary>

src/GitHub.Exports.Reactive/ViewModels/Dialog/IGitHubDialogWindowViewModel.cs

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3+
using GitHub.Models;
34

45
namespace GitHub.ViewModels.Dialog
56
{
@@ -33,5 +34,13 @@ public interface IGitHubDialogWindowViewModel : IDisposable
3334
/// <param name="viewModel">The view model to display.</param>
3435
Task StartWithConnection<T>(T viewModel)
3536
where T : IDialogContentViewModel, IConnectionInitializedViewModel;
37+
38+
/// <summary>
39+
/// Starts displaying a view model that requires a connection that needs to be logged out
40+
/// and back in.
41+
/// </summary>
42+
/// <param name="viewModel">The view model to display.</param>
43+
Task StartWithLogout<T>(T viewModel, IConnection connection)
44+
where T : IDialogContentViewModel, IConnectionInitializedViewModel;
3645
}
3746
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using ReactiveUI;
3+
4+
namespace GitHub.ViewModels.Dialog
5+
{
6+
/// <summary>
7+
/// Represents the "Logout required" dialog page.
8+
/// </summary>
9+
public interface ILogOutRequiredViewModel : IDialogContentViewModel
10+
{
11+
/// <summary>
12+
/// Gets a command that will log out the user.
13+
/// </summary>
14+
ReactiveCommand<object> LogOut { get; }
15+
}
16+
}

src/GitHub.Exports/Models/ScopesCollection.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public ScopesCollection(IReadOnlyList<string> scopes)
3838
/// </summary>
3939
/// <param name="required">The required API scopes.</param>
4040
/// <returns>True if all required scopes are present, otherwise false.</returns>
41-
public bool Matches(IReadOnlyList<string> required)
41+
public bool Matches(IEnumerable<string> required)
4242
{
4343
foreach (var scope in required)
4444
{

src/GitHub.VisualStudio/GitHub.VisualStudio.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,9 @@
408408
<Compile Include="Views\Dialog\Login2FaView.xaml.cs">
409409
<DependentUpon>Login2FaView.xaml</DependentUpon>
410410
</Compile>
411+
<Compile Include="Views\Dialog\LogOutRequiredView.xaml.cs">
412+
<DependentUpon>LogOutRequiredView.xaml</DependentUpon>
413+
</Compile>
411414
<Compile Include="Views\Dialog\RepositoryCreationView.xaml.cs">
412415
<DependentUpon>RepositoryCreationView.xaml</DependentUpon>
413416
</Compile>
@@ -597,6 +600,9 @@
597600
<Generator>MSBuild:Compile</Generator>
598601
<SubType>Designer</SubType>
599602
</Page>
603+
<Page Include="Views\Dialog\LogOutRequiredView.xaml">
604+
<Generator>MSBuild:Compile</Generator>
605+
</Page>
600606
<Page Include="Views\Dialog\RepositoryCreationView.xaml">
601607
<Generator>MSBuild:Compile</Generator>
602608
<SubType>Designer</SubType>

src/GitHub.VisualStudio/Services/ShowDialogService.cs

+30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.ComponentModel.Composition;
34
using System.Reactive.Linq;
45
using System.Threading.Tasks;
6+
using GitHub.Models;
57
using GitHub.Services;
68
using GitHub.ViewModels;
79
using GitHub.ViewModels.Dialog;
@@ -37,6 +39,34 @@ public Task<object> Show(IDialogContentViewModel viewModel)
3739
return Task.FromResult(result);
3840
}
3941

42+
public async Task<object> Show<TViewModel>(
43+
TViewModel viewModel,
44+
IConnection connection,
45+
IEnumerable<string> scopes)
46+
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
47+
{
48+
var result = default(object);
49+
50+
using (var dialogViewModel = CreateViewModel())
51+
using (dialogViewModel.Done.Take(1).Subscribe(x => result = x))
52+
{
53+
if (!connection.Scopes.Matches(scopes))
54+
{
55+
await dialogViewModel.StartWithLogout(viewModel, connection);
56+
}
57+
else
58+
{
59+
await viewModel.InitializeAsync(connection);
60+
dialogViewModel.Start(viewModel);
61+
}
62+
63+
var window = new GitHubDialogWindow(dialogViewModel);
64+
window.ShowModal();
65+
}
66+
67+
return result;
68+
}
69+
4070
public async Task<object> ShowWithFirstConnection<TViewModel>(TViewModel viewModel)
4171
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
4272
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<UserControl x:Class="GitHub.VisualStudio.Views.Dialog.LogOutRequiredView"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:ghfvs="https://github.com/github/VisualStudio"
7+
xmlns:prop="clr-namespace:GitHub.VisualStudio.UI;assembly=GitHub.VisualStudio.UI"
8+
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
9+
10+
<Control.Resources>
11+
<ResourceDictionary>
12+
<ResourceDictionary.MergedDictionaries>
13+
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/SharedDictionary.xaml" />
14+
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml" />
15+
</ResourceDictionary.MergedDictionaries>
16+
</ResourceDictionary>
17+
</Control.Resources>
18+
19+
<DockPanel Margin="10">
20+
<ghfvs:OcticonImage DockPanel.Dock="Top"
21+
Icon="mark_github"
22+
Foreground="{DynamicResource GitHubVsWindowText}"
23+
Margin="0,5"
24+
Width="48"
25+
Height="48" />
26+
27+
<Label DockPanel.Dock="Top"
28+
Foreground="{DynamicResource GitHubVsWindowText}"
29+
HorizontalAlignment="Center"
30+
FontSize="16"
31+
Content="You need to sign out and back in." />
32+
33+
<ghfvs:OcticonCircleButton DockPanel.Dock="Bottom"
34+
Command="{Binding LogOut}"
35+
Content="Sign out"
36+
Icon="check"
37+
IsDefault="True"
38+
Margin="0 16"
39+
HorizontalAlignment="Center"/>
40+
41+
<TextBlock TextWrapping="Wrap"
42+
TextAlignment="Center"
43+
HorizontalAlignment="Center"
44+
Text="We're sorry, but the operation you requested requires more permissions than we currently have. Please sign out and back in."/>
45+
</DockPanel>
46+
</UserControl>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.ComponentModel.Composition;
2+
using System.Windows.Controls;
3+
using GitHub.Exports;
4+
using GitHub.ViewModels.Dialog;
5+
6+
namespace GitHub.VisualStudio.Views.Dialog
7+
{
8+
[ExportViewFor(typeof(ILogOutRequiredViewModel))]
9+
[PartCreationPolicy(CreationPolicy.NonShared)]
10+
public partial class LogOutRequiredView : UserControl
11+
{
12+
public LogOutRequiredView()
13+
{
14+
InitializeComponent();
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)