Browse Source
* [EC-980] Added iOS otpauth handler (#2370) * EC-980 added Bitwarden as otpauth scheme handler * EC-980 Fix format * [EC-981] OTP handling - Set to selected cipher (#2404) * EC-981 Started adding OTP to existing cipher. Reused AutofillCiphersPage for the cipher selection and refactored it so that we have more code reuse * EC-981 Fix navigation on otp handling * EC-981 Fix formatting * EC-981 Added otp cipher selection callout and add close toolbar item when needed * PM-1131 implemented cipher creation from otp handling flow with otp key filled (#2407) * PM-1133 Updated empty states for search and cipher selection on otp flow (#2408)github-services/pull/2411/head
34 changed files with 1223 additions and 576 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using System; |
||||
|
||||
namespace Bit.App.Abstractions |
||||
{ |
||||
public interface IDeepLinkContext |
||||
{ |
||||
bool OnNewUri(Uri uri); |
||||
} |
||||
} |
||||
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using System.Windows.Input; |
||||
using Bit.App.Abstractions; |
||||
using Bit.App.Controls; |
||||
using Bit.App.Models; |
||||
using Bit.App.Resources; |
||||
using Bit.App.Utilities; |
||||
using Bit.Core; |
||||
using Bit.Core.Abstractions; |
||||
using Bit.Core.Models.View; |
||||
using Bit.Core.Utilities; |
||||
using Xamarin.CommunityToolkit.ObjectModel; |
||||
using Xamarin.Forms; |
||||
|
||||
namespace Bit.App.Pages |
||||
{ |
||||
public abstract class CipherSelectionPageViewModel : BaseViewModel |
||||
{ |
||||
protected readonly IPlatformUtilsService _platformUtilsService; |
||||
protected readonly IDeviceActionService _deviceActionService; |
||||
protected readonly IAutofillHandler _autofillHandler; |
||||
protected readonly ICipherService _cipherService; |
||||
protected readonly IStateService _stateService; |
||||
protected readonly IPasswordRepromptService _passwordRepromptService; |
||||
protected readonly IMessagingService _messagingService; |
||||
protected readonly ILogger _logger; |
||||
|
||||
protected bool _showNoData; |
||||
protected bool _showList; |
||||
protected string _noDataText; |
||||
protected bool _websiteIconsEnabled; |
||||
|
||||
public CipherSelectionPageViewModel() |
||||
{ |
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>(); |
||||
_cipherService = ServiceContainer.Resolve<ICipherService>(); |
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>(); |
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>(); |
||||
_stateService = ServiceContainer.Resolve<IStateService>(); |
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>(); |
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>(); |
||||
_logger = ServiceContainer.Resolve<ILogger>(); |
||||
|
||||
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>(); |
||||
CipherOptionsCommand = new AsyncCommand<CipherView>(cipher => AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService), |
||||
onException: ex => HandleException(ex), |
||||
allowsMultipleExecutions: false); |
||||
SelectCipherCommand = new AsyncCommand<IGroupingsPageListItem>(SelectCipherAsync, |
||||
onException: ex => HandleException(ex), |
||||
allowsMultipleExecutions: false); |
||||
AddCipherCommand = new AsyncCommand(AddCipherAsync, |
||||
onException: ex => HandleException(ex), |
||||
allowsMultipleExecutions: false); |
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) |
||||
{ |
||||
AllowAddAccountRow = false |
||||
}; |
||||
} |
||||
|
||||
public string Name { get; set; } |
||||
public bool LoadedOnce { get; set; } |
||||
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; } |
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } |
||||
|
||||
public ICommand CipherOptionsCommand { get; set; } |
||||
public ICommand SelectCipherCommand { get; set; } |
||||
public ICommand AddCipherCommand { get; set; } |
||||
|
||||
public bool ShowNoData |
||||
{ |
||||
get => _showNoData; |
||||
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new string[] { nameof(ShowCallout) }); |
||||
} |
||||
|
||||
public bool ShowList |
||||
{ |
||||
get => _showList; |
||||
set => SetProperty(ref _showList, value); |
||||
} |
||||
|
||||
public string NoDataText |
||||
{ |
||||
get => _noDataText; |
||||
set => SetProperty(ref _noDataText, value); |
||||
} |
||||
|
||||
public bool WebsiteIconsEnabled |
||||
{ |
||||
get => _websiteIconsEnabled; |
||||
set => SetProperty(ref _websiteIconsEnabled, value); |
||||
} |
||||
|
||||
public virtual bool ShowCallout => false; |
||||
|
||||
public abstract void Init(Models.AppOptions options); |
||||
|
||||
public async Task LoadAsync() |
||||
{ |
||||
LoadedOnce = true; |
||||
ShowList = false; |
||||
ShowNoData = false; |
||||
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault(); |
||||
|
||||
var groupedItems = await LoadGroupedItemsAsync(); |
||||
|
||||
// TODO: refactor this |
||||
if (Device.RuntimePlatform == Device.Android |
||||
|| |
||||
GroupedItems.Any()) |
||||
{ |
||||
var items = new List<IGroupingsPageListItem>(); |
||||
foreach (var itemGroup in groupedItems) |
||||
{ |
||||
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); |
||||
items.AddRange(itemGroup); |
||||
} |
||||
|
||||
GroupedItems.ReplaceRange(items); |
||||
} |
||||
else |
||||
{ |
||||
// HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list |
||||
var first = true; |
||||
var items = new List<IGroupingsPageListItem>(); |
||||
foreach (var itemGroup in groupedItems) |
||||
{ |
||||
if (!first) |
||||
{ |
||||
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount)); |
||||
} |
||||
else |
||||
{ |
||||
first = false; |
||||
} |
||||
items.AddRange(itemGroup); |
||||
} |
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => |
||||
{ |
||||
if (groupedItems.Any()) |
||||
{ |
||||
GroupedItems.ReplaceRange(new List<IGroupingsPageListItem> { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) }); |
||||
GroupedItems.AddRange(items); |
||||
} |
||||
else |
||||
{ |
||||
GroupedItems.Clear(); |
||||
} |
||||
}); |
||||
} |
||||
await Device.InvokeOnMainThreadAsync(() => |
||||
{ |
||||
ShowList = groupedItems.Any(); |
||||
ShowNoData = !ShowList; |
||||
}); |
||||
} |
||||
|
||||
protected abstract Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync(); |
||||
|
||||
protected abstract Task SelectCipherAsync(IGroupingsPageListItem item); |
||||
|
||||
protected abstract Task AddCipherAsync(); |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using Bit.App.Resources; |
||||
using Bit.Core.Abstractions; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Utilities; |
||||
using Xamarin.Forms; |
||||
|
||||
namespace Bit.App.Pages |
||||
{ |
||||
public class OTPCipherSelectionPageViewModel : CipherSelectionPageViewModel |
||||
{ |
||||
private readonly ISearchService _searchService = ServiceContainer.Resolve<ISearchService>(); |
||||
|
||||
private OtpData _otpData; |
||||
private Models.AppOptions _appOptions; |
||||
|
||||
public override bool ShowCallout => !ShowNoData; |
||||
|
||||
public override void Init(Models.AppOptions options) |
||||
{ |
||||
_appOptions = options; |
||||
_otpData = options.OtpData.Value; |
||||
|
||||
Name = _otpData.Issuer ?? _otpData.AccountName; |
||||
PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--"); |
||||
NoDataText = string.Format(AppResources.ThereAreNoItemsInYourVaultThatMatchX, Name ?? "--") |
||||
+ Environment.NewLine |
||||
+ AppResources.SearchForAnItemOrAddANewItem; |
||||
} |
||||
|
||||
protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync() |
||||
{ |
||||
var groupedItems = new List<GroupingsPageListGroup>(); |
||||
var allCiphers = await _cipherService.GetAllDecryptedAsync(); |
||||
var ciphers = await _searchService.SearchCiphersAsync(_otpData.Issuer ?? _otpData.AccountName, |
||||
c => c.Type == CipherType.Login && !c.IsDeleted, allCiphers); |
||||
|
||||
if (ciphers?.Any() ?? false) |
||||
{ |
||||
groupedItems.Add( |
||||
new GroupingsPageListGroup(ciphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(), |
||||
AppResources.MatchingItems, |
||||
ciphers.Count, |
||||
false, |
||||
true)); |
||||
} |
||||
|
||||
return groupedItems; |
||||
} |
||||
|
||||
protected override async Task SelectCipherAsync(IGroupingsPageListItem item) |
||||
{ |
||||
if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var cipher = listItem.Cipher; |
||||
|
||||
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync()) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
var editCipherPage = new CipherAddEditPage(cipher.Id, appOptions: _appOptions); |
||||
await Page.Navigation.PushModalAsync(new NavigationPage(editCipherPage)); |
||||
return; |
||||
} |
||||
|
||||
protected override async Task AddCipherAsync() |
||||
{ |
||||
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: Name, appOptions: _appOptions); |
||||
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin)); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
using System; |
||||
using Bit.App.Abstractions; |
||||
using Bit.Core; |
||||
using Bit.Core.Abstractions; |
||||
|
||||
namespace Bit.App.Services |
||||
{ |
||||
public class DeepLinkContext : IDeepLinkContext |
||||
{ |
||||
public const string NEW_OTP_MESSAGE = "handleOTPUriMessage"; |
||||
|
||||
private readonly IMessagingService _messagingService; |
||||
|
||||
public DeepLinkContext(IMessagingService messagingService) |
||||
{ |
||||
_messagingService = messagingService; |
||||
} |
||||
|
||||
public bool OnNewUri(Uri uri) |
||||
{ |
||||
if (uri.Scheme == Constants.OtpAuthScheme) |
||||
{ |
||||
_messagingService.Send(NEW_OTP_MESSAGE, uri.AbsoluteUri); |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using Bit.Core.Enums; |
||||
|
||||
namespace Bit.Core.Utilities |
||||
{ |
||||
public struct OtpData |
||||
{ |
||||
const string LABEL_SEPARATOR = ":"; |
||||
|
||||
public OtpData(string absoluteUri) |
||||
{ |
||||
if (!System.Uri.TryCreate(absoluteUri, UriKind.Absolute, out var uri) |
||||
|| |
||||
uri.Scheme != Constants.OtpAuthScheme) |
||||
{ |
||||
throw new InvalidOperationException("Cannot create OtpData. Invalid OTP uri"); |
||||
} |
||||
|
||||
Uri = absoluteUri; |
||||
AccountName = null; |
||||
Issuer = null; |
||||
Secret = null; |
||||
Digits = null; |
||||
Period = null; |
||||
Algorithm = null; |
||||
|
||||
var escapedlabel = uri.Segments.Last(); |
||||
if (escapedlabel != "/") |
||||
{ |
||||
var label = UriExtensions.UnescapeDataString(escapedlabel); |
||||
if (label.Contains(LABEL_SEPARATOR)) |
||||
{ |
||||
var parts = label.Split(LABEL_SEPARATOR); |
||||
AccountName = parts[0].Trim(); |
||||
Issuer = parts[1].Trim(); |
||||
} |
||||
else |
||||
{ |
||||
AccountName = label.Trim(); |
||||
} |
||||
} |
||||
|
||||
var qsParams = CoreHelpers.GetQueryParams(uri); |
||||
if (Issuer is null && qsParams.TryGetValue("issuer", out var issuer)) |
||||
{ |
||||
Issuer = issuer; |
||||
} |
||||
|
||||
if (qsParams.TryGetValue("secret", out var secret)) |
||||
{ |
||||
Secret = secret; |
||||
} |
||||
|
||||
if (qsParams.TryGetValue("digits", out var digitParam) |
||||
&& |
||||
int.TryParse(digitParam?.Trim(), out var digits)) |
||||
{ |
||||
Digits = digits; |
||||
} |
||||
|
||||
if (qsParams.TryGetValue("period", out var periodParam) |
||||
&& |
||||
int.TryParse(periodParam?.Trim(), out var period) |
||||
&& |
||||
period > 0) |
||||
{ |
||||
Period = period; |
||||
} |
||||
|
||||
if (qsParams.TryGetValue("algorithm", out var algParam) |
||||
&& |
||||
algParam?.ToLower() is string alg) |
||||
{ |
||||
if (alg == "sha256") |
||||
{ |
||||
Algorithm = CryptoHashAlgorithm.Sha256; |
||||
} |
||||
else if (alg == "sha512") |
||||
{ |
||||
Algorithm = CryptoHashAlgorithm.Sha512; |
||||
} |
||||
} |
||||
} |
||||
|
||||
public string Uri { get; } |
||||
public string AccountName { get; } |
||||
public string Issuer { get; } |
||||
public string Secret { get; } |
||||
public int? Digits { get; } |
||||
public int? Period { get; } |
||||
public CryptoHashAlgorithm? Algorithm { get; } |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using System; |
||||
|
||||
namespace Bit.Core.Utilities |
||||
{ |
||||
public static class UriExtensions |
||||
{ |
||||
public static string UnescapeDataString(string uriString) |
||||
{ |
||||
string unescapedUri; |
||||
while ((unescapedUri = System.Uri.UnescapeDataString(uriString)) != uriString) |
||||
{ |
||||
uriString = unescapedUri; |
||||
} |
||||
|
||||
return unescapedUri; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
{ |
||||
"info" : { |
||||
"author" : "xcode", |
||||
"version" : 1 |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
{ |
||||
"images" : [ |
||||
{ |
||||
"filename" : "Empty-items-state.pdf", |
||||
"idiom" : "universal" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"filename" : "Empty-items-state-dark.pdf", |
||||
"idiom" : "universal" |
||||
} |
||||
], |
||||
"info" : { |
||||
"author" : "xcode", |
||||
"version" : 1 |
||||
}, |
||||
"properties" : { |
||||
"preserves-vector-representation" : true |
||||
} |
||||
} |
||||
Binary file not shown.
Binary file not shown.
@ -1,528 +1,608 @@
@@ -1,528 +1,608 @@
|
||||
{ |
||||
"images": [ |
||||
"images" : [ |
||||
{ |
||||
"filename": "ic_warning-1.pdf", |
||||
"idiom": "universal" |
||||
"filename" : "ic_warning-1.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" |
||||
"appearances" : [ |
||||
{ |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom" : "universal" |
||||
}, |
||||
{ |
||||
"scale": "1x", |
||||
"idiom": "mac" |
||||
"appearances" : [ |
||||
{ |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom" : "universal" |
||||
}, |
||||
{ |
||||
"scale": "2x", |
||||
"idiom": "mac" |
||||
"idiom" : "universal", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom": "car" |
||||
"appearances" : [ |
||||
{ |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom" : "universal", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"scale": "2x", |
||||
"idiom": "car" |
||||
"appearances" : [ |
||||
{ |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom" : "universal", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"scale": "3x", |
||||
"idiom": "car" |
||||
"idiom" : "universal", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "universal" |
||||
"idiom" : "universal", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "universal" |
||||
"idiom" : "universal", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"idiom" : "universal", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "universal" |
||||
"idiom" : "universal", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "universal" |
||||
"idiom" : "universal", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"idiom" : "iphone" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "iphone" |
||||
"idiom" : "iphone" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "iphone" |
||||
"idiom" : "iphone" |
||||
}, |
||||
{ |
||||
"idiom" : "iphone", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "iphone" |
||||
"idiom" : "iphone", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"subtype": "retina4", |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "iphone" |
||||
"idiom" : "iphone", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"idiom" : "iphone", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "iphone" |
||||
"idiom" : "iphone", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom": "ipad" |
||||
"idiom" : "iphone", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "iphone", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "ipad" |
||||
"idiom" : "iphone", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "ipad" |
||||
"idiom" : "iphone", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"idiom" : "iphone", |
||||
"scale" : "1x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "watch" |
||||
"idiom" : "iphone", |
||||
"scale" : "1x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "iphone", |
||||
"scale" : "1x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"screenWidth": "{130,145}", |
||||
"appearances": [ |
||||
"idiom" : "iphone", |
||||
"scale" : "2x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "iphone", |
||||
"scale" : "2x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"screenWidth": "{146,165}", |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "iphone", |
||||
"scale" : "2x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"idiom" : "iphone", |
||||
"scale" : "3x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "mac" |
||||
"idiom" : "iphone", |
||||
"scale" : "3x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "mac" |
||||
"idiom" : "iphone", |
||||
"scale" : "3x", |
||||
"subtype" : "retina4" |
||||
}, |
||||
{ |
||||
"idiom" : "ipad" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "mac" |
||||
"idiom" : "ipad" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom": "car" |
||||
"idiom" : "ipad" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"idiom" : "ipad", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "car" |
||||
"idiom" : "ipad", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "dark" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "car" |
||||
"idiom" : "ipad", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "ipad", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "universal" |
||||
"idiom" : "ipad", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "universal" |
||||
"idiom" : "ipad", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "car" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "universal" |
||||
"idiom" : "car" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "universal" |
||||
"idiom" : "car" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"idiom" : "car", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "iphone" |
||||
"idiom" : "car", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "iphone" |
||||
"idiom" : "car", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "car", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "iphone" |
||||
"idiom" : "car", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"subtype": "retina4", |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "iphone" |
||||
"idiom" : "car", |
||||
"scale" : "3x" |
||||
}, |
||||
{ |
||||
"idiom" : "mac" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "iphone" |
||||
"idiom" : "mac" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom": "ipad" |
||||
"idiom" : "mac" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"idiom" : "mac", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "ipad" |
||||
"idiom" : "mac", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "ipad" |
||||
"idiom" : "mac", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "mac", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "watch" |
||||
"idiom" : "mac", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "mac", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "watch" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"screen-width" : "<=145" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"screen-width" : ">161" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"screen-width" : ">145" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"screen-width" : ">183" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : "<=145" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : ">161" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : ">145" |
||||
}, |
||||
{ |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : ">183" |
||||
}, |
||||
{ |
||||
"screenWidth": "{130,145}", |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "watch" |
||||
}, |
||||
{ |
||||
"screenWidth": "{146,165}", |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "watch" |
||||
"idiom" : "watch" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"idiom": "mac" |
||||
"idiom" : "watch", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "1x", |
||||
"idiom": "mac" |
||||
"idiom" : "watch", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "mac" |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : "<=145" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"idiom": "car" |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : "<=145" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "light" |
||||
} |
||||
], |
||||
"scale": "2x", |
||||
"idiom": "car" |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : ">145" |
||||
}, |
||||
{ |
||||
"appearances": [ |
||||
"appearances" : [ |
||||
{ |
||||
"appearance": "luminosity", |
||||
"value": "light" |
||||
"appearance" : "luminosity", |
||||
"value" : "dark" |
||||
} |
||||
], |
||||
"scale": "3x", |
||||
"idiom": "car" |
||||
"idiom" : "watch", |
||||
"scale" : "2x", |
||||
"screen-width" : ">145" |
||||
} |
||||
], |
||||
"info": { |
||||
"version": 1, |
||||
"author": "xcode" |
||||
"info" : { |
||||
"author" : "xcode", |
||||
"version" : 1 |
||||
} |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue