1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-25 12:05:59 +01:00

Added account deletion feature on settings (#1621)

* Added account deletion feature on settings

* Disabled using Microsoft.AppCenter.Crashes for FDroid

* Moved drawable on Android.csproj to be with the others

Co-authored-by: Federico Maccaroni <fmaccaroni@bitwarden.com>
This commit is contained in:
Federico Maccaroni 2021-11-24 16:09:39 -03:00 committed by GitHub
parent 833103b2a0
commit 9fdf2ada6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 884 additions and 20 deletions

View File

@ -172,6 +172,7 @@
<AndroidResource Include="Resources\drawable\cog.xml" /> <AndroidResource Include="Resources\drawable\cog.xml" />
<AndroidResource Include="Resources\drawable\icon.xml" /> <AndroidResource Include="Resources\drawable\icon.xml" />
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" /> <AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" />
<AndroidResource Include="Resources\drawable\ic_warning.xml" />
<AndroidResource Include="Resources\drawable\id.xml" /> <AndroidResource Include="Resources\drawable\id.xml" />
<AndroidResource Include="Resources\drawable\info.xml" /> <AndroidResource Include="Resources\drawable\info.xml" />
<AndroidResource Include="Resources\drawable\list_item_bg.xml" /> <AndroidResource Include="Resources\drawable\list_item_bg.xml" />

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="19dp"
android:viewportWidth="22"
android:viewportHeight="19">
<path
android:fillColor="#175DDC"
android:pathData="M19.16 18.71H2.64c-0.36 0-0.72-0.09-1.03-0.27c-0.31-0.2-0.57-0.46-0.74-0.78c-0.18-0.32-0.27-0.67-0.27-1.04c0-0.36 0.1-0.72 0.28-1.03L9.14 1.1C9.32 0.76 9.58 0.5 9.89 0.32c0.3-0.18 0.65-0.28 1-0.28c0.36 0 0.7 0.1 1.02 0.28c0.3 0.18 0.56 0.44 0.74 0.75l8.26 14.51c0.18 0.31 0.28 0.67 0.28 1.03c0 0.37-0.09 0.72-0.26 1.04c-0.18 0.32-0.44 0.59-0.75 0.78c-0.31 0.18-0.66 0.28-1.02 0.27zM10.9 1.38c-0.13 0-0.26 0.04-0.38 0.1c-0.11 0.07-0.2 0.16-0.27 0.28L1.99 16.27c-0.07 0.11-0.1 0.24-0.1 0.36C1.9 16.76 1.92 16.9 2 17c0.06 0.12 0.16 0.22 0.27 0.3c0.12 0.06 0.25 0.1 0.38 0.1h16.52c0.13 0 0.26-0.04 0.37-0.1c0.12-0.08 0.21-0.18 0.28-0.3c0.06-0.1 0.1-0.23 0.1-0.36c0-0.12-0.04-0.25-0.1-0.36l-8.26-14.5c-0.07-0.13-0.17-0.22-0.28-0.29c-0.11-0.06-0.24-0.1-0.37-0.1zm0 11.42c-0.17 0-0.34-0.07-0.46-0.2c-0.12-0.12-0.19-0.29-0.19-0.46v-6.1c0-0.18 0.07-0.35 0.2-0.47c0.11-0.13 0.28-0.2 0.45-0.2c0.17 0 0.33 0.07 0.45 0.2c0.12 0.12 0.19 0.3 0.19 0.47v6.1c0 0.17-0.07 0.34-0.19 0.47c-0.12 0.12-0.28 0.2-0.45 0.2zm0 3.3c0.42 0 0.76-0.36 0.76-0.8c0-0.43-0.34-0.78-0.76-0.78c-0.43 0-0.77 0.35-0.77 0.79c0 0.43 0.34 0.79 0.77 0.79z"/>
</vector>

View File

@ -7,6 +7,8 @@ namespace Bit.App.Abstractions
string[] ProtectedFields { get; } string[] ProtectedFields { get; }
Task<bool> ShowPasswordPromptAsync(); Task<bool> ShowPasswordPromptAsync();
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task<bool> Enabled(); Task<bool> Enabled();
} }

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.Accounts.DeleteAccountPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:DeleteAccountViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:DeleteAccountViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Padding="20, 30" Spacing="0">
<Image
Source="ic_warning"
WidthRequest="28"
HeightRequest="25"
HorizontalOptions="Start" />
<Label
Text="{u:I18n DeletingYourAccountIsPermanent}"
HorizontalOptions="Start"
StyleClass="text-body"
Margin="0,15,0,0"/>
<Label
Text="{u:I18n DeleteAccountExplanation}"
HorizontalOptions="Start"
Margin="0,6,50,0"
Opacity="0.6" />
<Button
Text="{u:I18n Cancel}"
StyleClass="btn-primary"
HorizontalOptions="Start"
Margin="0,26,0,0"
Padding="16,6"
CornerRadius="2"
TextTransform="Uppercase"
Clicked="Close_Clicked" />
<Button
Text="{u:I18n DeleteAccount}"
StyleClass="btn-secondary"
TextColor="#99000000"
HorizontalOptions="Start"
Margin="0,12,0,0"
Padding="16,6"
CornerRadius="2"
TextTransform="Uppercase"
Clicked="DeleteAccount_Clicked"/>
</StackLayout>
</ContentPage.Content>
</pages:BaseContentPage>

View File

@ -0,0 +1,33 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Pages.Accounts
{
public partial class DeleteAccountPage : BaseContentPage
{
DeleteAccountViewModel _vm;
public DeleteAccountPage()
{
InitializeComponent();
_vm = BindingContext as DeleteAccountViewModel;
_vm.Page = this;
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
private async void DeleteAccount_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.DeleteAccountAsync();
}
}
}
}

View File

@ -0,0 +1,84 @@
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
#if !FDROID
using Microsoft.AppCenter.Crashes;
#endif
namespace Bit.App.Pages
{
public class DeleteAccountViewModel : BaseViewModel
{
readonly IApiService _apiService;
readonly IPasswordRepromptService _passwordRepromptService;
readonly IMessagingService _messagingService;
readonly ICryptoService _cryptoService;
readonly IPlatformUtilsService _platformUtilsService;
readonly IDeviceActionService _deviceActionService;
public DeleteAccountViewModel()
{
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
PageTitle = AppResources.DeleteAccount;
}
public async Task DeleteAccountAsync()
{
try
{
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle, AppResources.Ok);
return;
}
var (password, valid) = await _passwordRepromptService.ShowPasswordPromptAndGetItAsync();
if (!valid)
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.DeletingYourAccount);
var masterPasswordHashKey = await _cryptoService.HashPasswordAsync(password, null);
await _apiService.DeleteAccountAsync(new Core.Models.Request.DeleteAccountRequest
{
MasterPasswordHash = masterPasswordHashKey
});
await _deviceActionService.HideLoadingAsync();
_messagingService.Send("logout");
await _platformUtilsService.ShowDialogAsync(AppResources.YourAccountHasBeenPermanentlyDeleted);
}
catch (ApiException apiEx)
{
await _deviceActionService.HideLoadingAsync();
if (apiEx?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(apiEx.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred);
}
}
catch (System.Exception ex)
{
await _deviceActionService.HideLoadingAsync();
#if !FDROID
Crashes.TrackError(ex);
#endif
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
}
}
}
}

View File

@ -1,10 +1,11 @@
using System.ComponentModel; using System.ComponentModel;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Utilities;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Pages.Accounts;
using Bit.App.Resources;
using Bit.Core.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -134,6 +135,10 @@ namespace Bit.App.Pages
{ {
await _vm.LogOutAsync(); await _vm.LogOutAsync();
} }
else if (item.Name == AppResources.DeleteAccount)
{
await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage()));
}
else if (item.Name == AppResources.LockNow) else if (item.Name == AppResources.LockNow)
{ {
await _vm.LockAsync(); await _vm.LockAsync();

View File

@ -490,7 +490,8 @@ namespace Bit.App.Pages
new SettingsPageListItem { Name = AppResources.Options }, new SettingsPageListItem { Name = AppResources.Options },
new SettingsPageListItem { Name = AppResources.About }, new SettingsPageListItem { Name = AppResources.About },
new SettingsPageListItem { Name = AppResources.HelpAndFeedback }, new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
new SettingsPageListItem { Name = AppResources.RateTheApp } new SettingsPageListItem { Name = AppResources.RateTheApp },
new SettingsPageListItem { Name = AppResources.DeleteAccount }
}; };
GroupedItems.ResetWithRange(new List<SettingsPageListGroup> GroupedItems.ResetWithRange(new List<SettingsPageListGroup>
{ {

View File

@ -3719,6 +3719,36 @@ namespace Bit.App.Resources {
} }
} }
public static string DeleteAccount {
get {
return ResourceManager.GetString("DeleteAccount", resourceCulture);
}
}
public static string DeletingYourAccountIsPermanent {
get {
return ResourceManager.GetString("DeletingYourAccountIsPermanent", resourceCulture);
}
}
public static string DeleteAccountExplanation {
get {
return ResourceManager.GetString("DeleteAccountExplanation", resourceCulture);
}
}
public static string DeletingYourAccount {
get {
return ResourceManager.GetString("DeletingYourAccount", resourceCulture);
}
}
public static string YourAccountHasBeenPermanentlyDeleted {
get {
return ResourceManager.GetString("YourAccountHasBeenPermanentlyDeleted", resourceCulture);
}
}
public static string InvalidVerificationCode { public static string InvalidVerificationCode {
get { get {
return ResourceManager.GetString("InvalidVerificationCode", resourceCulture); return ResourceManager.GetString("InvalidVerificationCode", resourceCulture);

View File

@ -2093,6 +2093,21 @@
<data name="DisablePersonalVaultExportPolicyInEffect"> <data name="DisablePersonalVaultExportPolicyInEffect">
<value>One or more organization policies prevents your from exporting your personal vault.</value> <value>One or more organization policies prevents your from exporting your personal vault.</value>
</data> </data>
<data name="DeleteAccount" xml:space="preserve">
<value>Delete Account</value>
</data>
<data name="DeletingYourAccountIsPermanent" xml:space="preserve">
<value>Deleting your account is permanent</value>
</data>
<data name="DeleteAccountExplanation" xml:space="preserve">
<value>Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue?</value>
</data>
<data name="DeletingYourAccount" xml:space="preserve">
<value>Deleting your account</value>
</data>
<data name="YourAccountHasBeenPermanentlyDeleted" xml:space="preserve">
<value>Your account has been permanently deleted</value>
</data>
<data name="InvalidVerificationCode" xml:space="preserve"> <data name="InvalidVerificationCode" xml:space="preserve">
<value>Invalid Verification Code.</value> <value>Invalid Verification Code.</value>
</data> </data>

View File

@ -1,7 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions;
using System; using System;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -22,23 +22,23 @@ namespace Bit.App.Services
public async Task<bool> ShowPasswordPromptAsync() public async Task<bool> ShowPasswordPromptAsync()
{ {
if (!await Enabled()) return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
{ }
return true;
}
Func<string, Task<bool>> validator = async (string password) => public async Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync()
{ {
// Assume user has canceled. return await _platformUtilsService.ShowPasswordDialogAndGetItAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
if (string.IsNullOrWhiteSpace(password)) }
{
return false;
};
return await _cryptoService.CompareAndUpdateKeyHashAsync(password, null); private async Task<bool> ValidatePasswordAsync(string password)
{
// Assume user has canceled.
if (string.IsNullOrWhiteSpace(password))
{
return false;
}; };
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator); return await _cryptoService.CompareAndUpdateKeyHashAsync(password, null);
} }
public async Task<bool> Enabled() public async Task<bool> Enabled()

View File

@ -167,13 +167,18 @@ namespace Bit.App.Services
} }
public async Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator) public async Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator)
{
return (await ShowPasswordDialogAndGetItAsync(title, body, validator)).valid;
}
public async Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> validator)
{ {
var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation, var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation,
AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true); AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true);
if (password == null) if (password == null)
{ {
return false; return (password, false);
} }
var valid = await validator(password); var valid = await validator(password);
@ -183,7 +188,7 @@ namespace Bit.App.Services
await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok); await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok);
} }
return valid; return (password, valid);
} }
public bool IsDev() public bool IsDev()

View File

@ -151,6 +151,39 @@
</VisualStateGroupList> </VisualStateGroupList>
</Setter> </Setter>
</Style> </Style>
<Style TargetType="Button"
Class="btn-secondary">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBorderColor}" />
<Setter Property="BorderWidth"
Value="1" />
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColor}" />
<Setter Property="FontSize"
Value="Medium" />
<Setter Property="CornerRadius"
Value="5" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColorDisabled}" />
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button" <Style TargetType="Button"
ApplyToDerivedTypes="True" ApplyToDerivedTypes="True"
Class="btn-icon-platform"> Class="btn-icon-platform">

View File

@ -67,6 +67,13 @@
<Setter Property="TextType" <Setter Property="TextType"
Value="Html" /> Value="Html" />
</Style> </Style>
<Style TargetType="Label"
Class="text-body">
<Setter Property="FontSize"
Value="Body" />
<Setter Property="TextColor"
Value="{DynamicResource TextColor}" />
</Style>
<!-- Pages --> <!-- Pages -->
<Style TargetType="TabbedPage" <Style TargetType="TabbedPage"

View File

@ -172,6 +172,44 @@
</VisualStateGroupList> </VisualStateGroupList>
</Setter> </Setter>
</Style> </Style>
<Style TargetType="Button"
Class="btn-secondary">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBorderColor}" />
<Setter Property="BorderWidth"
Value="1" />
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColor}" />
<Setter Property="FontSize"
Value="Medium" />
<Setter Property="CornerRadius"
Value="5" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColorDisabled}" />
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
<Setter Property="BorderColor"
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Button" <Style TargetType="Button"
ApplyToDerivedTypes="True" ApplyToDerivedTypes="True"
Class="btn-icon-platform"> Class="btn-icon-platform">

