diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 42b7bfae2..ee6bb23df 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -128,6 +128,7 @@ namespace Bit.Android .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Other .RegisterInstance(CrossDeviceInfo.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) diff --git a/src/App/Abstractions/Repositories/IAccountsApiRepository.cs b/src/App/Abstractions/Repositories/IAccountsApiRepository.cs new file mode 100644 index 000000000..a8fda105d --- /dev/null +++ b/src/App/Abstractions/Repositories/IAccountsApiRepository.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface IAccountsApiRepository + { + Task PostRegisterAsync(RegisterRequest requestObj); + } +} \ No newline at end of file diff --git a/src/App/App.cs b/src/App/App.cs index 8c05cd018..1ad0e1321 100644 --- a/src/App/App.cs +++ b/src/App/App.cs @@ -42,8 +42,6 @@ namespace Bit.App MainPage = new HomePage(); } - MainPage.BackgroundColor = Color.FromHex("ecf0f5"); - MessagingCenter.Subscribe(Current, "Lock", async (sender, args) => { await CheckLockAsync(args); diff --git a/src/App/App.csproj b/src/App/App.csproj index d84182f2d..6c37a7989 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -35,6 +35,7 @@ 4 + @@ -70,6 +71,7 @@ + @@ -91,6 +93,7 @@ + @@ -102,6 +105,7 @@ + diff --git a/src/App/Models/Api/ApiResult.cs b/src/App/Models/Api/ApiResult.cs index 19def0ae7..6c3ddc3b2 100644 --- a/src/App/Models/Api/ApiResult.cs +++ b/src/App/Models/Api/ApiResult.cs @@ -38,4 +38,38 @@ namespace Bit.App.Models.Api return result; } } + + public class ApiResult + { + private List m_errors = new List(); + + public bool Succeeded { get; private set; } + public IEnumerable Errors => m_errors; + public HttpStatusCode StatusCode { get; private set; } + + public static ApiResult Success(HttpStatusCode statusCode) + { + return new ApiResult + { + Succeeded = true, + StatusCode = statusCode + }; + } + + public static ApiResult Failed(HttpStatusCode statusCode, params ApiError[] errors) + { + var result = new ApiResult + { + Succeeded = false, + StatusCode = statusCode + }; + + if(errors != null) + { + result.m_errors.AddRange(errors); + } + + return result; + } + } } diff --git a/src/App/Models/Api/Request/RegisterRequest.cs b/src/App/Models/Api/Request/RegisterRequest.cs new file mode 100644 index 000000000..5f37f1e64 --- /dev/null +++ b/src/App/Models/Api/Request/RegisterRequest.cs @@ -0,0 +1,10 @@ +namespace Bit.App.Models.Api +{ + public class RegisterRequest + { + public string Name { get; set; } + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string MasterPasswordHint { get; set; } + } +} diff --git a/src/App/Pages/HomePage.cs b/src/App/Pages/HomePage.cs index 70f41f598..3194ed186 100644 --- a/src/App/Pages/HomePage.cs +++ b/src/App/Pages/HomePage.cs @@ -25,7 +25,6 @@ namespace Bit.App.Pages Init(); } - public void Init() { var logo = new Image @@ -48,7 +47,7 @@ namespace Bit.App.Pages var createAccountButton = new Button { Text = "Create Account", - //Command = new Command(async () => await RegisterAsync()), + Command = new Command(async () => await RegisterAsync()), VerticalOptions = LayoutOptions.End, HorizontalOptions = LayoutOptions.Fill, Style = (Style)Application.Current.Resources["btn-primary"], @@ -74,12 +73,17 @@ namespace Bit.App.Pages Title = "bitwarden"; Content = buttonStackLayout; - BackgroundImage = "bg.png"; + BackgroundColor = Color.FromHex("ecf0f5"); } public async Task LoginAsync() { await Navigation.PushModalAsync(new ExtendedNavigationPage(new LoginPage())); } + + public async Task RegisterAsync() + { + await Navigation.PushModalAsync(new ExtendedNavigationPage(new RegisterPage())); + } } } diff --git a/src/App/Pages/LoginPage.cs b/src/App/Pages/LoginPage.cs index 7a9b1abc6..cad444085 100644 --- a/src/App/Pages/LoginPage.cs +++ b/src/App/Pages/LoginPage.cs @@ -44,8 +44,9 @@ namespace Bit.App.Pages var table = new ExtendedTableView { + BackgroundColor = Color.FromHex("ecf0f5"), Intent = TableIntent.Settings, - EnableScrolling = false, + EnableScrolling = true, HasUnevenRows = true, EnableSelection = false, Root = new TableRoot @@ -73,6 +74,7 @@ namespace Bit.App.Pages ToolbarItems.Add(loginToolbarItem); Title = AppResources.Bitwarden; Content = table; + BackgroundColor = Color.FromHex("ecf0f5"); } protected override void OnAppearing() diff --git a/src/App/Pages/RegisterPage.cs b/src/App/Pages/RegisterPage.cs new file mode 100644 index 000000000..62b328050 --- /dev/null +++ b/src/App/Pages/RegisterPage.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using Bit.App.Abstractions; +using Bit.App.Controls; +using Bit.App.Models.Api; +using Bit.App.Resources; +using Xamarin.Forms; +using XLabs.Ioc; +using Acr.UserDialogs; +using System.Threading.Tasks; + +namespace Bit.App.Pages +{ + public class RegisterPage : ContentPage + { + private ICryptoService _cryptoService; + private IUserDialogs _userDialogs; + private IAccountsApiRepository _accountsApiRepository; + + public RegisterPage() + { + _cryptoService = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + _accountsApiRepository = Resolver.Resolve(); + + Init(); + } + + public FormEntryCell NameCell { get; set; } + public FormEntryCell EmailCell { get; set; } + public FormEntryCell PasswordCell { get; set; } + public FormEntryCell ConfirmPasswordCell { get; set; } + public FormEntryCell PasswordHintCell { get; set; } + + private void Init() + { + PasswordHintCell = new FormEntryCell("Master Password Hint (optional)"); + ConfirmPasswordCell = new FormEntryCell("Re-type Master Password", IsPassword: true, nextElement: PasswordHintCell.Entry); + PasswordCell = new FormEntryCell(AppResources.MasterPassword, IsPassword: true, nextElement: ConfirmPasswordCell.Entry); + NameCell = new FormEntryCell("Your Name", nextElement: PasswordCell.Entry); + EmailCell = new FormEntryCell(AppResources.EmailAddress, nextElement: NameCell.Entry, entryKeyboard: Keyboard.Email); + + PasswordHintCell.Entry.ReturnType = Enums.ReturnType.Done; + PasswordHintCell.Entry.Completed += Entry_Completed; + + var table = new ExtendedTableView + { + BackgroundColor = Color.FromHex("ecf0f5"), + Intent = TableIntent.Settings, + EnableScrolling = true, + HasUnevenRows = true, + EnableSelection = false, + Root = new TableRoot + { + new TableSection() + { + EmailCell, + NameCell, + PasswordCell, + ConfirmPasswordCell, + PasswordHintCell + } + } + }; + + var loginToolbarItem = new ToolbarItem("Submit", null, async () => + { + await Register(); + }, ToolbarItemOrder.Default, 0); + + if(Device.OS == TargetPlatform.iOS) + { + table.RowHeight = -1; + table.EstimatedRowHeight = 70; + ToolbarItems.Add(new DismissModalToolBarItem(this, "Cancel")); + } + + ToolbarItems.Add(loginToolbarItem); + Title = "Create Account"; + Content = table; + BackgroundColor = Color.FromHex("ecf0f5"); + } + + protected override void OnAppearing() + { + base.OnAppearing(); + EmailCell.Entry.Focus(); + } + + private async void Entry_Completed(object sender, EventArgs e) + { + await Register(); + } + + private async Task Register() + { + if(string.IsNullOrWhiteSpace(EmailCell.Entry.Text)) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress), AppResources.Ok); + return; + } + + if(string.IsNullOrWhiteSpace(NameCell.Entry.Text)) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, "Your Name"), AppResources.Ok); + return; + } + + if(string.IsNullOrWhiteSpace(PasswordCell.Entry.Text)) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, "Your Name"), AppResources.Ok); + return; + } + + if(ConfirmPasswordCell.Entry.Text != PasswordCell.Entry.Text) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, "Password confirmation is not correct.", AppResources.Ok); + return; + } + + var key = _cryptoService.MakeKeyFromPassword(PasswordCell.Entry.Text, EmailCell.Entry.Text); + var request = new RegisterRequest + { + Name = NameCell.Entry.Text, + Email = EmailCell.Entry.Text, + MasterPasswordHash = _cryptoService.HashPasswordBase64(key, PasswordCell.Entry.Text), + MasterPasswordHint = !string.IsNullOrWhiteSpace(PasswordHintCell.Entry.Text) ? PasswordHintCell.Entry.Text : null + }; + + var responseTask = _accountsApiRepository.PostRegisterAsync(request); + _userDialogs.ShowLoading("Creating account...", MaskType.Black); + var response = await responseTask; + _userDialogs.HideLoading(); + if(!response.Succeeded) + { + await DisplayAlert(AppResources.AnErrorHasOccurred, response.Errors.FirstOrDefault()?.Message, AppResources.Ok); + return; + } + + _userDialogs.SuccessToast("Account Created", "Your new account has been created! You may now log in."); + await Navigation.PopModalAsync(); + } + } +} diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs new file mode 100644 index 000000000..8dc78f50c --- /dev/null +++ b/src/App/Repositories/AccountsApiRepository.cs @@ -0,0 +1,30 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; + +namespace Bit.App.Repositories +{ + public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository + { + protected override string ApiRoute => "accounts"; + + public virtual async Task PostRegisterAsync(RegisterRequest requestObj) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Post, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/register")), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + return ApiResult.Success(response.StatusCode); + } + } +} diff --git a/src/App/Repositories/BaseApiRepository.cs b/src/App/Repositories/BaseApiRepository.cs index 4c0af6524..18d5ee0d5 100644 --- a/src/App/Repositories/BaseApiRepository.cs +++ b/src/App/Repositories/BaseApiRepository.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; using System.Threading.Tasks; using Bit.App.Models.Api; using ModernHttpClient; @@ -27,21 +25,7 @@ namespace Bit.App.Repositories { try { - var errors = new List(); - if(response.StatusCode == System.Net.HttpStatusCode.BadRequest) - { - var responseContent = await response.Content.ReadAsStringAsync(); - var errorResponseModel = JsonConvert.DeserializeObject(responseContent); - - foreach(var valError in errorResponseModel.ValidationErrors) - { - foreach(var errorMessage in valError.Value) - { - errors.Add(new ApiError { Message = errorMessage }); - } - } - } - + var errors = await ParseErrorsAsync(response); return ApiResult.Failed(response.StatusCode, errors.ToArray()); } catch(JsonReaderException) @@ -49,5 +33,38 @@ namespace Bit.App.Repositories return ApiResult.Failed(response.StatusCode, new ApiError { Message = "An unknown error has occured." }); } + + public async Task HandleErrorAsync(HttpResponseMessage response) + { + try + { + var errors = await ParseErrorsAsync(response); + return ApiResult.Failed(response.StatusCode, errors.ToArray()); + } + catch(JsonReaderException) + { } + + return ApiResult.Failed(response.StatusCode, new ApiError { Message = "An unknown error has occured." }); + } + + private async Task> ParseErrorsAsync(HttpResponseMessage response) + { + var errors = new List(); + if(response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + var responseContent = await response.Content.ReadAsStringAsync(); + var errorResponseModel = JsonConvert.DeserializeObject(responseContent); + + foreach(var valError in errorResponseModel.ValidationErrors) + { + foreach(var errorMessage in valError.Value) + { + errors.Add(new ApiError { Message = errorMessage }); + } + } + } + + return errors; + } } } diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index f7e760bed..9a83f6cd0 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -29,7 +29,7 @@ namespace Bit.iOS.Extension public override void ViewDidLoad() { base.ViewDidLoad(); - View.BackgroundColor = UIColor.FromPatternImage(new UIImage("bg.png")); + View.BackgroundColor = new UIColor(red: 0.93f, green: 0.94f, blue: 0.96f, alpha: 1.0f); _context.ExtContext = ExtensionContext; if(!Resolver.IsSet) diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index b742777a4..2912c1a2d 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -62,7 +62,7 @@ namespace Bit.iOS var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) { - BackgroundColor = UIColor.FromPatternImage(new UIImage("bg.png")) + BackgroundColor = new UIColor(red: 0.93f, green: 0.94f, blue: 0.96f, alpha: 1.0f) }; var imageView = new UIImageView(new UIImage("logo.png")) @@ -188,6 +188,7 @@ namespace Bit.iOS .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) // Other .RegisterInstance(CrossDeviceInfo.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager()) diff --git a/src/iOS/LaunchScreen.storyboard b/src/iOS/LaunchScreen.storyboard index ced91723a..5672c5431 100644 --- a/src/iOS/LaunchScreen.storyboard +++ b/src/iOS/LaunchScreen.storyboard @@ -12,22 +12,15 @@ - + - + - - - - + - - - - @@ -65,6 +58,13 @@ + + + + + + +