From decd3fc24ec786d5d367adeb98351944f5481f68 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 May 2016 00:17:38 -0400 Subject: [PATCH] Added icons for iOS. Broke out data access into repositories. Added syncing service. --- src/Android/MainActivity.cs | 15 ++- .../Repositories/IApiRepository.cs | 19 +++ .../Repositories/IAuthApiRepository.cs | 11 ++ .../Repositories/IFolderApiRepository.cs | 12 ++ .../Repositories/IFolderRepository.cs | 12 ++ .../Abstractions/Repositories/IRepository.cs | 18 +++ .../Repositories/ISiteApiRepository.cs | 12 ++ .../Repositories/ISiteRepository.cs | 12 ++ src/App/Abstractions/Services/IApiService.cs | 13 -- src/App/Abstractions/Services/ISiteService.cs | 1 + src/App/Abstractions/Services/ISyncService.cs | 9 ++ src/App/App.csproj | 22 +++- .../Api/Request/TokenTwoFactorRequest.cs | 8 ++ src/App/Models/Api/Response/FolderResponse.cs | 5 +- src/App/Models/Api/Response/ListResponse.cs | 14 +++ src/App/Models/Api/Response/SiteResponse.cs | 5 +- src/App/Pages/MainPage.cs | 20 ++-- src/App/Pages/SyncPage.cs | 42 ++++++- src/App/Pages/VaultListPage.cs | 27 +++-- src/App/Repositories/ApiRepository.cs | 112 ++++++++++++++++++ src/App/Repositories/AuthApiRepository.cs | 52 ++++++++ .../BaseApiRepository.cs} | 14 ++- src/App/Repositories/FolderApiRepository.cs | 34 ++++++ src/App/Repositories/FolderRepository.cs | 22 ++++ .../{Services => Repositories}/Repository.cs | 16 +-- src/App/Repositories/SiteApiRepository.cs | 34 ++++++ src/App/Repositories/SiteRepository.cs | 22 ++++ src/App/Services/AuthService.cs | 18 +-- src/App/Services/FolderService.cs | 69 +++++------ src/App/Services/SiteService.cs | 89 +++++++------- src/App/Services/SyncService.cs | 108 +++++++++++++++++ src/iOS/AppDelegate.cs | 15 ++- src/iOS/Info.plist | 2 +- src/iOS/Resources/fa-cogs.png | Bin 0 -> 705 bytes src/iOS/Resources/fa-cogs@2x.png | Bin 0 -> 1165 bytes src/iOS/Resources/fa-cogs@3x.png | Bin 0 -> 1660 bytes src/iOS/Resources/fa-lock.png | Bin 0 -> 388 bytes src/iOS/Resources/fa-lock@2x.png | Bin 0 -> 587 bytes src/iOS/Resources/fa-lock@3x.png | Bin 0 -> 820 bytes src/iOS/Resources/fa-plus.png | Bin 0 -> 242 bytes src/iOS/Resources/fa-plus@2x.png | Bin 0 -> 335 bytes src/iOS/Resources/fa-plus@3x.png | Bin 0 -> 418 bytes src/iOS/Resources/fa-refresh.png | Bin 0 -> 595 bytes src/iOS/Resources/fa-refresh@2x.png | Bin 0 -> 960 bytes src/iOS/Resources/fa-refresh@3x.png | Bin 0 -> 1269 bytes src/iOS/iOS.csproj | 39 ++++++ 46 files changed, 773 insertions(+), 150 deletions(-) create mode 100644 src/App/Abstractions/Repositories/IApiRepository.cs create mode 100644 src/App/Abstractions/Repositories/IAuthApiRepository.cs create mode 100644 src/App/Abstractions/Repositories/IFolderApiRepository.cs create mode 100644 src/App/Abstractions/Repositories/IFolderRepository.cs create mode 100644 src/App/Abstractions/Repositories/IRepository.cs create mode 100644 src/App/Abstractions/Repositories/ISiteApiRepository.cs create mode 100644 src/App/Abstractions/Repositories/ISiteRepository.cs delete mode 100644 src/App/Abstractions/Services/IApiService.cs create mode 100644 src/App/Abstractions/Services/ISyncService.cs create mode 100644 src/App/Models/Api/Request/TokenTwoFactorRequest.cs create mode 100644 src/App/Models/Api/Response/ListResponse.cs create mode 100644 src/App/Repositories/ApiRepository.cs create mode 100644 src/App/Repositories/AuthApiRepository.cs rename src/App/{Services/ApiService.cs => Repositories/BaseApiRepository.cs} (76%) create mode 100644 src/App/Repositories/FolderApiRepository.cs create mode 100644 src/App/Repositories/FolderRepository.cs rename src/App/{Services => Repositories}/Repository.cs (70%) create mode 100644 src/App/Repositories/SiteApiRepository.cs create mode 100644 src/App/Repositories/SiteRepository.cs create mode 100644 src/App/Services/SyncService.cs create mode 100644 src/iOS/Resources/fa-cogs.png create mode 100644 src/iOS/Resources/fa-cogs@2x.png create mode 100644 src/iOS/Resources/fa-cogs@3x.png create mode 100644 src/iOS/Resources/fa-lock.png create mode 100644 src/iOS/Resources/fa-lock@2x.png create mode 100644 src/iOS/Resources/fa-lock@3x.png create mode 100644 src/iOS/Resources/fa-plus.png create mode 100644 src/iOS/Resources/fa-plus@2x.png create mode 100644 src/iOS/Resources/fa-plus@3x.png create mode 100644 src/iOS/Resources/fa-refresh.png create mode 100644 src/iOS/Resources/fa-refresh@2x.png create mode 100644 src/iOS/Resources/fa-refresh@3x.png diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 2b3b55c85..696bf2c88 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -15,6 +15,7 @@ using Bit.Android.Services; using Plugin.Settings; using Plugin.Connectivity; using Acr.UserDialogs; +using Bit.App.Repositories; namespace Bit.Android { @@ -40,15 +41,23 @@ namespace Bit.Android var container = new UnityContainer(); container - .RegisterType(new ContainerControlledLifetimeManager()) + // Services .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) - .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) - .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + // Repositories + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + // Other + .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager()); diff --git a/src/App/Abstractions/Repositories/IApiRepository.cs b/src/App/Abstractions/Repositories/IApiRepository.cs new file mode 100644 index 000000000..651e69701 --- /dev/null +++ b/src/App/Abstractions/Repositories/IApiRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface IApiRepository + where TRequest : class + where TResponse : class + where TId : IEquatable + { + Task> GetByIdAsync(TId id); + Task>> GetAsync(); + Task> PostAsync(TRequest requestObj); + Task> PutAsync(TId id, TRequest requestObj); + Task> DeleteAsync(TId id); + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/IAuthApiRepository.cs b/src/App/Abstractions/Repositories/IAuthApiRepository.cs new file mode 100644 index 000000000..208c515a2 --- /dev/null +++ b/src/App/Abstractions/Repositories/IAuthApiRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface IAuthApiRepository + { + Task> PostTokenAsync(TokenRequest requestObj); + Task> PostTokenTwoFactorAsync(TokenTwoFactorRequest requestObj); + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/IFolderApiRepository.cs b/src/App/Abstractions/Repositories/IFolderApiRepository.cs new file mode 100644 index 000000000..c46e84aa4 --- /dev/null +++ b/src/App/Abstractions/Repositories/IFolderApiRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface IFolderApiRepository : IApiRepository + { + Task>> GetByRevisionDateAsync(DateTime since); + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/IFolderRepository.cs b/src/App/Abstractions/Repositories/IFolderRepository.cs new file mode 100644 index 000000000..57045d646 --- /dev/null +++ b/src/App/Abstractions/Repositories/IFolderRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Data; + +namespace Bit.App.Abstractions +{ + public interface IFolderRepository : IRepository + { + Task> GetAllByUserIdAsync(string userId); + } +} diff --git a/src/App/Abstractions/Repositories/IRepository.cs b/src/App/Abstractions/Repositories/IRepository.cs new file mode 100644 index 000000000..4bf4e762d --- /dev/null +++ b/src/App/Abstractions/Repositories/IRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bit.App.Abstractions +{ + public interface IRepository + where T : class, IDataObject, new() + where TId : IEquatable + { + Task GetByIdAsync(TId id); + Task> GetAllAsync(); + Task UpdateAsync(T obj); + Task InsertAsync(T obj); + Task DeleteAsync(TId id); + Task DeleteAsync(T obj); + } +} diff --git a/src/App/Abstractions/Repositories/ISiteApiRepository.cs b/src/App/Abstractions/Repositories/ISiteApiRepository.cs new file mode 100644 index 000000000..6cd45b70d --- /dev/null +++ b/src/App/Abstractions/Repositories/ISiteApiRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface ISiteApiRepository : IApiRepository + { + Task>> GetByRevisionDateAsync(DateTime since); + } +} \ No newline at end of file diff --git a/src/App/Abstractions/Repositories/ISiteRepository.cs b/src/App/Abstractions/Repositories/ISiteRepository.cs new file mode 100644 index 000000000..c16edea2b --- /dev/null +++ b/src/App/Abstractions/Repositories/ISiteRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Data; + +namespace Bit.App.Abstractions +{ + public interface ISiteRepository : IRepository + { + Task> GetAllByUserIdAsync(string userId); + } +} diff --git a/src/App/Abstractions/Services/IApiService.cs b/src/App/Abstractions/Services/IApiService.cs deleted file mode 100644 index 0373a2d27..000000000 --- a/src/App/Abstractions/Services/IApiService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net.Http; -using System.Threading.Tasks; -using Bit.App.Models.Api; - -namespace Bit.App.Abstractions -{ - public interface IApiService - { - HttpClient Client { get; set; } - - Task> HandleErrorAsync(HttpResponseMessage response); - } -} diff --git a/src/App/Abstractions/Services/ISiteService.cs b/src/App/Abstractions/Services/ISiteService.cs index 96b11482e..0f3bde43a 100644 --- a/src/App/Abstractions/Services/ISiteService.cs +++ b/src/App/Abstractions/Services/ISiteService.cs @@ -7,6 +7,7 @@ namespace Bit.App.Abstractions { public interface ISiteService { + Task GetByIdAsync(string id); Task> GetAllAsync(); Task> SaveAsync(Site site); Task> DeleteAsync(string id); diff --git a/src/App/Abstractions/Services/ISyncService.cs b/src/App/Abstractions/Services/ISyncService.cs new file mode 100644 index 000000000..4317e67da --- /dev/null +++ b/src/App/Abstractions/Services/ISyncService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Bit.App.Abstractions +{ + public interface ISyncService + { + Task SyncAsync(); + } +} \ No newline at end of file diff --git a/src/App/App.csproj b/src/App/App.csproj index 050fcad7d..bb21c8877 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -48,8 +48,10 @@ + + @@ -65,13 +67,28 @@ + + + + + + + + + + + + + + - - + + + @@ -83,7 +100,6 @@ - diff --git a/src/App/Models/Api/Request/TokenTwoFactorRequest.cs b/src/App/Models/Api/Request/TokenTwoFactorRequest.cs new file mode 100644 index 000000000..edf1258e8 --- /dev/null +++ b/src/App/Models/Api/Request/TokenTwoFactorRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.App.Models.Api +{ + public class TokenTwoFactorRequest + { + public string Code { get; set; } + public string Provider { get; set; } + } +} diff --git a/src/App/Models/Api/Response/FolderResponse.cs b/src/App/Models/Api/Response/FolderResponse.cs index 6ad1f0a69..869a476f5 100644 --- a/src/App/Models/Api/Response/FolderResponse.cs +++ b/src/App/Models/Api/Response/FolderResponse.cs @@ -1,8 +1,11 @@ -namespace Bit.App.Models.Api +using System; + +namespace Bit.App.Models.Api { public class FolderResponse { public string Id { get; set; } public string Name { get; set; } + public DateTime RevisionDate { get; set; } } } diff --git a/src/App/Models/Api/Response/ListResponse.cs b/src/App/Models/Api/Response/ListResponse.cs new file mode 100644 index 000000000..148170a40 --- /dev/null +++ b/src/App/Models/Api/Response/ListResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Bit.App.Models.Api +{ + public class ListResponse + { + public ListResponse(IEnumerable data) + { + Data = data; + } + + public IEnumerable Data { get; set; } + } +} diff --git a/src/App/Models/Api/Response/SiteResponse.cs b/src/App/Models/Api/Response/SiteResponse.cs index cf2dd2d69..1db44bda5 100644 --- a/src/App/Models/Api/Response/SiteResponse.cs +++ b/src/App/Models/Api/Response/SiteResponse.cs @@ -1,4 +1,6 @@ -namespace Bit.App.Models.Api +using System; + +namespace Bit.App.Models.Api { public class SiteResponse { @@ -9,6 +11,7 @@ public string Username { get; set; } public string Password { get; set; } public string Notes { get; set; } + public DateTime RevisionDate { get; set; } // Expandables public FolderResponse Folder { get; set; } diff --git a/src/App/Pages/MainPage.cs b/src/App/Pages/MainPage.cs index 0f0cdb74e..f903edd61 100644 --- a/src/App/Pages/MainPage.cs +++ b/src/App/Pages/MainPage.cs @@ -1,5 +1,4 @@ using System; -using Bit.App.Abstractions; using Xamarin.Forms; namespace Bit.App.Pages @@ -8,18 +7,21 @@ namespace Bit.App.Pages { public MainPage() { - var vaultNavigation = new NavigationPage(new VaultListPage()); - vaultNavigation.BarBackgroundColor = Color.FromHex("3c8dbc"); - vaultNavigation.BarTextColor = Color.FromHex("ffffff"); - vaultNavigation.Title = "My Vault"; - var settingsNavigation = new NavigationPage(new SettingsPage()); - settingsNavigation.BarBackgroundColor = Color.FromHex("3c8dbc"); - settingsNavigation.BarTextColor = Color.FromHex("ffffff"); + var vaultNavigation = new NavigationPage(new VaultListPage()); + var syncPage = new SyncPage(); + + vaultNavigation.BarBackgroundColor = settingsNavigation.BarBackgroundColor = Color.FromHex("3c8dbc"); + vaultNavigation.BarTextColor = settingsNavigation.BarTextColor = Color.FromHex("ffffff"); + + vaultNavigation.Title = "My Vault"; + vaultNavigation.Icon = "fa-lock"; + settingsNavigation.Title = "Settings"; + settingsNavigation.Icon = "fa-cogs"; Children.Add(vaultNavigation); - Children.Add(new SyncPage()); + Children.Add(syncPage); Children.Add(settingsNavigation); } } diff --git a/src/App/Pages/SyncPage.cs b/src/App/Pages/SyncPage.cs index 28a531030..d129293e9 100644 --- a/src/App/Pages/SyncPage.cs +++ b/src/App/Pages/SyncPage.cs @@ -1,14 +1,54 @@ using System; +using System.Threading.Tasks; +using Acr.UserDialogs; +using Bit.App.Abstractions; using Xamarin.Forms; +using XLabs.Ioc; namespace Bit.App.Pages { public class SyncPage : ContentPage { + private readonly ISyncService _syncService; + private readonly IUserDialogs _userDialogs; + public SyncPage() { + _syncService = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); + + Init(); + } + + public void Init() + { + var syncButton = new Button + { + Text = "Sync Vault", + Command = new Command(async () => await SyncAsync()) + }; + + var stackLayout = new StackLayout { }; + stackLayout.Children.Add(syncButton); + Title = "Sync"; - Content = null; + Content = stackLayout; + Icon = "fa-refresh"; + } + + public async Task SyncAsync() + { + _userDialogs.ShowLoading("Syncing...", MaskType.Black); + var succeeded = await _syncService.SyncAsync(); + _userDialogs.HideLoading(); + if(succeeded) + { + _userDialogs.SuccessToast("Syncing complete."); + } + else + { + _userDialogs.ErrorToast("Syncing failed."); + } } } } diff --git a/src/App/Pages/VaultListPage.cs b/src/App/Pages/VaultListPage.cs index 129750f9c..8a20fb68c 100644 --- a/src/App/Pages/VaultListPage.cs +++ b/src/App/Pages/VaultListPage.cs @@ -31,21 +31,14 @@ namespace Bit.App.Pages { ToolbarItems.Add(new AddSiteToolBarItem(this)); - var moreAction = new MenuItem { Text = "More" }; - moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding(".")); - moreAction.Clicked += MoreClickedAsync; - - var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true }; - deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding(".")); - deleteAction.Clicked += DeleteClickedAsync; - var listView = new ListView { IsGroupingEnabled = true, ItemsSource = Folders }; listView.GroupDisplayBinding = new Binding("Name"); listView.ItemSelected += SiteSelected; - listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(moreAction, deleteAction)); + listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(this)); Title = "My Vault"; Content = listView; + NavigationPage.SetBackButtonTitle(this, string.Empty); } protected override void OnAppearing() @@ -122,7 +115,7 @@ namespace Bit.App.Pages { _page = page; Text = "Add"; - Icon = ""; + Icon = "fa-plus"; Clicked += ClickedItem; } @@ -144,12 +137,20 @@ namespace Bit.App.Pages private class VaultListViewCell : TextCell { - public VaultListViewCell(MenuItem moreMenuItem, MenuItem deleteMenuItem) + public VaultListViewCell(VaultListPage page) { + var moreAction = new MenuItem { Text = "More" }; + moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding(".")); + moreAction.Clicked += page.MoreClickedAsync; + + var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true }; + deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding(".")); + deleteAction.Clicked += page.DeleteClickedAsync; + this.SetBinding(TextProperty, s => s.Name); this.SetBinding(DetailProperty, s => s.Username); - ContextActions.Add(moreMenuItem); - ContextActions.Add(deleteMenuItem); + ContextActions.Add(moreAction); + ContextActions.Add(deleteAction); } } } diff --git a/src/App/Repositories/ApiRepository.cs b/src/App/Repositories/ApiRepository.cs new file mode 100644 index 000000000..080055d33 --- /dev/null +++ b/src/App/Repositories/ApiRepository.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Newtonsoft.Json; + +namespace Bit.App.Repositories +{ + public abstract class ApiRepository : BaseApiRepository, IApiRepository + where TId : IEquatable + where TRequest : class + where TResponse : class + { + public ApiRepository() + { } + + public virtual async Task> GetByIdAsync(TId id) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + + public virtual async Task>> GetAsync() + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, ApiRoute), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject>(responseContent); + return ApiResult>.Success(responseObj, response.StatusCode); + } + + public virtual async Task> PostAsync(TRequest requestObj) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Post, + RequestUri = new Uri(Client.BaseAddress, ApiRoute), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + + public virtual async Task> PutAsync(TId id, TRequest requestObj) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Put, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + + public virtual async Task> DeleteAsync(TId id) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Delete, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + return ApiResult.Success(null, response.StatusCode); + } + } +} diff --git a/src/App/Repositories/AuthApiRepository.cs b/src/App/Repositories/AuthApiRepository.cs new file mode 100644 index 000000000..a149d10b9 --- /dev/null +++ b/src/App/Repositories/AuthApiRepository.cs @@ -0,0 +1,52 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Newtonsoft.Json; + +namespace Bit.App.Repositories +{ + public class AuthApiRepository : BaseApiRepository, IAuthApiRepository + { + protected override string ApiRoute => "auth"; + + public virtual async Task> PostTokenAsync(TokenRequest requestObj) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Post, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/token")), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + + public virtual async Task> PostTokenTwoFactorAsync(TokenTwoFactorRequest requestObj) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Post, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/token/two-factor")), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject(responseContent); + return ApiResult.Success(responseObj, response.StatusCode); + } + } +} diff --git a/src/App/Services/ApiService.cs b/src/App/Repositories/BaseApiRepository.cs similarity index 76% rename from src/App/Services/ApiService.cs rename to src/App/Repositories/BaseApiRepository.cs index 306d809dc..e8fe811d4 100644 --- a/src/App/Services/ApiService.cs +++ b/src/App/Repositories/BaseApiRepository.cs @@ -1,23 +1,27 @@ 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.Abstractions; using Bit.App.Models.Api; using ModernHttpClient; using Newtonsoft.Json; -namespace Bit.App.Services +namespace Bit.App.Repositories { - public class ApiService : IApiService + public abstract class BaseApiRepository { - public ApiService() + public BaseApiRepository() { Client = new HttpClient(new NativeMessageHandler()); Client.BaseAddress = new Uri("https://api.bitwarden.com"); + Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } - public HttpClient Client { get; set; } + protected virtual HttpClient Client { get; private set; } + protected abstract string ApiRoute { get; } public async Task> HandleErrorAsync(HttpResponseMessage response) { diff --git a/src/App/Repositories/FolderApiRepository.cs b/src/App/Repositories/FolderApiRepository.cs new file mode 100644 index 000000000..7e116833f --- /dev/null +++ b/src/App/Repositories/FolderApiRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Newtonsoft.Json; + +namespace Bit.App.Repositories +{ + public class FolderApiRepository : ApiRepository, IFolderApiRepository + { + protected override string ApiRoute => "folders"; + + public virtual async Task>> GetByRevisionDateAsync(DateTime since) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject>(responseContent); + return ApiResult>.Success(responseObj, response.StatusCode); + } + } +} diff --git a/src/App/Repositories/FolderRepository.cs b/src/App/Repositories/FolderRepository.cs new file mode 100644 index 000000000..0b636caf7 --- /dev/null +++ b/src/App/Repositories/FolderRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Data; + +namespace Bit.App.Repositories +{ + public class FolderRepository : Repository, IFolderRepository + { + public FolderRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task> GetAllByUserIdAsync(string userId) + { + var folders = Connection.Table().Where(f => f.UserId == userId).Cast(); + return Task.FromResult(folders); + } + } +} diff --git a/src/App/Services/Repository.cs b/src/App/Repositories/Repository.cs similarity index 70% rename from src/App/Services/Repository.cs rename to src/App/Repositories/Repository.cs index c549a8ed4..b6a7146b6 100644 --- a/src/App/Services/Repository.cs +++ b/src/App/Repositories/Repository.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using Bit.App.Abstractions; using SQLite; -namespace Bit.App.Services +namespace Bit.App.Repositories { - public abstract class Repository + public abstract class Repository : IRepository where TId : IEquatable where T : class, IDataObject, new() { @@ -18,34 +18,34 @@ namespace Bit.App.Services protected SQLiteConnection Connection { get; private set; } - protected virtual Task GetByIdAsync(TId id) + public virtual Task GetByIdAsync(TId id) { return Task.FromResult(Connection.Get(id)); } - protected virtual Task> GetAllAsync() + public virtual Task> GetAllAsync() { return Task.FromResult(Connection.Table().Cast()); } - protected virtual Task CreateAsync(T obj) + public virtual Task InsertAsync(T obj) { Connection.Insert(obj); return Task.FromResult(0); } - protected virtual Task ReplaceAsync(T obj) + public virtual Task UpdateAsync(T obj) { Connection.Update(obj); return Task.FromResult(0); } - protected virtual async Task DeleteAsync(T obj) + public virtual async Task DeleteAsync(T obj) { await DeleteAsync(obj.Id); } - protected virtual Task DeleteAsync(TId id) + public virtual Task DeleteAsync(TId id) { Connection.Delete(id); return Task.FromResult(0); diff --git a/src/App/Repositories/SiteApiRepository.cs b/src/App/Repositories/SiteApiRepository.cs new file mode 100644 index 000000000..67f746b65 --- /dev/null +++ b/src/App/Repositories/SiteApiRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Newtonsoft.Json; + +namespace Bit.App.Repositories +{ + public class SiteApiRepository : ApiRepository, ISiteApiRepository + { + protected override string ApiRoute => "sites"; + + public virtual async Task>> GetByRevisionDateAsync(DateTime since) + { + var requestMessage = new TokenHttpRequestMessage() + { + Method = HttpMethod.Get, + RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)), + }; + + var response = await Client.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject>(responseContent); + return ApiResult>.Success(responseObj, response.StatusCode); + } + } +} diff --git a/src/App/Repositories/SiteRepository.cs b/src/App/Repositories/SiteRepository.cs new file mode 100644 index 000000000..1c76cd0b3 --- /dev/null +++ b/src/App/Repositories/SiteRepository.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Data; + +namespace Bit.App.Repositories +{ + public class SiteRepository : Repository, ISiteRepository + { + public SiteRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task> GetAllByUserIdAsync(string userId) + { + var sites = Connection.Table().Where(f => f.UserId == userId).Cast(); + return Task.FromResult(sites); + } + } +} diff --git a/src/App/Services/AuthService.cs b/src/App/Services/AuthService.cs index 53b6f49d5..988633db2 100644 --- a/src/App/Services/AuthService.cs +++ b/src/App/Services/AuthService.cs @@ -17,7 +17,7 @@ namespace Bit.App.Services private readonly ISecureStorageService _secureStorage; private readonly ISettings _settings; private readonly ICryptoService _cryptoService; - private readonly IApiService _apiService; + private readonly IAuthApiRepository _authApiRepository; private string _token; private string _userId; @@ -26,12 +26,12 @@ namespace Bit.App.Services ISecureStorageService secureStorage, ISettings settings, ICryptoService cryptoService, - IApiService apiService) + IAuthApiRepository authApiRepository) { _secureStorage = secureStorage; _settings = settings; _cryptoService = cryptoService; - _apiService = apiService; + _authApiRepository = authApiRepository; } public string Token @@ -110,16 +110,8 @@ namespace Bit.App.Services public async Task> TokenPostAsync(TokenRequest request) { - var requestContent = JsonConvert.SerializeObject(request); - var response = await _apiService.Client.PostAsync("/auth/token", new StringContent(requestContent, Encoding.UTF8, "application/json")); - if(!response.IsSuccessStatusCode) - { - return await _apiService.HandleErrorAsync(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject(responseContent); - return ApiResult.Success(responseObj, response.StatusCode); + // TODO: move more logic in here + return await _authApiRepository.PostTokenAsync(request); } } } diff --git a/src/App/Services/FolderService.cs b/src/App/Services/FolderService.cs index 9cfc77ead..f1e7c17fa 100644 --- a/src/App/Services/FolderService.cs +++ b/src/App/Services/FolderService.cs @@ -6,70 +6,73 @@ using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Models.Data; using Bit.App.Models.Api; -using Newtonsoft.Json; -using System.Net.Http; namespace Bit.App.Services { - public class FolderService : Repository, IFolderService + public class FolderService : IFolderService { + private readonly IFolderRepository _folderRepository; private readonly IAuthService _authService; - private readonly IApiService _apiService; + private readonly IFolderApiRepository _folderApiRepository; public FolderService( - ISqlService sqlService, + IFolderRepository folderRepository, IAuthService authService, - IApiService apiService) - : base(sqlService) + IFolderApiRepository folderApiRepository) { + _folderRepository = folderRepository; _authService = authService; - _apiService = apiService; + _folderApiRepository = folderApiRepository; } - public new Task GetByIdAsync(string id) + public async Task GetByIdAsync(string id) { - var data = Connection.Table().Where(f => f.UserId == _authService.UserId && f.Id == id).FirstOrDefault(); + var data = await _folderRepository.GetByIdAsync(id); + if(data == null || data.UserId != _authService.UserId) + { + return null; + } + var folder = new Folder(data); - return Task.FromResult(folder); + return folder; } - public new Task> GetAllAsync() + public async Task> GetAllAsync() { - var data = Connection.Table().Where(f => f.UserId == _authService.UserId).Cast(); + var data = await _folderRepository.GetAllByUserIdAsync(_authService.UserId); var folders = data.Select(f => new Folder(f)); - return Task.FromResult(folders); + return folders; } public async Task> SaveAsync(Folder folder) { + ApiResult response = null; var request = new FolderRequest(folder); - var requestMessage = new TokenHttpRequestMessage(request) - { - Method = folder.Id == null ? HttpMethod.Post : HttpMethod.Put, - RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : $"/folders/{folder.Id}"), - }; - - var response = await _apiService.Client.SendAsync(requestMessage); - if(!response.IsSuccessStatusCode) - { - return await _apiService.HandleErrorAsync(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject(responseContent); - var data = new FolderData(responseObj, _authService.UserId); if(folder.Id == null) { - await CreateAsync(data); - folder.Id = responseObj.Id; + response = await _folderApiRepository.PostAsync(request); } else { - await ReplaceAsync(data); + response = await _folderApiRepository.PutAsync(folder.Id, request); } - return ApiResult.Success(responseObj, response.StatusCode); + if(response.Succeeded) + { + var data = new FolderData(response.Result, _authService.UserId); + if(folder.Id == null) + { + await _folderRepository.InsertAsync(data); + folder.Id = data.Id; + } + else + { + await _folderRepository.UpdateAsync(data); + } + } + + return response; } } } diff --git a/src/App/Services/SiteService.cs b/src/App/Services/SiteService.cs index 83aa7fc71..f993d2e6f 100644 --- a/src/App/Services/SiteService.cs +++ b/src/App/Services/SiteService.cs @@ -1,86 +1,89 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Models.Api; using Bit.App.Models.Data; -using Newtonsoft.Json; namespace Bit.App.Services { - public class SiteService : Repository, ISiteService + public class SiteService : ISiteService { + private readonly ISiteRepository _siteRepository; private readonly IAuthService _authService; - private readonly IApiService _apiService; + private readonly ISiteApiRepository _siteApiRepository; public SiteService( - ISqlService sqlService, + ISiteRepository siteRepository, IAuthService authService, - IApiService apiService) - : base(sqlService) + ISiteApiRepository siteApiRepository) { + _siteRepository = siteRepository; _authService = authService; - _apiService = apiService; + _siteApiRepository = siteApiRepository; } - public new Task> GetAllAsync() + public async Task GetByIdAsync(string id) { - var data = Connection.Table().Where(f => f.UserId == _authService.UserId).Cast(); - var sites = data.Select(s => new Site(s)); - return Task.FromResult(sites); + var data = await _siteRepository.GetByIdAsync(id); + if(data == null || data.UserId != _authService.UserId) + { + return null; + } + + var site = new Site(data); + return site; + } + + public async Task> GetAllAsync() + { + var data = await _siteRepository.GetAllByUserIdAsync(_authService.UserId); + var sites = data.Select(f => new Site(f)); + return sites; } public async Task> SaveAsync(Site site) { + ApiResult response = null; var request = new SiteRequest(site); - var requestMessage = new TokenHttpRequestMessage(request) - { - Method = site.Id == null ? HttpMethod.Post : HttpMethod.Put, - RequestUri = new Uri(_apiService.Client.BaseAddress, site.Id == null ? "/sites" : $"/folders/{site.Id}") - }; - - var response = await _apiService.Client.SendAsync(requestMessage); - if(!response.IsSuccessStatusCode) - { - return await _apiService.HandleErrorAsync(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject(responseContent); - var data = new SiteData(responseObj, _authService.UserId); if(site.Id == null) { - await base.CreateAsync(data); - site.Id = responseObj.Id; + response = await _siteApiRepository.PostAsync(request); } else { - await base.ReplaceAsync(data); + response = await _siteApiRepository.PutAsync(site.Id, request); } - return ApiResult.Success(responseObj, response.StatusCode); + if(response.Succeeded) + { + var data = new SiteData(response.Result, _authService.UserId); + if(site.Id == null) + { + await _siteRepository.InsertAsync(data); + site.Id = data.Id; + } + else + { + await _siteRepository.UpdateAsync(data); + } + } + + return response; } - public new async Task> DeleteAsync(string id) + public async Task> DeleteAsync(string id) { - var requestMessage = new TokenHttpRequestMessage + ApiResult response = await _siteApiRepository.DeleteAsync(id); + if(response.Succeeded) { - Method = HttpMethod.Delete, - RequestUri = new Uri(_apiService.Client.BaseAddress, $"/sites/{id}") - }; - - var response = await _apiService.Client.SendAsync(requestMessage); - if(!response.IsSuccessStatusCode) - { - return await _apiService.HandleErrorAsync(response); + await _siteRepository.DeleteAsync(id); } - await base.DeleteAsync(id); - return ApiResult.Success(null, response.StatusCode); + return response; } } } diff --git a/src/App/Services/SyncService.cs b/src/App/Services/SyncService.cs new file mode 100644 index 000000000..46e05629d --- /dev/null +++ b/src/App/Services/SyncService.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Data; + +namespace Bit.App.Services +{ + public class SyncService : ISyncService + { + private readonly IFolderApiRepository _folderApiRepository; + private readonly ISiteApiRepository _siteApiRepository; + private readonly IFolderRepository _folderRepository; + private readonly ISiteRepository _siteRepository; + private readonly IAuthService _authService; + + public SyncService( + IFolderApiRepository folderApiRepository, + ISiteApiRepository siteApiRepository, + IFolderRepository folderRepository, + ISiteRepository siteRepository, + IAuthService authService) + { + _folderApiRepository = folderApiRepository; + _siteApiRepository = siteApiRepository; + _folderRepository = folderRepository; + _siteRepository = siteRepository; + _authService = authService; + } + + public async Task SyncAsync() + { + // TODO: store now in settings and only fetch from last time stored + var now = DateTime.UtcNow.AddYears(-100); + + var siteTask = await SyncSitesAsync(now); + var folderTask = await SyncFoldersAsync(now); + + return siteTask && folderTask; + } + + private async Task SyncFoldersAsync(DateTime now) + { + var folderResponse = await _folderApiRepository.GetAsync(); + if(!folderResponse.Succeeded) + { + return false; + } + + var serverFolders = folderResponse.Result.Data; + var folders = await _folderRepository.GetAllByUserIdAsync(_authService.UserId); + + foreach(var serverFolder in serverFolders.Where(f => f.RevisionDate >= now)) + { + var data = new FolderData(serverFolder, _authService.UserId); + var existingLocalFolder = folders.SingleOrDefault(f => f.Id == serverFolder.Id); + if(existingLocalFolder == null) + { + await _folderRepository.InsertAsync(data); + } + else + { + await _folderRepository.UpdateAsync(data); + } + } + + foreach(var folder in folders.Where(localFolder => !serverFolders.Any(serverFolder => serverFolder.Id == localFolder.Id))) + { + await _folderRepository.DeleteAsync(folder.Id); + } + + return true; + } + + private async Task SyncSitesAsync(DateTime now) + { + var siteResponse = await _siteApiRepository.GetAsync(); + if(!siteResponse.Succeeded) + { + return false; + } + + var serverSites = siteResponse.Result.Data; + var sites = await _siteRepository.GetAllByUserIdAsync(_authService.UserId); + + foreach(var serverSite in serverSites.Where(s => s.RevisionDate >= now)) + { + var data = new SiteData(serverSite, _authService.UserId); + var existingLocalSite = sites.SingleOrDefault(s => s.Id == serverSite.Id); + if(existingLocalSite == null) + { + await _siteRepository.InsertAsync(data); + } + else + { + await _siteRepository.UpdateAsync(data); + } + } + + foreach(var site in sites.Where(localSite => !serverSites.Any(serverSite => serverSite.Id == localSite.Id))) + { + await _siteRepository.DeleteAsync(site.Id); + } + + return true; + } + } +} diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index c09c7e4ba..56bd49a93 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -13,6 +13,7 @@ using Bit.iOS.Services; using Plugin.Settings; using Plugin.Connectivity; using Acr.UserDialogs; +using Bit.App.Repositories; namespace Bit.iOS { @@ -48,15 +49,23 @@ namespace Bit.iOS var container = new UnityContainer(); container - .RegisterType(new ContainerControlledLifetimeManager()) + // Services .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) - .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) - .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + // Repositories + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + .RegisterType(new ContainerControlledLifetimeManager()) + // Other + .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager()) .RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager()); diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist index ffbb5beaa..a27212cb0 100644 --- a/src/iOS/Info.plist +++ b/src/iOS/Info.plist @@ -26,7 +26,7 @@ CFBundleDisplayName bitwarden CFBundleIdentifier - com.bitwarden.bitwarden + com.bitwarden.vault CFBundleVersion 1.0 CFBundleIconFiles diff --git a/src/iOS/Resources/fa-cogs.png b/src/iOS/Resources/fa-cogs.png new file mode 100644 index 0000000000000000000000000000000000000000..436f9683f8e80ea9d4dcd283fdc99e6567037cbb GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EaHVU{nk632_B-M*+bQV7vC^BT!Ld zNswPK10xeN3o9Et2PY3NzmSNin7D+bjJ&d%x|X(%o|(CgqmzrPyQi18e_&WdY+QVH zZhm2Lc}+u8Yg^yMDYND-Sg~sL`VCvQ?%H?Y;L+nJPMte{`RdJE4<0>v@$&u0&tHH3 z{##_Y{u$7+m!2+;Arg{H4^|p86*3%u_3+$6sTgfJe}@pZpierFrZp(} z^xm1nn|sx*`IF9v+z2y0Q|0-U$NOev%&gX&Y= zJSh>A***svc$R#5`Y72w==-zRvu0+zJ9g!ngvRUX&!;=yei>qYS#`B@{a4Oa>x_e+ Smb9mVqQTSE&t;ucLK6UPw6g60 literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-cogs@2x.png b/src/iOS/Resources/fa-cogs@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5c927a1d90aa13c4ae30db01f057731342aac7 GIT binary patch literal 1165 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv9xeFg6GHgt!8^qhMe|fbmWI8K4Uq zN`m}?85o(ESy` z?m2Mq@X_O^&YU}c@$%K{H*ej!_u$F17cXCb{`&pr@4roQhdwYcFj;!KIEF|_UOo6b z-n&rd_{Z;-o44wGk52K>DHrmc=%Tl?q=Z+xW?a^V)uT3s(NsFpYLJQ zy0Q~DoxSHy5)Uw6+r}9sF@4MQh}}v5td*{woH+5M)4|NMW^D+-BUOcFE`^ zr;~Wfx##5qnPT_ zRAmp=ya|eZ_SS3t6tzjy_*g!#4n5>^@<&To(VsPEjMOG%eqSn-7IaKKPtLgaypP)y1 zQAvRlimIBrrk1v@p1y&hk%_69 zg_X69t)0E2le3GPyQi18kFS3~P)KN4M08ASTw+pkN?LkGc1~`7L1A%8c|~MfQrwg=zu%w%-eGf?4k zT#`CXYQof=M;3*71bR+;BE%IqMYY>$DF<7dg23z(T8D++$KSGjK5u6AyPm^;+RoeY z$K6_b;3>P9%EjN#9J>o&SyqZ)(ob0vrjXsQQOo>5W~pGxTeGQ?*M|oEl#Q&OT-QEf zZ@P(Yg7~WptM)HWT@GDUUK`c3r$|k>Cam>e$NVDVlsXW=)!DU$>kX5U&}@OE*Ua^im> zYZjUNLBD1AUZ?qwB`Jl}Ur{)HB zh1Tvo)$+V|`8JKUA}qU-Ld2XWFl`RGwu`AiG{4BbySgDDkX3eZE5n;aE3Gc4j?H2- z586-Ix7?)Uwc^s%1})oW99-4#xOSG`fsms-aqT94msM)?irvK(re0bvV#oZ&$85g+ zeH%}I^C@Op)BS2(m-5;P9=m&+ed!Oo$2l6tQ|eeRcx8&&rLT;2I#hmT)k2o_lVk)L zjtHE5?fB!&#j8ArOLr|>Ao|7jZ0i)`&+dvsE>Auzf4jKEw(Yz`?A_j}7iB!eV#Ih3 zo5(brmo_O;mX%8SApF`yK~}12!V^ATkM=e(m7R{vmli%`n{;OhagH(q=3* zN7Q&dmen$S=+%p?pQl{w3p;O^ zxa`*hp`}+3Jy3}K%;<4VB$nCnNNc+1KUua5o@==s&M};v%8`?m{=mxbteew2i`lF) zyjS!NDXuFx#g>y52udCmYnU|LSJtvDbzs=Aa_XT_pR;bOlCqwDxN&9HgIx|lj=)v} zn?>u2XI%;tT=J*X>+<&3EO7}9r*go5Hw-`aTL=X1rUCqN~rr>mdKI;Vst0FMU;{Qv*} literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-lock.png b/src/iOS/Resources/fa-lock.png new file mode 100644 index 0000000000000000000000000000000000000000..00856ab8546ab6ebef82343d175db37819c2de05 GIT binary patch literal 388 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6T>(BJu0VQ_0AVH$eV|ixOM?7@ z85o&ZSUI_Qghj+9WEGUvwG1q*9Yf<%a!SfuJEzZEuxRmyorllgx%cAr*WZ7a7$x@t zHP(2#IEF}spS^fn=%9g!+d~#d&8}&SuKc{R?t8uM;*%khW)@}NeN(^hw(gPZzfRv( zl>N19k=EqCv>8Rw%L`<^CF2yf{Wuf6H~;y$w~KQLx*5{tck8^mO%eS?83{1OS^O BWqSYs literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-lock@2x.png b/src/iOS/Resources/fa-lock@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..071a2073295a21f705f5b9fc3ee38d470d3ee6eb GIT binary patch literal 587 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv81SO94J1u0VR2fh+5xZUMdIS`y?J z%)rRR!pg?M$;HhhEFvx?qoAy=simWDVqs}*XYb_Z>E$039v7dKl9O9lT2a~1+CO{g zhD|&79X@*e)WrwSUcCSK{pat$hYLfqfd)SIba4!^IQ@3=b-pG85eLTa_bdvu-|hYG z@caM%lsy?m+O1hNmkRDxz4M&BZKXm@t9EFP$>~kMdqd_;Woyd+MjZ7;ZVeD#)Sk(l$%S$1i6t}dVY+^W9yYsb~9uVQz@oy-~CVwybp zwM9eb3Z8%0+%2sAvZ6fNjc0dHra6PAWEA@kEnU$Ga+5zl&6vp|X>z3QgJje($F_e8 zeOrz_c<$V#dBRh@EqZ2w#)68o-TdqQb31={>fGEY``>5J&0|@%zu;fA7yX z-U*D73{Mxwkch)?XM{%|G7xFA);Se$r1#Oi#VZuLgulB4vb1D<{$Jk~uD({$GFM>z znev(6ALnwaRtEY0zWl{+%^Nw9;@2-T(&zg>dDXIPe|J#-O~>_vS8DR=aLUJ7b@{hWwo2nWVc4-znkF{`^DP9Kj+M5e_+0pDZN@p;N+YQ zZIMcILofcfsLAR%G%e>o31PcQ8QAE%nyt=9m`e*SCJ1D-GK& kU(qnTh=p%cLBxZ6{hjqZ{Q4|U13)hKboFyt=akR{05jx9(*OVf literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-plus@2x.png b/src/iOS/Resources/fa-plus@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..87f33d4d205628f8245cccf579c5d4c4c076b470 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv81S@Bp6>S0GIbz&CwzF3?<=k|4ie z21Zs+0TCsA3p)pQ-?YrOiHp{6*tBQg<$LeH{%(%APykfd?djqeV$u8dvL|1I0*^!B zoWdiO2k!l!<76^zYVISq-ulRU>s&jcSzS}Lm%85$I2`Do%$jc4$$mt$)BVWJe*2EU zD#fz|ub;EHyIw1*AvFE{?x&FqnIBFaU3xHv`>OZHBS%{gT3&v2U|oRO;>pLJ-<0In xs=3tp@5jd-j&mkN%q#oosq^Wt`I=mp+pEpq9}m5u)xrr56;D?`mvv4FO#p;OW@-Qc literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-plus@3x.png b/src/iOS/Resources/fa-plus@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0240071db1151b4fd0cc1d626d980da2f3e1adf6 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYgdr@&kNAT!Az_LGG))Zb17~N`m}? z85o(_1;nK^bo33a>>Qk&`Fs9FZuXmJvm_KFSNz}gb=uVf6FW7|#;NvgbT1J9c~#*G zYw=Y3t*>14Ji}8LEo)itQ!<5PWq*bi&Z&b!cQ|JCPxO> zi?^AT)dY^}Bs(2!uuKqhW#P&He|n>EXieZ#%bwJ2=c_M0&lElVgmc@kF1u@<@2V<% zdoA`%FX&5&%{zB?jm-a3hgW%3%|1H2VgmDwbyrRbtLR!DShYl`G4mwztV_CqfpP2x znOPF2mi9f|-!*gdu?r>Bd>P|EJ?o5HTI+C+^J3c)CWYrcD?aGPCY*dZ?W5T5OXpRl zHO;=;dg|WdHL@}bP0l+XkK-IJBw literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-refresh@2x.png b/src/iOS/Resources/fa-refresh@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..138dd599c6f9c8f6d7090894ae7ceb042679bb86 GIT binary patch literal 960 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv9xeFa`(sgt!8^qky_0u$Qwb8)#xd zNswPK10xd)D;qlpCpRx2zmSNSgrt;=oV~P2`u*qczrSOqumYoYji-xah=kBxjmyT*>qN9&}Hzna40 zw3F?>hl0Yao{IH)y-(OawZ8nNug1x8&)`e(JvSBO?e23MikcUSN4{92Wy*AE!RZ4` zKH(C=6SR2lIC=FNGR}V-xKe4UTW>@Dyi?m)gL|zQ@2gFfoH5V9kwIe7rumJE=T)Q+ zuxfD|L|R{2dnkSGtS`PP6O`1{k{^rxGoE6%?)bHLibsF$dH1`BWgnZc-+?b3UW|5X z6_yfjn7wp8uIzklFmnvpvn|__c$tQbV-jrHze z=~ZX?}-EP70g2Dg5oTurN`;=TelF{r5}E+BHxNYt literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-refresh@3x.png b/src/iOS/Resources/fa-refresh@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..4558159448ecdf5b646cbb5f7fb15324f0f6eced GIT binary patch literal 1269 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYnNIz}O$)6XFWwj)K7x0{>?;F95ox zr6kBNn1PXrnT3^&or9B$n}?T=Ur z!q(2g(aGJ@%iGt_KO{6FIyOErDJ>%>H@~2;w7jCauD+qExwW&qr*HbKxeFF8U9ook zhD}?x?%cEQz`?^ukDoYm{^Hf^H*ei}@a)Cwx9>lG{`&pr@4r&Dmvk5ym?nCfyRn#m!>9Jmks3t;Rl;oO~L}^FIsf0i%ZOji>0W3$Lvji_o^Bk z?vb})f335v^!dGa>&|WKmAd9|-~7uekK;bCB8}G$cpZp#{S{5+s;if{HKI0r*Zzs`EzL35)*}q z>8%adzwx~OqPETUycT2sIi=TT(Fwf`tM{JRDo~IUn-K8p*hSgsM<1Wb)*gPpQYoSP z?~?o1Cl^*6@vB)jrQxk=?WNy>mwrwZVlqvCv9&-dXn7Zp;EF7*OOKs}MP@CRR0%M3 z>|ZW5<;!&*ffXK?9&;*5M6z*&>{;@@APy<*#MiCMKJ zY6iX%ypnf$Be&Qo*@mE;&TXo^VqP-?cDo!;o)UPhZ>`~oTVvR`dyT&yr+nDV4Rhj>WbJaA;yY8-w9M)sb-9_a_*_gnRAzSEV}7(? zQcA?DcI~pqdt4=)$wO$*BZ6|n!v)%QFV(ZhwwW dpKNvQV!YJNW3@b-DSJU#*VEO{Wt~$(696vseC_}M literal 0 HcmV?d00001 diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index ff83fd540..6489af708 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -50,6 +50,9 @@ iPhone Developer true Entitlements.plist + 2ae5608a-6142-4e1d-9344-326d1982b392 + + none @@ -205,6 +208,42 @@ App + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +