Added icons for iOS. Broke out data access into repositories. Added syncing service.

This commit is contained in:
Kyle Spearrin 2016-05-06 00:17:38 -04:00
parent 24a5a16723
commit decd3fc24e
46 changed files with 773 additions and 150 deletions

View file

@ -15,6 +15,7 @@ using Bit.Android.Services;
using Plugin.Settings; using Plugin.Settings;
using Plugin.Connectivity; using Plugin.Connectivity;
using Acr.UserDialogs; using Acr.UserDialogs;
using Bit.App.Repositories;
namespace Bit.Android namespace Bit.Android
{ {
@ -40,15 +41,23 @@ namespace Bit.Android
var container = new UnityContainer(); var container = new UnityContainer();
container container
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) // Services
.RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager()) .RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager())
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
.RegisterType<ISecureStorageService, KeyStoreStorageService>(new ContainerControlledLifetimeManager()) .RegisterType<ISecureStorageService, KeyStoreStorageService>(new ContainerControlledLifetimeManager())
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager())
.RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager()) .RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager()) .RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteService, SiteService>(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(CrossConnectivity.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager()); .RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager());

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -7,6 +7,7 @@ namespace Bit.App.Abstractions
{ {
public interface ISiteService public interface ISiteService
{ {
Task<Site> GetByIdAsync(string id);
Task<IEnumerable<Site>> GetAllAsync(); Task<IEnumerable<Site>> GetAllAsync();
Task<ApiResult<SiteResponse>> SaveAsync(Site site); Task<ApiResult<SiteResponse>> SaveAsync(Site site);
Task<ApiResult<object>> DeleteAsync(string id); Task<ApiResult<object>> DeleteAsync(string id);

View file

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Bit.App.Abstractions
{
public interface ISyncService
{
Task<bool> SyncAsync();
}
}

View file

@ -48,8 +48,10 @@
<Compile Include="Models\Api\Request\FolderRequest.cs" /> <Compile Include="Models\Api\Request\FolderRequest.cs" />
<Compile Include="Models\Api\Request\SiteRequest.cs" /> <Compile Include="Models\Api\Request\SiteRequest.cs" />
<Compile Include="Models\Api\Request\TokenRequest.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\ErrorResponse.cs" />
<Compile Include="Models\Api\Response\FolderResponse.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\SiteResponse.cs" />
<Compile Include="Models\Api\Response\TokenResponse.cs" /> <Compile Include="Models\Api\Response\TokenResponse.cs" />
<Compile Include="Models\Api\Response\ProfileResponse.cs" /> <Compile Include="Models\Api\Response\ProfileResponse.cs" />
@ -65,13 +67,28 @@
<Compile Include="Pages\SyncPage.cs" /> <Compile Include="Pages\SyncPage.cs" />
<Compile Include="Pages\SettingsPage.cs" /> <Compile Include="Pages\SettingsPage.cs" />
<Compile Include="Properties\AssemblyInfo.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\DatabaseService.cs" />
<Compile Include="Services\FolderService.cs" /> <Compile Include="Services\FolderService.cs" />
<Compile Include="Services\Repository.cs" /> <Compile Include="Repositories\Repository.cs" />
<Compile Include="Abstractions\Services\IApiService.cs" />
<Compile Include="Abstractions\Services\IAuthService.cs" /> <Compile Include="Abstractions\Services\IAuthService.cs" />
<Compile Include="Abstractions\Services\ICryptoService.cs" /> <Compile Include="Abstractions\Services\ICryptoService.cs" />
<Compile Include="Abstractions\Services\IDatabaseService.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\SiteService.cs" />
<Compile Include="Services\AuthService.cs" /> <Compile Include="Services\AuthService.cs" />
<Compile Include="Services\CryptoService.cs" /> <Compile Include="Services\CryptoService.cs" />
@ -83,7 +100,6 @@
<Compile Include="Pages\VaultViewSitePage.cs" /> <Compile Include="Pages\VaultViewSitePage.cs" />
<Compile Include="Pages\VaultEditSitePage.cs" /> <Compile Include="Pages\VaultEditSitePage.cs" />
<Compile Include="Pages\VaultListPage.cs" /> <Compile Include="Pages\VaultListPage.cs" />
<Compile Include="Services\ApiService.cs" />
<Compile Include="Utilities\Extentions.cs" /> <Compile Include="Utilities\Extentions.cs" />
<Compile Include="Utilities\TokenHttpRequestMessage.cs" /> <Compile Include="Utilities\TokenHttpRequestMessage.cs" />
</ItemGroup> </ItemGroup>

View file

@ -0,0 +1,8 @@
namespace Bit.App.Models.Api
{
public class TokenTwoFactorRequest
{
public string Code { get; set; }
public string Provider { get; set; }
}
}

View file

@ -1,8 +1,11 @@
namespace Bit.App.Models.Api using System;
namespace Bit.App.Models.Api
{ {
public class FolderResponse public class FolderResponse
{ {
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public DateTime RevisionDate { get; set; }
} }
} }

View file

@ -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; }
}
}

View file

@ -1,4 +1,6 @@
namespace Bit.App.Models.Api using System;
namespace Bit.App.Models.Api
{ {
public class SiteResponse public class SiteResponse
{ {
@ -9,6 +11,7 @@
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public string Notes { get; set; } public string Notes { get; set; }
public DateTime RevisionDate { get; set; }
// Expandables // Expandables
public FolderResponse Folder { get; set; } public FolderResponse Folder { get; set; }

View file

@ -1,5 +1,4 @@
using System; using System;
using Bit.App.Abstractions;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -8,18 +7,21 @@ namespace Bit.App.Pages
{ {
public MainPage() 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()); var settingsNavigation = new NavigationPage(new SettingsPage());
settingsNavigation.BarBackgroundColor = Color.FromHex("3c8dbc"); var vaultNavigation = new NavigationPage(new VaultListPage());
settingsNavigation.BarTextColor = Color.FromHex("ffffff"); 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.Title = "Settings";
settingsNavigation.Icon = "fa-cogs";
Children.Add(vaultNavigation); Children.Add(vaultNavigation);
Children.Add(new SyncPage()); Children.Add(syncPage);
Children.Add(settingsNavigation); Children.Add(settingsNavigation);
} }
} }

View file

@ -1,14 +1,54 @@
using System; using System;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Xamarin.Forms; using Xamarin.Forms;
using XLabs.Ioc;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class SyncPage : ContentPage public class SyncPage : ContentPage
{ {
private readonly ISyncService _syncService;
private readonly IUserDialogs _userDialogs;
public SyncPage() 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"; 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.");
}
} }
} }
} }

View file

@ -31,21 +31,14 @@ namespace Bit.App.Pages
{ {
ToolbarItems.Add(new AddSiteToolBarItem(this)); 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 }; var listView = new ListView { IsGroupingEnabled = true, ItemsSource = Folders };
listView.GroupDisplayBinding = new Binding("Name"); listView.GroupDisplayBinding = new Binding("Name");
listView.ItemSelected += SiteSelected; listView.ItemSelected += SiteSelected;
listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(moreAction, deleteAction)); listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(this));
Title = "My Vault"; Title = "My Vault";
Content = listView; Content = listView;
NavigationPage.SetBackButtonTitle(this, string.Empty);
} }
protected override void OnAppearing() protected override void OnAppearing()
@ -122,7 +115,7 @@ namespace Bit.App.Pages
{ {
_page = page; _page = page;
Text = "Add"; Text = "Add";
Icon = ""; Icon = "fa-plus";
Clicked += ClickedItem; Clicked += ClickedItem;
} }
@ -144,12 +137,20 @@ namespace Bit.App.Pages
private class VaultListViewCell : TextCell 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>(TextProperty, s => s.Name);
this.SetBinding<VaultView.Site>(DetailProperty, s => s.Username); this.SetBinding<VaultView.Site>(DetailProperty, s => s.Username);
ContextActions.Add(moreMenuItem); ContextActions.Add(moreAction);
ContextActions.Add(deleteMenuItem); ContextActions.Add(deleteAction);
} }
} }
} }

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -1,23 +1,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using ModernHttpClient; using ModernHttpClient;
using Newtonsoft.Json; 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 = new HttpClient(new NativeMessageHandler());
Client.BaseAddress = new Uri("https://api.bitwarden.com"); 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) public async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
{ {

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -5,9 +5,9 @@ using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using SQLite; 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 TId : IEquatable<TId>
where T : class, IDataObject<TId>, new() where T : class, IDataObject<TId>, new()
{ {
@ -18,34 +18,34 @@ namespace Bit.App.Services
protected SQLiteConnection Connection { get; private set; } 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)); 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>()); return Task.FromResult(Connection.Table<T>().Cast<T>());
} }
protected virtual Task CreateAsync(T obj) public virtual Task InsertAsync(T obj)
{ {
Connection.Insert(obj); Connection.Insert(obj);
return Task.FromResult(0); return Task.FromResult(0);
} }
protected virtual Task ReplaceAsync(T obj) public virtual Task UpdateAsync(T obj)
{ {
Connection.Update(obj); Connection.Update(obj);
return Task.FromResult(0); return Task.FromResult(0);
} }
protected virtual async Task DeleteAsync(T obj) public virtual async Task DeleteAsync(T obj)
{ {
await DeleteAsync(obj.Id); await DeleteAsync(obj.Id);
} }
protected virtual Task DeleteAsync(TId id) public virtual Task DeleteAsync(TId id)
{ {
Connection.Delete<T>(id); Connection.Delete<T>(id);
return Task.FromResult(0); return Task.FromResult(0);

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -17,7 +17,7 @@ namespace Bit.App.Services
private readonly ISecureStorageService _secureStorage; private readonly ISecureStorageService _secureStorage;
private readonly ISettings _settings; private readonly ISettings _settings;
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
private readonly IApiService _apiService; private readonly IAuthApiRepository _authApiRepository;
private string _token; private string _token;
private string _userId; private string _userId;
@ -26,12 +26,12 @@ namespace Bit.App.Services
ISecureStorageService secureStorage, ISecureStorageService secureStorage,
ISettings settings, ISettings settings,
ICryptoService cryptoService, ICryptoService cryptoService,
IApiService apiService) IAuthApiRepository authApiRepository)
{ {
_secureStorage = secureStorage; _secureStorage = secureStorage;
_settings = settings; _settings = settings;
_cryptoService = cryptoService; _cryptoService = cryptoService;
_apiService = apiService; _authApiRepository = authApiRepository;
} }
public string Token public string Token
@ -110,16 +110,8 @@ namespace Bit.App.Services
public async Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request) public async Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request)
{ {
var requestContent = JsonConvert.SerializeObject(request); // TODO: move more logic in here
var response = await _apiService.Client.PostAsync("/auth/token", new StringContent(requestContent, Encoding.UTF8, "application/json")); return await _authApiRepository.PostTokenAsync(request);
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);
} }
} }
} }

View file

@ -6,70 +6,73 @@ using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Models.Data; using Bit.App.Models.Data;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using Newtonsoft.Json;
using System.Net.Http;
namespace Bit.App.Services 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 IAuthService _authService;
private readonly IApiService _apiService; private readonly IFolderApiRepository _folderApiRepository;
public FolderService( public FolderService(
ISqlService sqlService, IFolderRepository folderRepository,
IAuthService authService, IAuthService authService,
IApiService apiService) IFolderApiRepository folderApiRepository)
: base(sqlService)
{ {
_folderRepository = folderRepository;
_authService = authService; _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); 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)); var folders = data.Select(f => new Folder(f));
return Task.FromResult(folders); return folders;
} }
public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder) public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder)
{ {
ApiResult<FolderResponse> response = null;
var request = new FolderRequest(folder); 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) if(folder.Id == null)
{ {
await CreateAsync(data); response = await _folderApiRepository.PostAsync(request);
folder.Id = responseObj.Id;
} }
else 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;
} }
} }
} }

View file

@ -1,86 +1,89 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using Bit.App.Models.Data; using Bit.App.Models.Data;
using Newtonsoft.Json;
namespace Bit.App.Services 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 IAuthService _authService;
private readonly IApiService _apiService; private readonly ISiteApiRepository _siteApiRepository;
public SiteService( public SiteService(
ISqlService sqlService, ISiteRepository siteRepository,
IAuthService authService, IAuthService authService,
IApiService apiService) ISiteApiRepository siteApiRepository)
: base(sqlService)
{ {
_siteRepository = siteRepository;
_authService = authService; _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 data = await _siteRepository.GetByIdAsync(id);
var sites = data.Select(s => new Site(s)); if(data == null || data.UserId != _authService.UserId)
return Task.FromResult(sites); {
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) public async Task<ApiResult<SiteResponse>> SaveAsync(Site site)
{ {
ApiResult<SiteResponse> response = null;
var request = new SiteRequest(site); 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) if(site.Id == null)
{ {
await base.CreateAsync(data); response = await _siteApiRepository.PostAsync(request);
site.Id = responseObj.Id;
} }
else else
{ {
await base.ReplaceAsync(data); response = await _siteApiRepository.PutAsync(site.Id, request);
} }
return ApiResult<SiteResponse>.Success(responseObj, response.StatusCode); if(response.Succeeded)
}
public new async Task<ApiResult<object>> DeleteAsync(string id)
{ {
var requestMessage = new TokenHttpRequestMessage var data = new SiteData(response.Result, _authService.UserId);
if(site.Id == null)
{ {
Method = HttpMethod.Delete, await _siteRepository.InsertAsync(data);
RequestUri = new Uri(_apiService.Client.BaseAddress, $"/sites/{id}") site.Id = data.Id;
}; }
else
var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{ {
return await _apiService.HandleErrorAsync<object>(response); await _siteRepository.UpdateAsync(data);
}
} }
await base.DeleteAsync(id); return response;
return ApiResult<object>.Success(null, response.StatusCode); }
public async Task<ApiResult<object>> DeleteAsync(string id)
{
ApiResult<object> response = await _siteApiRepository.DeleteAsync(id);
if(response.Succeeded)
{
await _siteRepository.DeleteAsync(id);
}
return response;
} }
} }
} }

