diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln
index 26793a007..c41f97745 100644
--- a/Git-Credential-Manager.sln
+++ b/Git-Credential-Manager.sln
@@ -73,6 +73,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packaging.Linux", "src\linu
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "linux", "linux", "{8F9D7E67-7DD7-4E32-9134-423281AF00E9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\shared\GitHub.UI\GitHub.UI.csproj", "{B5F00B46-FE93-45F2-B283-52B74B3E13B9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI", "src\shared\Atlassian.Bitbucket.UI\Atlassian.Bitbucket.UI.csproj", "{EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Git.CredentialManager.UI", "src\shared\Microsoft.Git.CredentialManager.UI\Microsoft.Git.CredentialManager.UI.csproj", "{001846B0-462B-4A27-90CD-2435D4C0F680}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -315,6 +321,54 @@ Global
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.Build.0 = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.Build.0 = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.Build.0 = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.Build.0 = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
+ {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -341,6 +395,9 @@ Global
{D34D31DF-B44A-45D3-9B39-73573077BAE0} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
{AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9}
+ {B5F00B46-FE93-45F2-B283-52B74B3E13B9} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
+ {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
+ {001846B0-462B-4A27-90CD-2435D4C0F680} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B}
diff --git a/assets/gcm.xaml b/assets/gcm.xaml
new file mode 100644
index 000000000..31afc3ca2
--- /dev/null
+++ b/assets/gcm.xaml
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/src/linux/Packaging.Linux/build.sh b/src/linux/Packaging.Linux/build.sh
index 374bd04bb..f1c74732c 100755
--- a/src/linux/Packaging.Linux/build.sh
+++ b/src/linux/Packaging.Linux/build.sh
@@ -44,6 +44,8 @@ ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )"
SRC="$ROOT/src"
OUT="$ROOT/out"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
+BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI"
+GITHUB_UI_SRC="$SRC/shared/GitHub.UI"
PROJ_OUT="$OUT/linux/Packaging.Linux"
# Build parameters
@@ -94,8 +96,26 @@ dotnet publish "$GCM_SRC" \
--configuration="$CONFIGURATION" \
--framework="$FRAMEWORK" \
--runtime="$RUNTIME" \
- --self-contained=true \
- "/p:PublishSingleFile=True" \
+ --self-contained=true \
+ -p:PublishSingleFile=true \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+
+echo "Publishing Bitbucket UI helper..."
+dotnet publish "$BITBUCKET_UI_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --runtime="$RUNTIME" \
+ --self-contained=true \
+ -p:PublishSingleFile=true \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+
+echo "Publishing GitHub UI helper..."
+dotnet publish "$GITHUB_UI_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --runtime="$RUNTIME" \
+ --self-contained=true \
+ -p:PublishSingleFile=true \
--output="$(make_absolute "$PAYLOAD")" || exit 1
# Collect symbols
diff --git a/src/osx/Installer.Mac/Installer.Mac.csproj b/src/osx/Installer.Mac/Installer.Mac.csproj
index b6ceb7de0..cd23e4176 100644
--- a/src/osx/Installer.Mac/Installer.Mac.csproj
+++ b/src/osx/Installer.Mac/Installer.Mac.csproj
@@ -13,6 +13,8 @@
+
+
diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh
index 2f1f7f9f3..9d4493e66 100755
--- a/src/osx/Installer.Mac/layout.sh
+++ b/src/osx/Installer.Mac/layout.sh
@@ -21,6 +21,8 @@ SRC="$ROOT/src"
OUT="$ROOT/out"
INSTALLER_SRC="$SRC/osx/Installer.Mac"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
+BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI"
+GITHUB_UI_SRC="$SRC/shared/GitHub.UI"
# Build parameters
FRAMEWORK=net5.0
@@ -77,6 +79,20 @@ dotnet publish "$GCM_SRC" \
--runtime="$RUNTIME" \
--output="$(make_absolute "$PAYLOAD")" || exit 1
+echo "Publishing Bitbucket UI helper..."
+dotnet publish "$BITBUCKET_UI_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --runtime="$RUNTIME" \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+
+echo "Publishing GitHub UI helper..."
+dotnet publish "$GITHUB_UI_SRC" \
+ --configuration="$CONFIGURATION" \
+ --framework="$FRAMEWORK" \
+ --runtime="$RUNTIME" \
+ --output="$(make_absolute "$PAYLOAD")" || exit 1
+
# Collect symbols
echo "Collecting managed symbols..."
mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
diff --git a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj b/src/osx/SignFiles.Mac/SignFiles.Mac.csproj
index a29cd8c7f..45454fc7a 100644
--- a/src/osx/SignFiles.Mac/SignFiles.Mac.csproj
+++ b/src/osx/SignFiles.Mac/SignFiles.Mac.csproj
@@ -20,15 +20,20 @@
Microsoft400
false
+ $(OutDir)\git-credential-manager-core;
+ $(OutDir)\GitHub.UI;
+ $(OutDir)\Atlassian.Bitbucket.UI;">
false
diff --git a/src/shared/Atlassian.Bitbucket.UI/Assets/atlassian-logo.png b/src/shared/Atlassian.Bitbucket.UI/Assets/atlassian-logo.png
new file mode 100644
index 000000000..6226f936d
Binary files /dev/null and b/src/shared/Atlassian.Bitbucket.UI/Assets/atlassian-logo.png differ
diff --git a/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj b/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
new file mode 100644
index 000000000..ea9086bd1
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
@@ -0,0 +1,22 @@
+
+
+
+ WinExe
+ net5.0
+ osx-x64;linux-x64
+
+
+
+
+
+
+
+
+
+ TesterWindow.axaml
+ Code
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs
new file mode 100644
index 000000000..957d19de3
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Threading;
+using System.Threading.Tasks;
+using Atlassian.Bitbucket.UI.ViewModels;
+using Atlassian.Bitbucket.UI.Views;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.UI;
+
+namespace Atlassian.Bitbucket.UI.Commands
+{
+ internal class CredentialsCommand : HelperCommand
+ {
+ public CredentialsCommand(CommandContext context)
+ : base(context, "userpass", "Show authentication prompt.")
+ {
+ AddOption(
+ new Option("--username", "Username or email.")
+ );
+
+ Handler = CommandHandler.Create(ExecuteAsync);
+ }
+
+ private async Task ExecuteAsync(string userName)
+ {
+ var viewModel = new CredentialsViewModel(Context.Environment)
+ {
+ UserName = userName
+ };
+
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), CancellationToken.None);
+
+ if (!viewModel.WindowResult)
+ {
+ throw new Exception("User cancelled dialog.");
+ }
+
+ WriteResult(new Dictionary
+ {
+ ["username"] = viewModel.UserName,
+ ["password"] = viewModel.Password,
+ });
+
+ return 0;
+ }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs b/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs
new file mode 100644
index 000000000..6776f5aa5
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Threading;
+using System.Threading.Tasks;
+using Atlassian.Bitbucket.UI.ViewModels;
+using Atlassian.Bitbucket.UI.Views;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.UI;
+
+namespace Atlassian.Bitbucket.UI.Commands
+{
+ internal class OAuthCommand : HelperCommand
+ {
+ public OAuthCommand(CommandContext context)
+ : base(context, "oauth", "Show OAuth required prompt.")
+ {
+ Handler = CommandHandler.Create(ExecuteAsync);
+ }
+
+ private async Task ExecuteAsync()
+ {
+ var viewModel = new OAuthViewModel(Context.Environment);
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), CancellationToken.None);
+
+ if (!viewModel.WindowResult)
+ {
+ throw new Exception("User cancelled dialog.");
+ }
+
+ WriteResult(new Dictionary
+ {
+ ["continue"] = "true"
+ });
+
+ return 0;
+ }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml b/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml
new file mode 100644
index 000000000..86b010696
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml.cs b/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml.cs
new file mode 100644
index 000000000..51a265941
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Controls/TesterWindow.axaml.cs
@@ -0,0 +1,68 @@
+using Atlassian.Bitbucket.UI.ViewModels;
+using Atlassian.Bitbucket.UI.Views;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.Interop.Linux;
+using Microsoft.Git.CredentialManager.Interop.MacOS;
+using Microsoft.Git.CredentialManager.Interop.Posix;
+using Microsoft.Git.CredentialManager.Interop.Windows;
+using Microsoft.Git.CredentialManager.UI.Controls;
+
+namespace Atlassian.Bitbucket.UI.Controls
+{
+ public class TesterWindow : Window
+ {
+ private readonly IEnvironment _environment;
+
+ public TesterWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+
+ if (PlatformUtils.IsWindows())
+ {
+ _environment = new WindowsEnvironment(new WindowsFileSystem());
+ }
+ else
+ {
+ IFileSystem fs;
+ if (PlatformUtils.IsMacOS())
+ {
+ fs = new MacOSFileSystem();
+ }
+ else
+ {
+ fs = new LinuxFileSystem();
+ }
+
+ _environment = new PosixEnvironment(fs);
+ }
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void ShowCredentials(object sender, RoutedEventArgs e)
+ {
+ var vm = new CredentialsViewModel(_environment);
+ var view = new CredentialsView();
+ var window = new DialogWindow(view) {DataContext = vm};
+ window.ShowDialog(this);
+ }
+
+ private void ShowOAuth(object sender, RoutedEventArgs e)
+ {
+ var vm = new OAuthViewModel(_environment);
+ var view = new OAuthView();
+ var window = new DialogWindow(view) {DataContext = vm};
+ window.ShowDialog(this);
+ }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Program.cs b/src/shared/Atlassian.Bitbucket.UI/Program.cs
new file mode 100644
index 000000000..ae9877c55
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Program.cs
@@ -0,0 +1,69 @@
+using System;
+using System.CommandLine;
+using System.Threading;
+using Atlassian.Bitbucket.UI.Commands;
+using Atlassian.Bitbucket.UI.Controls;
+using Avalonia;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.UI;
+
+namespace Atlassian.Bitbucket.UI
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ // If we have no arguments then just start the app with the test window.
+ if (args.Length == 0)
+ {
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ return;
+ }
+
+ // Create the dispatcher on the main thread. This is required
+ // for some platform UI services such as macOS that mandates
+ // all controls are created/accessed on the initial thread
+ // created by the process (the process entry thread).
+ Dispatcher.Initialize();
+
+ // Run AppMain in a new thread and keep the main thread free
+ // to process the dispatcher's job queue.
+ var appMain = new Thread(AppMain) {Name = nameof(AppMain)};
+ appMain.Start(args);
+
+ // Process the dispatcher job queue (aka: message pump, run-loop, etc...)
+ // We must ensure to run this on the same thread that it was created on
+ // (the main thread) so we cannot use any async/await calls between
+ // Dispatcher.Create and Run.
+ Dispatcher.MainThread.Run();
+
+ // Execution should never reach here as AppMain terminates the process on completion.
+ throw new InvalidOperationException("Main dispatcher job queue shutdown unexpectedly");
+ }
+
+ private static void AppMain(object o)
+ {
+ string[] args = (string[]) o;
+
+ string appPath = ApplicationBase.GetEntryApplicationPath();
+ using (var context = new CommandContext(appPath))
+ using (var app = new HelperApplication(context))
+ {
+ app.RegisterCommand(new CredentialsCommand(context));
+ app.RegisterCommand(new OAuthCommand(context));
+
+ int exitCode = app.RunAsync(args)
+ .ConfigureAwait(false)
+ .GetAwaiter()
+ .GetResult();
+
+ Environment.Exit(exitCode);
+ }
+ }
+
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure(() => new AvaloniaApp(() => new TesterWindow()))
+ .UsePlatformDetect()
+ .LogToTrace();
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs
new file mode 100644
index 000000000..4858a0833
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs
@@ -0,0 +1,94 @@
+using System.ComponentModel;
+using System.Windows.Input;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.UI;
+using Microsoft.Git.CredentialManager.UI.ViewModels;
+
+namespace Atlassian.Bitbucket.UI.ViewModels
+{
+ public class CredentialsViewModel : WindowViewModel
+ {
+ private readonly IEnvironment _environment;
+
+ private string _userName;
+ private string _password;
+
+ public CredentialsViewModel()
+ {
+ // Constructor the XAML designer
+ }
+
+ public CredentialsViewModel(IEnvironment environment)
+ {
+ EnsureArgument.NotNull(environment, nameof(environment));
+
+ _environment = environment;
+
+ Title = "Connect to Bitbucket";
+ LoginCommand = new RelayCommand(Accept, CanLogin);
+ CancelCommand = new RelayCommand(Cancel);
+ ForgotPasswordCommand = new RelayCommand(ForgotPassword);
+ SignUpCommand = new RelayCommand(SignUp);
+
+ PropertyChanged += OnPropertyChanged;
+ }
+
+ private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(UserName):
+ case nameof(Password):
+ LoginCommand.RaiseCanExecuteChanged();
+ break;
+ }
+ }
+
+ private bool CanLogin()
+ {
+ return !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password);
+ }
+
+ private void ForgotPassword()
+ {
+ BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/password/reset/");
+ }
+
+ private void SignUp()
+ {
+ BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/signup/");
+ }
+
+ public string UserName
+ {
+ get => _userName;
+ set => SetAndRaisePropertyChanged(ref _userName, value);
+ }
+
+ public string Password
+ {
+ get => _password;
+ set => SetAndRaisePropertyChanged(ref _password, value);
+ }
+
+ ///
+ /// Start the process to validate the username/password
+ ///
+ public RelayCommand LoginCommand { get; }
+
+ ///
+ /// Cancel the authentication attempt.
+ ///
+ public ICommand CancelCommand { get; }
+
+ ///
+ /// Hyperlink to the Bitbucket forgotten password process.
+ ///
+ public ICommand ForgotPasswordCommand { get; }
+
+ ///
+ /// Hyperlink to the Bitbucket sign up process.
+ ///
+ public ICommand SignUpCommand { get; }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs b/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs
new file mode 100644
index 000000000..144ccea51
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs
@@ -0,0 +1,71 @@
+using System.Windows.Input;
+using Microsoft.Git.CredentialManager;
+using Microsoft.Git.CredentialManager.UI;
+using Microsoft.Git.CredentialManager.UI.ViewModels;
+
+namespace Atlassian.Bitbucket.UI.ViewModels
+{
+ public class OAuthViewModel : WindowViewModel
+ {
+ private readonly IEnvironment _environment;
+
+ public OAuthViewModel()
+ {
+ // Constructor the XAML designer
+ }
+
+ public OAuthViewModel(IEnvironment environment)
+ {
+ EnsureArgument.NotNull(environment, nameof(environment));
+
+ _environment = environment;
+
+ Title = "OAuth authentication required";
+ OkCommand = new RelayCommand(Accept);
+ CancelCommand = new RelayCommand(Cancel);
+ LearnMoreCommand = new RelayCommand(LearnMore);
+ ForgotPasswordCommand = new RelayCommand(ForgotPassword);
+ SignUpCommand = new RelayCommand(SignUp);
+ }
+
+ private void LearnMore()
+ {
+ BrowserUtils.OpenDefaultBrowser(_environment, "https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html");
+ }
+
+ private void ForgotPassword()
+ {
+ BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/password/reset/");
+ }
+
+ private void SignUp()
+ {
+ BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/signup/");
+ }
+
+ ///
+ /// Provides a link to Bitbucket OAuth documentation
+ ///
+ public ICommand LearnMoreCommand { get; }
+
+ ///
+ /// Hyperlink to the Bitbucket forgotten password process.
+ ///
+ public ICommand ForgotPasswordCommand { get; }
+
+ ///
+ /// Hyperlink to the Bitbucket sign up process.
+ ///
+ public ICommand SignUpCommand { get; }
+
+ ///
+ /// Run the OAuth dance.
+ ///
+ public ICommand OkCommand { get; }
+
+ ///
+ /// Cancel the authentication attempt.
+ ///
+ public ICommand CancelCommand { get; }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml b/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml
new file mode 100644
index 000000000..4ec609f76
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml.cs b/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml.cs
new file mode 100644
index 000000000..2e766d343
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Views/CredentialsView.axaml.cs
@@ -0,0 +1,43 @@
+using Atlassian.Bitbucket.UI.ViewModels;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Microsoft.Git.CredentialManager.UI.Controls;
+
+namespace Atlassian.Bitbucket.UI.Views
+{
+ public class CredentialsView : UserControl, IFocusable
+ {
+ private TextBox _userNameTextBox;
+ private TextBox _passwordTextBox;
+
+ public CredentialsView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _userNameTextBox = this.FindControl("userNameTextBox");
+ _passwordTextBox = this.FindControl("passwordTextBox");
+ }
+
+ public void SetFocus()
+ {
+ if (!(DataContext is CredentialsViewModel vm))
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(vm.UserName))
+ {
+ _userNameTextBox.Focus();
+ }
+ else
+ {
+ _passwordTextBox.Focus();
+ }
+ }
+ }
+}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml b/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml
new file mode 100644
index 000000000..80ef5198e
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml.cs b/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml.cs
new file mode 100644
index 000000000..a2ca53767
--- /dev/null
+++ b/src/shared/Atlassian.Bitbucket.UI/Views/OAuthView.axaml.cs
@@ -0,0 +1,29 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Microsoft.Git.CredentialManager.UI.Controls;
+
+namespace Atlassian.Bitbucket.UI.Views
+{
+ public class OAuthView : UserControl, IFocusable
+ {
+ private Button _okButton;
+
+ public OAuthView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _okButton = this.FindControl