View File

@ -61,6 +61,7 @@ namespace Bit.Core.Abstractions
Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request); Task PutDeviceTokenAsync(string identifier, DeviceTokenRequest request);
Task PostEventsCollectAsync(IEnumerable<EventRequest> request); Task PostEventsCollectAsync(IEnumerable<EventRequest> request);
Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request); Task PutUpdateTempPasswordAsync(UpdateTempPasswordRequest request);
Task DeleteAccountAsync(DeleteAccountRequest request);
Task<OrganizationKeysResponse> GetOrganizationKeysAsync(string id); Task<OrganizationKeysResponse> GetOrganizationKeysAsync(string id);
Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier); Task<OrganizationAutoEnrollStatusResponse> GetOrganizationAutoEnrollStatusAsync(string identifier);
Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId, Task PutOrganizationUserResetPasswordEnrollmentAsync(string orgId, string userId,

View File

@ -22,6 +22,7 @@ namespace Bit.Core.Abstractions
Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null, Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
string cancelText = null, string type = null); string cancelText = null, string type = null);
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator); Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> validator);
void ShowToast(string type, string title, string text, Dictionary<string, object> options = null); void ShowToast(string type, string title, string text, Dictionary<string, object> options = null);
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null); void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
bool SupportsFido2(); bool SupportsFido2();

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Request
{
public class DeleteAccountRequest
{
public string MasterPasswordHash { get; set; }
}
}

View File

@ -195,6 +195,11 @@ namespace Bit.Core.Services
request, true, false); request, true, false);
} }
public Task DeleteAccountAsync(DeleteAccountRequest request)
{
return SendAsync<DeleteAccountRequest, object>(HttpMethod.Delete, "/accounts", request, true, false);
}
public Task PostConvertToKeyConnector() public Task PostConvertToKeyConnector()
{ {
return SendAsync<object, object>(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false); return SendAsync<object, object>(HttpMethod.Post, "/accounts/convert-to-key-connector", null, true, false);

View File

@ -0,0 +1,528 @@
{
"images": [
{
"filename": "ic_warning.pdf",
"idiom": "universal"
},
{
"scale": "1x",
"idiom": "universal"
},
{
"scale": "2x",
"idiom": "universal"
},
{
"scale": "3x",
"idiom": "universal"
},
{
"idiom": "iphone"
},
{
"scale": "1x",
"idiom": "iphone"
},
{
"scale": "2x",
"idiom": "iphone"
},
{
"subtype": "retina4",
"scale": "2x",
"idiom": "iphone"
},
{
"scale": "3x",
"idiom": "iphone"
},
{
"idiom": "ipad"
},
{
"scale": "1x",
"idiom": "ipad"
},
{
"scale": "2x",
"idiom": "ipad"
},
{
"idiom": "watch"
},
{
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{130,145}",
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{146,165}",
"scale": "2x",
"idiom": "watch"
},
{
"idiom": "mac"
},
{
"scale": "1x",
"idiom": "mac"
},
{
"scale": "2x",
"idiom": "mac"
},
{
"idiom": "car"
},
{
"scale": "2x",
"idiom": "car"
},
{
"scale": "3x",
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "1x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "3x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "1x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "iphone"
},
{
"subtype": "retina4",
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "3x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "1x",
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "watch"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{130,145}",
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{146,165}",
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "1x",
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "2x",
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"scale": "3x",
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "1x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "3x",
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "1x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "iphone"
},
{
"subtype": "retina4",
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "3x",
"idiom": "iphone"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "1x",
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "ipad"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "watch"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{130,145}",
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"screenWidth": "{146,165}",
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "watch"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "1x",
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "mac"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "2x",
"idiom": "car"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "light"
}
],
"scale": "3x",
"idiom": "car"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

View File

@ -151,6 +151,8 @@
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white.png" /> <ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white.png" />
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%402x.png" /> <ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%402x.png" />
<ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%403x.png" /> <ImageAsset Include="Resources\Assets.xcassets\LaunchScreen.imageset\logo_white%403x.png" />
<ImageAsset Include="Resources\Assets.xcassets\ic_warning.imageset\Contents.json" />
<ImageAsset Include="Resources\Assets.xcassets\ic_warning.imageset\ic_warning.pdf" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<InterfaceDefinition Include="LaunchScreen.storyboard" /> <InterfaceDefinition Include="LaunchScreen.storyboard" />
@ -415,5 +417,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Resources\Assets.xcassets\LaunchScreen.imageset\" /> <Folder Include="Resources\Assets.xcassets\LaunchScreen.imageset\" />
<Folder Include="Resources\Assets.xcassets\ic_warning.imageset\" />
</ItemGroup> </ItemGroup>
</Project> </Project>