View file

@ -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;
}
}
}

View file

@ -13,6 +13,7 @@ using Bit.iOS.Services;
using Plugin.Settings; using Plugin.Settings;
using Plugin.Connectivity; using Plugin.Connectivity;
using Acr.UserDialogs; using Acr.UserDialogs;
using Bit.App.Repositories;
namespace Bit.iOS namespace Bit.iOS
{ {
@ -48,15 +49,23 @@ namespace Bit.iOS
var container = new UnityContainer(); var container = new UnityContainer();
container container
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager()) // Services
.RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager()) .RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager())
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
.RegisterType<ISecureStorageService, KeyChainStorageService>(new ContainerControlledLifetimeManager()) .RegisterType<ISecureStorageService, KeyChainStorageService>(new ContainerControlledLifetimeManager())
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager())
.RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager()) .RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager()) .RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager()) .RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteService, SiteService>(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(CrossConnectivity.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager()); .RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager());

View file

@ -26,7 +26,7 @@
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>bitwarden</string> <string>bitwarden</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.bitwarden.bitwarden</string> <string>com.bitwarden.vault</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>CFBundleIconFiles</key> <key>CFBundleIconFiles</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -50,6 +50,9 @@
<CodesignKey>iPhone Developer</CodesignKey> <CodesignKey>iPhone Developer</CodesignKey>
<MtouchDebug>true</MtouchDebug> <MtouchDebug>true</MtouchDebug>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<CodesignProvision>2ae5608a-6142-4e1d-9344-326d1982b392</CodesignProvision>
<CodesignResourceRules />
<CodesignExtraArgs />
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
<DebugType>none</DebugType> <DebugType>none</DebugType>
@ -205,6 +208,42 @@
<Name>App</Name> <Name>App</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </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" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup> <PropertyGroup>