From decd3fc24ec786d5d367adeb98351944f5481f68 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin <kyle.spearrin@gmail.com> 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<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) + // Services .RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager()) + .RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) .RegisterType<ISecureStorageService, KeyStoreStorageService>(new ContainerControlledLifetimeManager()) - .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) - .RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager()) .RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager()) .RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager()) .RegisterType<ISiteService, SiteService>(new ContainerControlledLifetimeManager()) + .RegisterType<ISyncService, SyncService>(new ContainerControlledLifetimeManager()) + // Repositories + .RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<ISiteRepository, SiteRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<ISiteApiRepository, SiteApiRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<IAuthApiRepository, AuthApiRepository>(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<TRequest, TResponse, TId> + where TRequest : class + where TResponse : class + where TId : IEquatable<TId> + { + Task<ApiResult<TResponse>> GetByIdAsync(TId id); + Task<ApiResult<ListResponse<TResponse>>> GetAsync(); + Task<ApiResult<TResponse>> PostAsync(TRequest requestObj); + Task<ApiResult<TResponse>> PutAsync(TId id, TRequest requestObj); + Task<ApiResult<object>> 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<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj); + Task<ApiResult<TokenResponse>> 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<FolderRequest, FolderResponse, string> + { + Task<ApiResult<ListResponse<FolderResponse>>> 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<FolderData, string> + { + Task<IEnumerable<FolderData>> 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<T, TId> + where T : class, IDataObject<TId>, new() + where TId : IEquatable<TId> + { + Task<T> GetByIdAsync(TId id); + Task<IEnumerable<T>> 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<SiteRequest, SiteResponse, string> + { + Task<ApiResult<ListResponse<SiteResponse>>> 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<SiteData, string> + { + Task<IEnumerable<SiteData>> 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<ApiResult<T>> HandleErrorAsync<T>(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<Site> GetByIdAsync(string id); Task<IEnumerable<Site>> GetAllAsync(); Task<ApiResult<SiteResponse>> SaveAsync(Site site); Task<ApiResult<object>> 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<bool> 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 @@ <Compile Include="Models\Api\Request\FolderRequest.cs" /> <Compile Include="Models\Api\Request\SiteRequest.cs" /> <Compile Include="Models\Api\Request\TokenRequest.cs" /> + <Compile Include="Models\Api\Request\TokenTwoFactorRequest.cs" /> <Compile Include="Models\Api\Response\ErrorResponse.cs" /> <Compile Include="Models\Api\Response\FolderResponse.cs" /> + <Compile Include="Models\Api\Response\ListResponse.cs" /> <Compile Include="Models\Api\Response\SiteResponse.cs" /> <Compile Include="Models\Api\Response\TokenResponse.cs" /> <Compile Include="Models\Api\Response\ProfileResponse.cs" /> @@ -65,13 +67,28 @@ <Compile Include="Pages\SyncPage.cs" /> <Compile Include="Pages\SettingsPage.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Abstractions\Repositories\ISiteRepository.cs" /> + <Compile Include="Repositories\ApiRepository.cs" /> + <Compile Include="Repositories\BaseApiRepository.cs" /> + <Compile Include="Abstractions\Repositories\IApiRepository.cs" /> + <Compile Include="Abstractions\Repositories\IFolderApiRepository.cs" /> + <Compile Include="Abstractions\Repositories\ISiteApiRepository.cs" /> + <Compile Include="Repositories\AuthApiRepository.cs" /> + <Compile Include="Abstractions\Repositories\IAuthApiRepository.cs" /> + <Compile Include="Repositories\SiteApiRepository.cs" /> + <Compile Include="Repositories\FolderApiRepository.cs" /> + <Compile Include="Repositories\SiteRepository.cs" /> + <Compile Include="Repositories\FolderRepository.cs" /> + <Compile Include="Abstractions\Repositories\IFolderRepository.cs" /> + <Compile Include="Abstractions\Repositories\IRepository.cs" /> <Compile Include="Services\DatabaseService.cs" /> <Compile Include="Services\FolderService.cs" /> - <Compile Include="Services\Repository.cs" /> - <Compile Include="Abstractions\Services\IApiService.cs" /> + <Compile Include="Repositories\Repository.cs" /> <Compile Include="Abstractions\Services\IAuthService.cs" /> <Compile Include="Abstractions\Services\ICryptoService.cs" /> <Compile Include="Abstractions\Services\IDatabaseService.cs" /> + <Compile Include="Abstractions\Services\ISyncService.cs" /> + <Compile Include="Services\SyncService.cs" /> <Compile Include="Services\SiteService.cs" /> <Compile Include="Services\AuthService.cs" /> <Compile Include="Services\CryptoService.cs" /> @@ -83,7 +100,6 @@ <Compile Include="Pages\VaultViewSitePage.cs" /> <Compile Include="Pages\VaultEditSitePage.cs" /> <Compile Include="Pages\VaultListPage.cs" /> - <Compile Include="Services\ApiService.cs" /> <Compile Include="Utilities\Extentions.cs" /> <Compile Include="Utilities\TokenHttpRequestMessage.cs" /> </ItemGroup> 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<T> + { + public ListResponse(IEnumerable<T> data) + { + Data = data; + } + + public IEnumerable<T> 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<ISyncService>(); + _userDialogs = Resolver.Resolve<IUserDialogs>(); + + 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<VaultView.Site>(TextProperty, s => s.Name); this.SetBinding<VaultView.Site>(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<TRequest, TResponse, TId> : BaseApiRepository, IApiRepository<TRequest, TResponse, TId> + where TId : IEquatable<TId> + where TRequest : class + where TResponse : class + { + public ApiRepository() + { } + + public virtual async Task<ApiResult<TResponse>> 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<TResponse>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent); + return ApiResult<TResponse>.Success(responseObj, response.StatusCode); + } + + public virtual async Task<ApiResult<ListResponse<TResponse>>> 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<ListResponse<TResponse>>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<ListResponse<TResponse>>(responseContent); + return ApiResult<ListResponse<TResponse>>.Success(responseObj, response.StatusCode); + } + + public virtual async Task<ApiResult<TResponse>> 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<TResponse>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent); + return ApiResult<TResponse>.Success(responseObj, response.StatusCode); + } + + public virtual async Task<ApiResult<TResponse>> 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<TResponse>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent); + return ApiResult<TResponse>.Success(responseObj, response.StatusCode); + } + + public virtual async Task<ApiResult<object>> 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<object>(response); + } + + return ApiResult<object>.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<ApiResult<TokenResponse>> 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<TokenResponse>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent); + return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode); + } + + public virtual async Task<ApiResult<TokenResponse>> 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<TokenResponse>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent); + return ApiResult<TokenResponse>.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<ApiResult<T>> HandleErrorAsync<T>(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<FolderRequest, FolderResponse, string>, IFolderApiRepository + { + protected override string ApiRoute => "folders"; + + public virtual async Task<ApiResult<ListResponse<FolderResponse>>> 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<ListResponse<FolderResponse>>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<ListResponse<FolderResponse>>(responseContent); + return ApiResult<ListResponse<FolderResponse>>.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<FolderData, string>, IFolderRepository + { + public FolderRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task<IEnumerable<FolderData>> GetAllByUserIdAsync(string userId) + { + var folders = Connection.Table<FolderData>().Where(f => f.UserId == userId).Cast<FolderData>(); + 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<T, TId> + public abstract class Repository<T, TId> : IRepository<T, TId> where TId : IEquatable<TId> where T : class, IDataObject<TId>, new() { @@ -18,34 +18,34 @@ namespace Bit.App.Services protected SQLiteConnection Connection { get; private set; } - protected virtual Task<T> GetByIdAsync(TId id) + public virtual Task<T> GetByIdAsync(TId id) { return Task.FromResult(Connection.Get<T>(id)); } - protected virtual Task<IEnumerable<T>> GetAllAsync() + public virtual Task<IEnumerable<T>> GetAllAsync() { return Task.FromResult(Connection.Table<T>().Cast<T>()); } - 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<T>(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<SiteRequest, SiteResponse, string>, ISiteApiRepository + { + protected override string ApiRoute => "sites"; + + public virtual async Task<ApiResult<ListResponse<SiteResponse>>> 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<ListResponse<SiteResponse>>(response); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObj = JsonConvert.DeserializeObject<ListResponse<SiteResponse>>(responseContent); + return ApiResult<ListResponse<SiteResponse>>.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<SiteData, string>, ISiteRepository + { + public SiteRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task<IEnumerable<SiteData>> GetAllByUserIdAsync(string userId) + { + var sites = Connection.Table<SiteData>().Where(f => f.UserId == userId).Cast<SiteData>(); + 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<ApiResult<TokenResponse>> 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<TokenResponse>(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent); - return ApiResult<TokenResponse>.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<FolderData, string>, 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<Folder> GetByIdAsync(string id) + public async Task<Folder> GetByIdAsync(string id) { - var data = Connection.Table<FolderData>().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<IEnumerable<Folder>> GetAllAsync() + public async Task<IEnumerable<Folder>> GetAllAsync() { - var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId).Cast<FolderData>(); + var data = await _folderRepository.GetAllByUserIdAsync(_authService.UserId); var folders = data.Select(f => new Folder(f)); - return Task.FromResult(folders); + return folders; } public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder) { + ApiResult<FolderResponse> 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<FolderResponse>(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject<FolderResponse>(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<FolderResponse>.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<SiteData, string>, 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<IEnumerable<Site>> GetAllAsync() + public async Task<Site> GetByIdAsync(string id) { - var data = Connection.Table<SiteData>().Where(f => f.UserId == _authService.UserId).Cast<SiteData>(); - 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<IEnumerable<Site>> GetAllAsync() + { + var data = await _siteRepository.GetAllByUserIdAsync(_authService.UserId); + var sites = data.Select(f => new Site(f)); + return sites; } public async Task<ApiResult<SiteResponse>> SaveAsync(Site site) { + ApiResult<SiteResponse> 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<SiteResponse>(response); - } - - var responseContent = await response.Content.ReadAsStringAsync(); - var responseObj = JsonConvert.DeserializeObject<SiteResponse>(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<SiteResponse>.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<ApiResult<object>> DeleteAsync(string id) + public async Task<ApiResult<object>> DeleteAsync(string id) { - var requestMessage = new TokenHttpRequestMessage + ApiResult<object> 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<object>(response); + await _siteRepository.DeleteAsync(id); } - await base.DeleteAsync(id); - return ApiResult<object>.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<bool> 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<bool> 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<bool> 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<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) + // Services .RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager()) + .RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) .RegisterType<ISecureStorageService, KeyChainStorageService>(new ContainerControlledLifetimeManager()) - .RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager()) - .RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager()) .RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager()) .RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager()) .RegisterType<ISiteService, SiteService>(new ContainerControlledLifetimeManager()) + .RegisterType<ISyncService, SyncService>(new ContainerControlledLifetimeManager()) + // Repositories + .RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<ISiteRepository, SiteRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<ISiteApiRepository, SiteApiRepository>(new ContainerControlledLifetimeManager()) + .RegisterType<IAuthApiRepository, AuthApiRepository>(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 @@ <key>CFBundleDisplayName</key> <string>bitwarden</string> <key>CFBundleIdentifier</key> - <string>com.bitwarden.bitwarden</string> + <string>com.bitwarden.vault</string> <key>CFBundleVersion</key> <string>1.0</string> <key>CFBundleIconFiles</key> 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_<sFV6_xNlnVrS+EuB&(u24~te*66Ie$L&- zDQfkS=NJFqq1)EK%5keTtK8K|=hl00oejD4P&M67>3+$6sTgfJe}@pZpierFrZp(} z^xm1nn|sx*`IF9v+z2y0Q|0-U$NOev%&gX&Y<zqAG@IY59A2-Q?`(={=(1ruv062H zx#R?q#$WU5B-(qHUJzByeC%-Ya6?UU!*7-yhIc<6m_0FHROUeA&Uv2?J@MRn{ls>= 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<WFIXJnvc?AT8ghj+9q-5k26qVJ~HMO*LboKNN49zVpt*mWq?HwGQ zUEJKgef$CfgF-^X!XqN1W8x~RYwGG7np)dBI=g%N`X^4AI(^2hx$_n+Ub<@ahD}>` z?m2Mq@X_O^&YU}c@$%K{H*ej!_u$F17cXCb{`&pr@4roQhdwYcFj;!KIEF|_UOo6b z-n&rd_{Z;-o44wGk52K>DHrmc=%Tl?<xqs4;`J30if`Y1UH*H{=Jzw(Z$2)p`|_dY zRs74Et!|fIuADRJZd2=&ZH{kSgr+vUJh=Wqrs^%%HNKsD8g^~^cj@;Hkt-9dc#5sB zePItVugEg$o;lI@4|7D`<PwI@n)=7jpS)sl+Gurf5T~)`V_~gTdfOwc)lxf_ELA?( zzQ$`twUOPzMM^u@AGrE1-DTdpy^K2B=KkVXv&Gwr>q=Z+xW?a^V)uT3s(NsFpYLJQ zy0Q~DoxSHy5)Uw6+r}9sF@4MQh}}v5td*{woH+5M)<pApON>4|NMW^D+-BUOcFE`^ zr;~Wfx<hl;_nNM17mfZZS?qaE;@NKX&6UfXFCDxcd6jp{e!*20MSHl^X2{yFNV7Pc zWzoMl&7#uuN#PY9_4REweX}0U2|vuyH%s)`y!8oM^JLz0FFd-^ZtH_kgXvFW4u;gs zJZmgF=~rIb*V_yWttDrkiFHa(U3Xy4dffoqB{ROy@+hb;`BLYQVyF62FyPgHCK1uy ze%$;jrNXCA-E{M|1^Z;i|2DB_;V1PE9D2vwY#Mg0vdBojA$#{cfyyn9l>##5qnPT_ zRAmp=ya|eZ_SS3t6tzjy_*g!#4n5>^@<&To(VsPEjMOG%eqSn-7IaKK<cfTnf&P;@ zzxi~wduk<!mA&F`-E?z3+kWw9yRLl;U%Br4lq1|Lw6$;MB}J-Marp4-GBSIaFm`L^ nw%#gKeO<FREGS-c`Twi>PtLgaypP)y1<F#Mu6{1-oD!M<bv$F^ literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-cogs@3x.png b/src/iOS/Resources/fa-cogs@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..da8fe3925fb2b79161eab6063bbe2a68fc6cd1b6 GIT binary patch literal 1660 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYnNI!1yx2C&U%V9R(vH1TwOwE(H2> zQAv<rFaskKGYcylI|nBhHxDl#zo3w?h^UyjgtUyDyn>RlimIBrrk1v@p1y&hk%_69 zg_X69t)0E2le3GPyQi18kFS3~P)KN4M08ASTw+pkN?LkGc1~`7L1A%8c|~<ieM3`o zYg<QWPyfV8Q>M<EJ$K%MMN3z$Ub}w7rY&2y@7%R#-+_aNj~+j9=G^&<m#<#GdF#%- z2aldSd-3x1+xH(ofBpXR_uu=+XFOnFU|!(q;us<!`S#$;_>fQrwg=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$<tt$XVFhx26l0eJXhw5jW$`9A0our z%{6jG9B%D)%S`@|&ApN@;5L81?^?li{wr>NVDVlsXW=)!DU$>kX5U&}@OE*Ua^im> zYZjUNLBD1AUZ<aX%4oJ#Xou33oFh~7C&`><c9Gv|&7?6$VP^=(>?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{;Oh<I4*dx>agH(q=3* zN7Q&dme<anclbl)?6%(?`T0MO+g~)*OAwe`^7m%VCLJD4hHwVWWiPW@7tUR0m~pYa zJJDm|%85ZLmcDCQ3l~kj#UAkOj*?{}qd>n$S=+<O=XXpj(s<f0Dc<;LN|1Sjnb+qJ zFPEq9T)c8+^2~XOj4NADojt!UFthO`SNRntO|ShLJjZ7o%=Az;={=-yZRWu;KABJx z+4hz5C4D$7ZoRm%uX^ood#kz@O_Rg2x^0SYHc45v9bzc@{c?roW>%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<heXC#zh)CoFJ0%=6f%iPHimtZF-@HFfii)~Z=j z6Kps7T<&tXc5mT*!IZV_rzVE;<p#fUy!LNV2>*g<muyqkKa%_@y@ao5q0sI`(=S{e gi=K0H>o5Hw-`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~KQ<zIr)az0<lgPLRd)d&08oENu1_ zTUswKd&Tkl#0Tc(mki?8);~U!{JE?3@zW@i?@aoc>Lx*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_;W<RZ?&a1<bB)siTn6gOt z(LK5X%%+>oyd+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<E`mmf<xl&xNlq$^=@ze c;`py-=E5KNB|dsR@&P3YPgg&ebxsLQ0CJz37XSbN literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-lock@3x.png b/src/iOS/Resources/fa-lock@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eed9ef096a46b99f2eb2506e25a90fb079b712e3 GIT binary patch literal 820 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYnNIz$hEw6XFWwjsjdEaP(eNB2X@_ zB*-tAfsu)sg_Vt+gOi(wPe4dmL{v;%LP|zXQAJf<OIydl$i%|h*3Qw%*~K?7C@efO zIwm$TIW0XqC%3Soy1uEowR6hsxl32CUB6-L_MHcho;q{x;^nK??>&0|@%zu;fA7yX z-U*D73{Mxwkch)?XM{%|G7xFA);Se$r1#Oi#VZuLgulB4vb1D<{$Jk~uD({$GFM>z znev(6ALnwaRtEY0zWl{+%^Nw9;@2-T(&zg>dDXIP<NPUNJaNKm<;g)zpJx;lCAOOL zT;VX6U!2JM(qE~=TKt@W^8}q%!=BwUno^p0FREoJ_$dVhe!F^qlcx9eHBX*(=YI(0 zH(2viZh2*X#jm^4+Y6g7ev(@{PyRty(|V3oHx?ZA{#NI&dCOUpcZIFA&ZYN_TQ}U4 zSQy^5{cs0!fly$|!jdD4Z##5%JQDLVl-t@6CcyefF4V4$rM<$)=Sy|QExv+}TG!a8 z>e|J#-O~>_vS8DR=aLUJ7b@{hWwo2nWVc4-znkF{`^DP9Kj+M5e_+0pDZN@p;N+YQ zZIMcILofcfsLAR%G%<X`YBzSaB#qV}2rBV^_^-}<^(pCDXXa&{*5<p(e0uVlg-P3F ltKO_SQ*-6Bn|XY34fB;1$-efp&hG`qyr-+5%Q~loCIH<Q<LCeY literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-plus.png b/src/iOS/Resources/fa-plus.png new file mode 100644 index 0000000000000000000000000000000000000000..770cf597b88595d77f6f15f3287848a6f22d3855 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6mH|E?u0WbRu%|4*7N}FGB*-tA zfsvEj$ja8iHz{-CvYq=b-}`z!CdM2n8|LZa7$OnA_nal)0R<kHz(ak9#qR&#u<_+0 zLCv32zuxIF{J+y_ivELBli1&#ddYKp#+H>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&<I*zACr+KcW!Ij4hfmyl|MBb3-+yO*NzVtW-{<M#7!q;# z?NxWaW(NV6i-(xip3mu>`Fs9FZuXmJvm_KFSNz}gb=uVf6FW7|#;NvgbT1J9c~#*G zYw=Y3t*>14Ji}8LEo)itQ!<5PW<nFE+P{_^Hyn3lE^*no_v8KQS7(aK*D^48zcP_v z(9(6XVo<YHVqBnpqNlT1sN7bG^}=gG!G;L$2o?qrsY9>q*bi&Z&b!<j*MEDqm3ies zpn;vA1$b4NQWq_0S?g2bGDH6q%U@o;GLi4|<1ItQ`ra@;?O{&&A0;6T3TaPQKbLh* G2~7Y6!+MJV literal 0 HcmV?d00001 diff --git a/src/iOS/Resources/fa-refresh.png b/src/iOS/Resources/fa-refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..14053d2b5e49d62556e18f812e4b0a4e532b63c3 GIT binary patch literal 595 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6Zv%WnT!Hk+0eNQEZUcSmR}$nG z%)rRR%)-jS&BMzlC?qT*DlQ=<uc)k|rmm@_t)r)JXl8C<Yv-1dTUcCLUQyH9(?4a# zthw_RtX;oh)7AqAkDfYn?()^^Hy^xs`TFhqpTD=as4N7UeB9H;F+@Ug>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@}<cdZjN-Sb`ha8dL{?@on-kyaH_JoBVQkNskPVr=GqL*?oOP}1;p L^>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<dPvYM8*j;@}*p{bdLrInq7le3GfyO(cZ zP)KBSOnhQWdVWD+aY<=iLu*HOU;otUGiJ?Ruz2Z;wd*%*+PZVkfuqMyoH=*#`ki|Z z9zA*X;^phN?>~P2`u*qczrSOqumYoYji-xah=k<ZgOTBv9T?aytUoDosmUvPs;jGD zph)<JchjO)?taj9B8*#P?X)Mq|NEz@c^c2$^5cP}U8VlNuGn1_%Y$lT=4nP)%-Qj) zGxpwXP3~IWFdMb4r_GqYmp@;y`(EO${?ilYP1<r{YHVWu>BxjmyT*>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_yfjn7wp8uIzklFm<I(i_6)A8b0o}TCN^FrG1B|++DPZsdZzvLF#^^fR;luth<+7 z;$zkqT#&c3VaDbY7tEy@)%MOm>nvpvn|__c$tQbV-j<a!-CBZPFFRegXY-3i>rHze z=~ZX<D_&Fl-p1JR{LRy5=CjOZ?F)L=UffbE=UK@;adPSOS>?}-EP70<e&0N{?|1Fz euXl}^{-4QT>g2Dg5oTurN`;=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<O`L{v;%LP}ajR!&|)Nm)fzO<hY{M^{hZ(Ad<> 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?nC<IEF|_ zzCD;8EF8$ddSSP>fyRn#m!>9Jmks3t;Rl;oO~L}^FIsf0i%ZOji>0W3$Lvji_o^Bk z?vb})f335v^!dGa>&|WKmAd9|-~7uekK;bCB8<bFUkj?X)$HHFS=tqQGVo!g^`2R7 z@5&W?cJDdC@lr*`ZCVO@)3?8mxQ>}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*&>{;@<mCaQ7v_hj%^Sb-I0mhqn1T~}=ua?bno0iBV zmfP5O8z?qKV8fRSfnClkRlOw{PbXUi1<YPDr9sudopEcWmoG!c!elPNV;ZG04Q3~2 z?F@6d;J%sXr*X&sf4{dM<@?h1=9+-(`&VnNRvf%EZTS%&WhI-%Z>@APy<*#MiCMKJ zY6iX%ypnf$Be&Qo*@mE;&TXo^VqP-?cDo!;o)UPh<yIS$Y;}Mer?8;f%NfqBhkaZm zmb{kHl1NfIB)E&qN=w3|i6d5Z7hAy51wYL+<}N?3&l}1U`K~?Yuyd8H3$N-uHHC|_ z7lw10dHHTpdho%dw(iA-z{Fkm8+`AV7SF3|6|-2e?T20IS(#nNc84cilH0LE?e2#K zxi45>Z>`~oTVvR`dyT&yr+nDV4Rhj>WbJaA;yY8-w9M)sb-9_a_*_gnRAzSEV}7(? zQcA?DcI~p<Mlbt*M>qdt4=)$wO$*BZ6|n!v)%QFV<qKI?U)fRr;NF$~GeL>(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 @@ <CodesignKey>iPhone Developer</CodesignKey> <MtouchDebug>true</MtouchDebug> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements> + <CodesignProvision>2ae5608a-6142-4e1d-9344-326d1982b392</CodesignProvision> + <CodesignResourceRules /> + <CodesignExtraArgs /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' "> <DebugType>none</DebugType> @@ -205,6 +208,42 @@ <Name>App</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-refresh.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-refresh%403x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-refresh%402x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-cogs.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-cogs%402x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-cogs%403x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-lock.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-lock%402x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-lock%403x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-plus.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-plus%402x.png" /> + </ItemGroup> + <ItemGroup> + <BundleResource Include="Resources\fa-plus%403x.png" /> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <PropertyGroup>