From a18f74a72adc1821697887dee58dd729aad62c0e Mon Sep 17 00:00:00 2001
From: Federico Maccaroni <fedemkr@gmail.com>
Date: Thu, 9 Mar 2023 11:16:48 -0300
Subject: [PATCH] [PM-1129] iOS 16 Third-Party 2FA OTP handling (#2409)

* [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)
---
 src/Android/MainActivity.cs                   |   8 +-
 src/App/Abstractions/IAccountsManager.cs      |   1 +
 src/App/Abstractions/IDeepLinkContext.cs      |   9 +
 src/App/App.xaml.cs                           |  39 +-
 src/App/Models/AppOptions.cs                  |   3 +
 .../Vault/AutofillCiphersPageViewModel.cs     | 229 ++----
 .../Pages/Vault/CipherAddEditPageViewModel.cs |  23 +
 ...hersPage.xaml => CipherSelectionPage.xaml} |  61 +-
 ...ge.xaml.cs => CipherSelectionPage.xaml.cs} |  72 +-
 .../Vault/CipherSelectionPageViewModel.cs     | 167 ++++
 src/App/Pages/Vault/CiphersPage.xaml          |  22 +-
 src/App/Pages/Vault/CiphersPage.xaml.cs       |  14 +-
 src/App/Pages/Vault/CiphersPageViewModel.cs   |  81 +-
 .../Vault/OTPCipherSelectionPageViewModel.cs  |  79 ++
 src/App/Resources/AppResources.Designer.cs    |  47 +-
 src/App/Resources/AppResources.resx           |  12 +
 src/App/Services/DeepLinkContext.cs           |  30 +
 .../AccountManagement/AccountsManager.cs      |  12 +-
 src/App/Utilities/AppHelpers.cs               |   6 +-
 src/App/Utilities/AppSetup.cs                 |   5 +-
 src/Core/Constants.cs                         |   1 +
 src/Core/Enums/NavigationTarget.cs            |   3 +-
 src/Core/Services/TotpService.cs              |  35 +-
 src/Core/Utilities/CoreHelpers.cs             |  19 +-
 src/Core/Utilities/OtpData.cs                 |  94 +++
 src/Core/Utilities/UriExtensions.cs           |  18 +
 src/iOS/AppDelegate.cs                        |   4 +-
 src/iOS/Info.plist                            |   8 +
 .../Resources/Assets.xcassets/Contents.json   |   6 +
 .../empty_items_state.imageset/Contents.json  |  25 +
 .../Empty-items-state-dark.pdf                | Bin 0 -> 1891 bytes
 .../Empty-items-state.pdf                     | Bin 0 -> 1875 bytes
 .../ic_warning.imageset/Contents.json         | 770 ++++++++++--------
 src/iOS/iOS.csproj                            |   4 +
 34 files changed, 1277 insertions(+), 630 deletions(-)
 create mode 100644 src/App/Abstractions/IDeepLinkContext.cs
 rename src/App/Pages/Vault/{AutofillCiphersPage.xaml => CipherSelectionPage.xaml} (68%)
 rename src/App/Pages/Vault/{AutofillCiphersPage.xaml.cs => CipherSelectionPage.xaml.cs} (66%)
 create mode 100644 src/App/Pages/Vault/CipherSelectionPageViewModel.cs
 create mode 100644 src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs
 create mode 100644 src/App/Services/DeepLinkContext.cs
 create mode 100644 src/Core/Utilities/OtpData.cs
 create mode 100644 src/Core/Utilities/UriExtensions.cs
 create mode 100644 src/iOS/Resources/Assets.xcassets/Contents.json
 create mode 100644 src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
 create mode 100644 src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
 create mode 100644 src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf

diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs
index 2e25bc1cc..f4fc640d0 100644
--- a/src/Android/MainActivity.cs
+++ b/src/Android/MainActivity.cs
@@ -170,7 +170,7 @@ namespace Bit.Droid
             {
                 if (intent?.GetStringExtra("uri") is string uri)
                 {
-                    _messagingService.Send("popAllAndGoToAutofillCiphers");
+                    _messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
                     if (_appOptions != null)
                     {
                        _appOptions.Uri = uri;
@@ -178,7 +178,7 @@ namespace Bit.Droid
                 }
                 else if (intent.GetBooleanExtra("generatorTile", false))
                 {
-                    _messagingService.Send("popAllAndGoToTabGenerator");
+                    _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE);
                     if (_appOptions != null)
                     {
                         _appOptions.GeneratorTile = true;
@@ -186,7 +186,7 @@ namespace Bit.Droid
                 }
                 else if (intent.GetBooleanExtra("myVaultTile", false))
                 {
-                    _messagingService.Send("popAllAndGoToTabMyVault");
+                    _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE);
                     if (_appOptions != null)
                     {
                         _appOptions.MyVaultTile = true;
@@ -198,7 +198,7 @@ namespace Bit.Droid
                     {
                         _appOptions.CreateSend = GetCreateSendRequest(intent);
                     }
-                    _messagingService.Send("popAllAndGoToTabSend");
+                    _messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE);
                 }
                 else
                 {
diff --git a/src/App/Abstractions/IAccountsManager.cs b/src/App/Abstractions/IAccountsManager.cs
index 532b75958..ab78c6ca4 100644
--- a/src/App/Abstractions/IAccountsManager.cs
+++ b/src/App/Abstractions/IAccountsManager.cs
@@ -8,6 +8,7 @@ namespace Bit.App.Abstractions
     {
         void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
         Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
+        Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction);
         Task LogOutAsync(string userId, bool userInitiated, bool expired);
         Task PromptToSwitchToExistingAccountAsync(string userId);
     }
diff --git a/src/App/Abstractions/IDeepLinkContext.cs b/src/App/Abstractions/IDeepLinkContext.cs
new file mode 100644
index 000000000..345596d50
--- /dev/null
+++ b/src/App/Abstractions/IDeepLinkContext.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Bit.App.Abstractions
+{
+    public interface IDeepLinkContext
+    {
+        bool OnNewUri(Uri uri);
+    }
+}
diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs
index 095d5778a..3cbc4ff52 100644
--- a/src/App/App.xaml.cs
+++ b/src/App/App.xaml.cs
@@ -23,6 +23,11 @@ namespace Bit.App
 {
     public partial class App : Application, IAccountsManagerHost
     {
+        public const string POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE = "popAllAndGoToTabGenerator";
+        public const string POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE = "popAllAndGoToTabMyVault";
+        public const string POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE = "popAllAndGoToTabSend";
+        public const string POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE = "popAllAndGoToAutofillCiphers";
+
         private readonly IBroadcasterService _broadcasterService;
         private readonly IMessagingService _messagingService;
         private readonly IStateService _stateService;
@@ -103,12 +108,18 @@ namespace Bit.App
                         await Task.Delay(1000);
                         await _accountsManager.NavigateOnAccountChangeAsync();
                     }
-                    else if (message.Command == "popAllAndGoToTabGenerator" ||
-                        message.Command == "popAllAndGoToTabMyVault" ||
-                        message.Command == "popAllAndGoToTabSend" ||
-                        message.Command == "popAllAndGoToAutofillCiphers")
+                    else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
+                        message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
+                        message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
+                        message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
+                        message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
                     {
-                        Device.BeginInvokeOnMainThread(async () =>
+                        if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
+                        {
+                            Options.OtpData = new OtpData((string)message.Data);
+                        }
+
+                        await Device.InvokeOnMainThreadAsync(async () =>
                         {
                             if (Current.MainPage is TabsPage tabsPage)
                             {
@@ -116,24 +127,29 @@ namespace Bit.App
                                 {
                                     await tabsPage.Navigation.PopModalAsync(false);
                                 }
-                                if (message.Command == "popAllAndGoToAutofillCiphers")
+                                if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
                                 {
-                                    Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
+                                    Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
                                 }
-                                else if (message.Command == "popAllAndGoToTabMyVault")
+                                else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE)
                                 {
                                     Options.MyVaultTile = false;
                                     tabsPage.ResetToVaultPage();
                                 }
-                                else if (message.Command == "popAllAndGoToTabGenerator")
+                                else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE)
                                 {
                                     Options.GeneratorTile = false;
                                     tabsPage.ResetToGeneratorPage();
                                 }
-                                else if (message.Command == "popAllAndGoToTabSend")
+                                else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE)
                                 {
                                     tabsPage.ResetToSendPage();
                                 }
+                                else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
+                                {
+                                    tabsPage.ResetToVaultPage();
+                                    await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
+                                }
                             }
                         });
                     }
@@ -494,7 +510,8 @@ namespace Bit.App
                     Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
                     break;
                 case NavigationTarget.AutofillCiphers:
-                    Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
+                case NavigationTarget.OtpCipherSelection:
+                    Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
                     break;
                 case NavigationTarget.SendAddEdit:
                     Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
diff --git a/src/App/Models/AppOptions.cs b/src/App/Models/AppOptions.cs
index 0a251d7cd..58fe79d49 100644
--- a/src/App/Models/AppOptions.cs
+++ b/src/App/Models/AppOptions.cs
@@ -1,5 +1,6 @@
 using System;
 using Bit.Core.Enums;
+using Bit.Core.Utilities;
 
 namespace Bit.App.Models
 {
@@ -23,6 +24,7 @@ namespace Bit.App.Models
         public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
         public bool CopyInsteadOfShareAfterSaving { get; set; }
         public bool HideAccountSwitcher { get; set; }
+        public OtpData? OtpData { get; set; }
 
         public void SetAllFrom(AppOptions o)
         {
@@ -48,6 +50,7 @@ namespace Bit.App.Models
             CreateSend = o.CreateSend;
             CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
             HideAccountSwitcher = o.HideAccountSwitcher;
+            OtpData = o.OtpData;
         }
     }
 }
diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
index 525f7dea1..aade36dd6 100644
--- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
+++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
@@ -1,91 +1,29 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
-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.Enums;
 using Bit.Core.Exceptions;
 using Bit.Core.Models.View;
 using Bit.Core.Utilities;
-using Xamarin.CommunityToolkit.ObjectModel;
 using Xamarin.Forms;
 
 namespace Bit.App.Pages
 {
-    public class AutofillCiphersPageViewModel : BaseViewModel
+    public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel
     {
-        private readonly IPlatformUtilsService _platformUtilsService;
-        private readonly IDeviceActionService _deviceActionService;
-        private readonly IAutofillHandler _autofillHandler;
-        private readonly ICipherService _cipherService;
-        private readonly IStateService _stateService;
-        private readonly IPasswordRepromptService _passwordRepromptService;
-        private readonly IMessagingService _messagingService;
-        private readonly ILogger _logger;
+        private CipherType? _fillType;
 
-        private bool _showNoData;
-        private bool _showList;
-        private string _noDataText;
-        private bool _websiteIconsEnabled;
-
-        public AutofillCiphersPageViewModel()
-        {
-            _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
-            _cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
-            _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
-            _autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
-            _stateService = ServiceContainer.Resolve<IStateService>("stateService");
-            _passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
-            _messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
-            _logger = ServiceContainer.Resolve<ILogger>("logger");
-
-            GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
-            CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
-
-            AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
-            {
-                AllowAddAccountRow = false
-            };
-        }
-
-        public string Name { get; set; }
         public string Uri { get; set; }
-        public Command CipherOptionsCommand { get; set; }
-        public bool LoadedOnce { get; set; }
-        public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
-        public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
 
-        public bool ShowNoData
-        {
-            get => _showNoData;
-            set => SetProperty(ref _showNoData, value);
-        }
-
-        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 void Init(AppOptions appOptions)
+        public override void Init(AppOptions appOptions)
         {
             Uri = appOptions?.Uri;
+            _fillType = appOptions.FillType;
+
             string name = null;
             if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
             {
@@ -104,14 +42,11 @@ namespace Bit.App.Pages
             NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
         }
 
-        public async Task LoadAsync()
+        protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync()
         {
-            LoadedOnce = true;
-            ShowList = false;
-            ShowNoData = false;
-            WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
             var groupedItems = new List<GroupingsPageListGroup>();
             var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null);
+
             var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
             var hasMatching = matching?.Any() ?? false;
             if (matching?.Any() ?? false)
@@ -119,6 +54,7 @@ namespace Bit.App.Pages
                 groupedItems.Add(
                     new GroupingsPageListGroup(matching, AppResources.MatchingItems, matching.Count, false, true));
             }
+
             var fuzzy = ciphers.Item2?.Select(c =>
                 new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList();
             if (fuzzy?.Any() ?? false)
@@ -128,123 +64,88 @@ namespace Bit.App.Pages
                     !hasMatching));
             }
 
-            // 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);
-                }
-
-                if (groupedItems.Any())
-                {
-                    GroupedItems.ReplaceRange(new List<IGroupingsPageListItem> { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) });
-                    GroupedItems.AddRange(items);
-                }
-                else
-                {
-                    GroupedItems.Clear();
-                }
-            }
-            ShowList = groupedItems.Any();
-            ShowNoData = !ShowList;
+            return groupedItems;
         }
 
-        public async Task SelectCipherAsync(CipherView cipher, bool fuzzy)
+        protected override async Task SelectCipherAsync(IGroupingsPageListItem item)
         {
-            if (cipher == null)
+            if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null)
             {
                 return;
             }
+
+            var cipher = listItem.Cipher;
+
             if (_deviceActionService.SystemMajorVersion() < 21)
             {
                 await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
+                return;
             }
-            else
+
+            if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
             {
-                if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
+                return;
+            }
+            var autofillResponse = AppResources.Yes;
+            if (listItem.FuzzyAutofill)
+            {
+                var options = new List<string> { AppResources.Yes };
+                if (cipher.Type == CipherType.Login &&
+                    Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
                 {
-                    return;
+                    options.Add(AppResources.YesAndSave);
                 }
-                var autofillResponse = AppResources.Yes;
-                if (fuzzy)
+                autofillResponse = await _deviceActionService.DisplayAlertAsync(null,
+                    string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No,
+                    options.ToArray());
+            }
+            if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login)
+            {
+                var uris = cipher.Login?.Uris?.ToList();
+                if (uris == null)
                 {
-                    var options = new List<string> { AppResources.Yes };
-                    if (cipher.Type == CipherType.Login &&
-                        Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
-                    {
-                        options.Add(AppResources.YesAndSave);
-                    }
-                    autofillResponse = await _deviceActionService.DisplayAlertAsync(null,
-                        string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No,
-                        options.ToArray());
+                    uris = new List<LoginUriView>();
                 }
-                if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login)
+                uris.Add(new LoginUriView
                 {
-                    var uris = cipher.Login?.Uris?.ToList();
-                    if (uris == null)
+                    Uri = Uri,
+                    Match = null
+                });
+                cipher.Login.Uris = uris;
+                try
+                {
+                    await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
+                    await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher));
+                    await _deviceActionService.HideLoadingAsync();
+                }
+                catch (ApiException e)
+                {
+                    await _deviceActionService.HideLoadingAsync();
+                    if (e?.Error != null)
                     {
-                        uris = new List<LoginUriView>();
-                    }
-                    uris.Add(new LoginUriView
-                    {
-                        Uri = Uri,
-                        Match = null
-                    });
-                    cipher.Login.Uris = uris;
-                    try
-                    {
-                        await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
-                        await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher));
-                        await _deviceActionService.HideLoadingAsync();
-                    }
-                    catch (ApiException e)
-                    {
-                        await _deviceActionService.HideLoadingAsync();
-                        if (e?.Error != null)
-                        {
-                            await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
-                                AppResources.AnErrorHasOccurred);
-                        }
+                        await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
+                            AppResources.AnErrorHasOccurred);
                     }
                 }
-                if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
-                {
-                    _autofillHandler.Autofill(cipher);
-                }
+            }
+            if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
+            {
+                _autofillHandler.Autofill(cipher);
             }
         }
 
-        private async void CipherOptionsAsync(CipherView cipher)
+        protected override async Task AddCipherAsync()
         {
-            if ((Page as BaseContentPage).DoOnce())
+            if (_fillType.HasValue && _fillType != CipherType.Login)
             {
-                await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
+                var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true);
+                await Page.Navigation.PushModalAsync(new NavigationPage(pageForOther));
+                return;
             }
+
+            var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: Uri, name: Name,
+                fromAutofill: true);
+            await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
         }
     }
 }
diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
index cbc3838e5..e0a6cc226 100644
--- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
+++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Bit.App.Abstractions;
 using Bit.App.Lists.ItemViewModels.CustomFields;
 using Bit.App.Models;
 using Bit.App.Resources;
@@ -30,6 +31,7 @@ namespace Bit.App.Pages
         private readonly IClipboardService _clipboardService;
         private readonly IAutofillHandler _autofillHandler;
         private readonly IWatchDeviceService _watchDeviceService;
+        private readonly IAccountsManager _accountsManager;
 
         private bool _showNotesSeparator;
         private bool _showPassword;
@@ -44,6 +46,8 @@ namespace Bit.App.Pages
         private bool _hasCollections;
         private string _previousCipherId;
         private List<Core.Models.View.CollectionView> _writeableCollections;
+        private bool _fromOtp;
+
         protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
         {
             nameof(IsLogin),
@@ -82,6 +86,8 @@ namespace Bit.App.Pages
             _clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
             _autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
             _watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
+            _accountsManager = ServiceContainer.Resolve<IAccountsManager>();
+
 
             GeneratePasswordCommand = new Command(GeneratePassword);
             TogglePasswordCommand = new Command(TogglePassword);
@@ -302,6 +308,7 @@ namespace Bit.App.Pages
         public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
         public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
         public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
+
         public void Init()
         {
             PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
@@ -309,6 +316,8 @@ namespace Bit.App.Pages
 
         public async Task<bool> LoadAsync(AppOptions appOptions = null)
         {
+            _fromOtp = appOptions?.OtpData != null;
+
             var myEmail = await _stateService.GetEmailAsync();
             OwnershipOptions.Add(new KeyValuePair<string, string>(myEmail, null));
             var orgs = await _organizationService.GetAllAsync();
@@ -358,6 +367,10 @@ namespace Bit.App.Pages
                             Cipher.OrganizationId = OrganizationId;
                         }
                     }
+                    if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login)
+                    {
+                        Cipher.Login.Totp = appOptions.OtpData.Value.Uri;
+                    }
                 }
                 else
                 {
@@ -380,6 +393,7 @@ namespace Bit.App.Pages
                         Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type);
                         Cipher.Login.Username = appOptions.SaveUsername;
                         Cipher.Login.Password = appOptions.SavePassword;
+                        Cipher.Login.Totp = appOptions.OtpData?.Uri;
                         Cipher.Card.Code = appOptions.SaveCardCode;
                         if (int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1)
                         {
@@ -424,6 +438,11 @@ namespace Bit.App.Pages
                 {
                     Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
                 }
+
+                if (appOptions?.OtpData != null)
+                {
+                    _platformUtilsService.ShowToast(null, AppResources.AuthenticatorKey, AppResources.AuthenticatorKeyAdded);
+                }
             }
 
             if (EditMode && _previousCipherId != CipherId)
@@ -517,6 +536,10 @@ namespace Bit.App.Pages
                     // Close and go back to app
                     _autofillHandler.CloseAutofill();
                 }
+                else if (_fromOtp)
+                {
+                    await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null);
+                }
                 else
                 {
                     if (CloneMode)
diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml b/src/App/Pages/Vault/CipherSelectionPage.xaml
similarity index 68%
rename from src/App/Pages/Vault/AutofillCiphersPage.xaml
rename to src/App/Pages/Vault/CipherSelectionPage.xaml
index e1cc992ff..979392e3b 100644
--- a/src/App/Pages/Vault/AutofillCiphersPage.xaml
+++ b/src/App/Pages/Vault/CipherSelectionPage.xaml
@@ -1,30 +1,26 @@
 <?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.AutofillCiphersPage"
+             xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
+             x:Class="Bit.App.Pages.CipherSelectionPage"
              xmlns:pages="clr-namespace:Bit.App.Pages"
              xmlns:u="clr-namespace:Bit.App.Utilities"
              xmlns:controls="clr-namespace:Bit.App.Controls"
              xmlns:effects="clr-namespace:Bit.App.Effects;assembly=BitwardenApp"
-             x:DataType="pages:AutofillCiphersPageViewModel"
+             x:DataType="pages:CipherSelectionPageViewModel"
              Title="{Binding PageTitle}"
              x:Name="_page">
-
-    <ContentPage.BindingContext>
-        <pages:AutofillCiphersPageViewModel />
-    </ContentPage.BindingContext>
-
     <ContentPage.ToolbarItems>
         <controls:ExtendedToolbarItem
             x:Name="_accountAvatar"
             IconImageSource="{Binding AvatarImageSource}"
             Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
             Order="Primary"
-            Priority="-1"
+            Priority="-2"
             UseOriginalImage="True"
             AutomationProperties.IsInAccessibleTree="True"
             AutomationProperties.Name="{u:I18n Account}" />
-        <ToolbarItem Icon="search.png" Clicked="Search_Clicked"
+        <ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked"
                      AutomationProperties.IsInAccessibleTree="True"
                      AutomationProperties.Name="{u:I18n Search}" />
     </ContentPage.ToolbarItems>
@@ -32,6 +28,21 @@
     <ContentPage.Resources>
         <ResourceDictionary>
             <u:InverseBoolConverter x:Key="inverseBool" />
+            <controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
+                        
+            <ToolbarItem
+                x:Name="_closeItem"
+                x:Key="_closeItem"
+                Text="{u:I18n Close}"
+                Clicked="CloseItem_Clicked"
+                Order="Primary"
+                Priority="-1" />
+            <ToolbarItem x:Name="_addItem" x:Key="addItem"
+                         IconImageSource="plus.png"
+                         Command="{Binding AddCipherCommand}"
+                         Order="Primary"
+                         AutomationProperties.IsInAccessibleTree="True"
+                         AutomationProperties.Name="{u:I18n AddItem}" />
 
             <DataTemplate x:Key="cipherTemplate"
                           x:DataType="pages:GroupingsPageListItem">
@@ -70,23 +81,44 @@
                     Padding="20, 0"
                     Spacing="20"
                     IsVisible="{Binding ShowNoData}">
+                    <Image
+                        Source="empty_items_state" />
                     <Label
                         Text="{Binding NoDataText}"
                         HorizontalTextAlignment="Center"></Label>
                     <Button
                         Text="{u:I18n AddAnItem}"
-                        Clicked="AddButton_Clicked"></Button>
+                        Command="{Binding AddCipherCommand}" />
                 </StackLayout>
 
+                <Frame
+                    IsVisible="{Binding ShowCallout}"
+                    Padding="10"
+                    Margin="20, 10"
+                    HasShadow="False"
+                    BackgroundColor="Transparent"
+                    BorderColor="{DynamicResource PrimaryColor}">
+                    <Label
+                        Text="{u:I18n AddTheKeyToAnExistingOrNewItem}"
+                        StyleClass="text-muted, text-sm, text-bold"
+                        HorizontalTextAlignment="Center" />
+                </Frame>
+
                 <controls:ExtendedCollectionView
                     IsVisible="{Binding ShowList}"
                     ItemsSource="{Binding GroupedItems}"
                     VerticalOptions="FillAndExpand"
                     ItemTemplate="{StaticResource listItemDataTemplateSelector}"
                     SelectionMode="Single"
-                    SelectionChanged="RowSelected"
                     StyleClass="list, list-platform"
-                    ExtraDataForLogging="Autofill Ciphers Page" />
+                    ExtraDataForLogging="Autofill Ciphers Page">
+                    <controls:ExtendedCollectionView.Behaviors>
+                        <xct:EventToCommandBehavior
+                            EventName="SelectionChanged"
+                            Command="{Binding SelectCipherCommand}"
+                            EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
+                    </controls:ExtendedCollectionView.Behaviors>
+                </controls:ExtendedCollectionView>
             </StackLayout>
         </ResourceDictionary>
     </ContentPage.Resources>
@@ -104,9 +136,10 @@
         <!-- Android FAB -->
         <Button
             x:Name="_fab"
-            Image="plus.png"
-            Clicked="AddButton_Clicked"
+            ImageSource="plus.png"
+            Command="{Binding AddCipherCommand}"
             Style="{StaticResource btn-fab}"
+            IsVisible="{OnPlatform iOS=false, Android=true}"
             AbsoluteLayout.LayoutFlags="PositionProportional"
             AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize">
             <Button.Effects>
diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs b/src/App/Pages/Vault/CipherSelectionPage.xaml.cs
similarity index 66%
rename from src/App/Pages/Vault/AutofillCiphersPage.xaml.cs
rename to src/App/Pages/Vault/CipherSelectionPage.xaml.cs
index 2d009a18e..9d08d7d8a 100644
--- a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs
+++ b/src/App/Pages/Vault/CipherSelectionPage.xaml.cs
@@ -1,7 +1,6 @@
 using System;
-using System.Linq;
 using System.Threading.Tasks;
-using Bit.App.Controls;
+using Bit.App.Abstractions;
 using Bit.App.Models;
 using Bit.App.Utilities;
 using Bit.Core.Abstractions;
@@ -12,27 +11,46 @@ using Xamarin.Forms;
 
 namespace Bit.App.Pages
 {
-    public partial class AutofillCiphersPage : BaseContentPage
+    public partial class CipherSelectionPage : BaseContentPage
     {
         private readonly AppOptions _appOptions;
         private readonly IBroadcasterService _broadcasterService;
         private readonly ISyncService _syncService;
         private readonly IVaultTimeoutService _vaultTimeoutService;
+        private readonly IAccountsManager _accountsManager;
 
-        private AutofillCiphersPageViewModel _vm;
+        private readonly CipherSelectionPageViewModel _vm;
 
-        public AutofillCiphersPage(AppOptions appOptions)
+        public CipherSelectionPage(AppOptions appOptions)
         {
             _appOptions = appOptions;
+
+            if (appOptions?.OtpData is null)
+            {
+                BindingContext = new AutofillCiphersPageViewModel();
+            }
+            else
+            {
+                BindingContext = new OTPCipherSelectionPageViewModel();
+            }
+
             InitializeComponent();
+
+            if (Device.RuntimePlatform == Device.iOS)
+            {
+                ToolbarItems.Add(_closeItem);
+                ToolbarItems.Add(_addItem);
+            }
+
             SetActivityIndicator(_mainContent);
-            _vm = BindingContext as AutofillCiphersPageViewModel;
+            _vm = BindingContext as CipherSelectionPageViewModel;
             _vm.Page = this;
             _vm.Init(appOptions);
 
             _broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
             _syncService = ServiceContainer.Resolve<ISyncService>("syncService");
             _vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
+            _accountsManager = ServiceContainer.Resolve<IAccountsManager>();
         }
 
         protected async override void OnAppearing()
@@ -51,10 +69,16 @@ namespace Bit.App.Pages
                 return;
             }
 
-            _accountAvatar?.OnAppearing();
-            _vm.AvatarImageSource = await GetAvatarImageSourceAsync();
+            // TODO: There's currently an issue on iOS where the toolbar item is not getting updated
+            // as the others somehow. Removing this so at least we get the circle with ".." instead
+            // of a white circle
+            if (Device.RuntimePlatform != Device.iOS)
+            {
+                _accountAvatar?.OnAppearing();
+                _vm.AvatarImageSource = await GetAvatarImageSourceAsync();
+            }
 
-            _broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
+            _broadcasterService.Subscribe(nameof(CipherSelectionPage), async (message) =>
             {
                 try
                 {
@@ -116,40 +140,44 @@ namespace Bit.App.Pages
             _accountAvatar?.OnDisappearing();
         }
 
-        private async void RowSelected(object sender, SelectionChangedEventArgs e)
+        private void AddButton_Clicked(object sender, System.EventArgs e)
         {
-            ((ExtendedCollectionView)sender).SelectedItem = null;
             if (!DoOnce())
             {
                 return;
             }
-            if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item && item.Cipher != null)
+
+            if (_vm is AutofillCiphersPageViewModel autofillVM)
             {
-                await _vm.SelectCipherAsync(item.Cipher, item.FuzzyAutofill);
+                AddFromAutofill(autofillVM).FireAndForget();
             }
         }
 
-        private async void AddButton_Clicked(object sender, System.EventArgs e)
+        private async Task AddFromAutofill(AutofillCiphersPageViewModel autofillVM)
         {
-            if (!DoOnce())
-            {
-                return;
-            }
             if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
             {
                 var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
                 await Navigation.PushModalAsync(new NavigationPage(pageForOther));
                 return;
             }
-            var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
+            var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: autofillVM.Uri, name: _vm.Name,
                 fromAutofill: true);
             await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
         }
 
-        private void Search_Clicked(object sender, System.EventArgs e)
+        private void Search_Clicked(object sender, EventArgs e)
         {
-            var page = new CiphersPage(null, autofillUrl: _vm.Uri);
-            Application.Current.MainPage = new NavigationPage(page);
+            var page = new CiphersPage(null, appOptions: _appOptions);
+            Navigation.PushModalAsync(new NavigationPage(page)).FireAndForget();
+        }
+
+        void CloseItem_Clicked(object sender, EventArgs e)
+        {
+            if (DoOnce())
+            {
+                _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null).FireAndForget();
+            }
         }
     }
 }
diff --git a/src/App/Pages/Vault/CipherSelectionPageViewModel.cs b/src/App/Pages/Vault/CipherSelectionPageViewModel.cs
new file mode 100644
index 000000000..1a0144a62
--- /dev/null
+++ b/src/App/Pages/Vault/CipherSelectionPageViewModel.cs
@@ -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();
+    }
+}
diff --git a/src/App/Pages/Vault/CiphersPage.xaml b/src/App/Pages/Vault/CiphersPage.xaml
index def72c316..36b9e01c0 100644
--- a/src/App/Pages/Vault/CiphersPage.xaml
+++ b/src/App/Pages/Vault/CiphersPage.xaml
@@ -81,12 +81,22 @@
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="CenterAndExpand"
                HorizontalTextAlignment="Center" />
-        <Label IsVisible="{Binding ShowNoData}"
-               Text="{u:I18n NoItemsToList}"
-               Margin="20, 0"
-               VerticalOptions="CenterAndExpand"
-               HorizontalOptions="CenterAndExpand"
-               HorizontalTextAlignment="Center" />
+        <StackLayout
+            HorizontalOptions="Center"
+            VerticalOptions="StartAndExpand"
+            Margin="20, 80, 20, 0"
+            Spacing="20"
+            IsVisible="{Binding ShowNoData}">
+            <Image
+                Source="empty_items_state" />
+            <Label
+                Text="{u:I18n ThereAreNoItemsThatMatchTheSearch}"
+                HorizontalTextAlignment="Center" />
+            <Button
+                Text="{u:I18n AddAnItem}"
+                Command="{Binding AddCipherCommand}"
+                IsVisible="{Binding ShowAddCipher}"/>
+        </StackLayout>
         <controls:ExtendedCollectionView
                 IsVisible="{Binding ShowList}"
                 ItemsSource="{Binding Ciphers}"
diff --git a/src/App/Pages/Vault/CiphersPage.xaml.cs b/src/App/Pages/Vault/CiphersPage.xaml.cs
index 610fb826c..fc53ce701 100644
--- a/src/App/Pages/Vault/CiphersPage.xaml.cs
+++ b/src/App/Pages/Vault/CiphersPage.xaml.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using Bit.App.Controls;
+using Bit.App.Models;
 using Bit.App.Resources;
 using Bit.Core.Abstractions;
 using Bit.Core.Models.View;
@@ -17,15 +18,18 @@ namespace Bit.App.Pages
         private CiphersPageViewModel _vm;
         private bool _hasFocused;
 
-        public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string vaultFilterSelection = null,
-            string autofillUrl = null, bool deleted = false)
+        public CiphersPage(Func<CipherView, bool> filter,
+            string pageTitle = null,
+            string vaultFilterSelection = null,
+            bool deleted = false,
+            AppOptions appOptions = null)
         {
             InitializeComponent();
             _vm = BindingContext as CiphersPageViewModel;
             _vm.Page = this;
-            _vm.Filter = filter;
-            _vm.AutofillUrl = _autofillUrl = autofillUrl;
-            _vm.Deleted = deleted;
+            _autofillUrl = appOptions?.Uri;
+            _vm.Prepare(filter, deleted, appOptions);
+
             if (pageTitle != null)
             {
                 _vm.PageTitle = string.Format(AppResources.SearchGroup, pageTitle);
diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs
index aaa505d92..7ca3f82d8 100644
--- a/src/App/Pages/Vault/CiphersPageViewModel.cs
+++ b/src/App/Pages/Vault/CiphersPageViewModel.cs
@@ -3,13 +3,16 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Windows.Input;
 using Bit.App.Abstractions;
+using Bit.App.Models;
 using Bit.App.Resources;
 using Bit.Core.Abstractions;
 using Bit.Core.Enums;
 using Bit.Core.Exceptions;
 using Bit.Core.Models.View;
 using Bit.Core.Utilities;
+using Xamarin.CommunityToolkit.ObjectModel;
 using Xamarin.Forms;
 
 namespace Bit.App.Pages
@@ -31,6 +34,7 @@ namespace Bit.App.Pages
         private bool _showNoData;
         private bool _showList;
         private bool _websiteIconsEnabled;
+        private AppOptions _appOptions;
 
         public CiphersPageViewModel()
         {
@@ -46,14 +50,21 @@ namespace Bit.App.Pages
             _logger = ServiceContainer.Resolve<ILogger>("logger");
 
             Ciphers = new ExtendedObservableCollection<CipherView>();
-            CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
+            CipherOptionsCommand = new AsyncCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
+                onException: ex => HandleException(ex),
+                allowsMultipleExecutions: false);
+            AddCipherCommand = new AsyncCommand(AddCipherAsync,
+                onException: ex => HandleException(ex),
+                allowsMultipleExecutions: false);
         }
 
-        public Command CipherOptionsCommand { get; set; }
+        public ICommand CipherOptionsCommand { get; }
+        public ICommand AddCipherCommand { get; }
         public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
         public Func<CipherView, bool> Filter { get; set; }
         public string AutofillUrl { get; set; }
         public bool Deleted { get; set; }
+        public bool ShowAllIfSearchTextEmpty { get; set; }
 
         protected override ICipherService cipherService => _cipherService;
         protected override IPolicyService policyService => _policyService;
@@ -65,7 +76,8 @@ namespace Bit.App.Pages
             get => _showNoData;
             set => SetProperty(ref _showNoData, value, additionalPropertyNames: new string[]
             {
-                nameof(ShowSearchDirection)
+                nameof(ShowSearchDirection),
+                nameof(ShowAddCipher)
             });
         }
 
@@ -80,12 +92,23 @@ namespace Bit.App.Pages
 
         public bool ShowSearchDirection => !ShowList && !ShowNoData;
 
+        public bool ShowAddCipher => ShowNoData && _appOptions?.OtpData != null;
+
         public bool WebsiteIconsEnabled
         {
             get => _websiteIconsEnabled;
             set => SetProperty(ref _websiteIconsEnabled, value);
         }
 
+        internal void Prepare(Func<CipherView, bool> filter, bool deleted, AppOptions appOptions)
+        {
+            Filter = filter;
+            AutofillUrl = appOptions?.Uri;
+            Deleted = deleted;
+            ShowAllIfSearchTextEmpty = appOptions?.OtpData != null;
+            _appOptions = appOptions;
+        }
+
         public async Task InitAsync()
         {
             await InitVaultFilterAsync(true);
@@ -101,25 +124,33 @@ namespace Bit.App.Pages
             {
                 List<CipherView> ciphers = null;
                 var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
-                if (searchable)
+                var shouldShowAllWhenEmpty = ShowAllIfSearchTextEmpty && string.IsNullOrEmpty(searchText);
+                if (searchable || shouldShowAllWhenEmpty)
                 {
                     if (timeout != null)
                     {
                         await Task.Delay(timeout.Value);
                     }
-                    if (searchText != (Page as CiphersPage).SearchBar.Text)
+                    if (searchText != (Page as CiphersPage).SearchBar.Text
+                        &&
+                        !shouldShowAllWhenEmpty)
                     {
                         return;
                     }
-                    else
-                    {
-                        previousCts?.Cancel();
-                    }
+
+                    previousCts?.Cancel();
                     try
                     {
                         var vaultFilteredCiphers = await GetAllCiphersAsync();
-                        ciphers = await _searchService.SearchCiphersAsync(searchText,
-                            Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
+                        if (!shouldShowAllWhenEmpty)
+                        {
+                            ciphers = await _searchService.SearchCiphersAsync(searchText,
+                                Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
+                        }
+                        else
+                        {
+                            ciphers = vaultFilteredCiphers;
+                        }
                         cts.Token.ThrowIfCancellationRequested();
                     }
                     catch (OperationCanceledException)
@@ -134,8 +165,8 @@ namespace Bit.App.Pages
                 Device.BeginInvokeOnMainThread(() =>
                 {
                     Ciphers.ResetWithRange(ciphers);
-                    ShowNoData = searchable && Ciphers.Count == 0;
-                    ShowList = searchable && !ShowNoData;
+                    ShowNoData = !shouldShowAllWhenEmpty && searchable && Ciphers.Count == 0;
+                    ShowList = (searchable || shouldShowAllWhenEmpty) && !ShowNoData;
                 });
             }, cts.Token);
             _searchCancellationTokenSource = cts;
@@ -144,6 +175,7 @@ namespace Bit.App.Pages
         public async Task SelectCipherAsync(CipherView cipher)
         {
             string selection = null;
+
             if (!string.IsNullOrWhiteSpace(AutofillUrl))
             {
                 var options = new List<string> { AppResources.Autofill };
@@ -156,6 +188,19 @@ namespace Bit.App.Pages
                 selection = await Page.DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
                     options.ToArray());
             }
+
+            if (_appOptions?.OtpData != null)
+            {
+                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;
+            }
+
             if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
             {
                 var page = new CipherDetailsPage(cipher.Id);
@@ -205,7 +250,7 @@ namespace Bit.App.Pages
 
         private void PerformSearchIfPopulated()
         {
-            if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
+            if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text) || ShowAllIfSearchTextEmpty)
             {
                 Search((Page as CiphersPage).SearchBar.Text, 200);
             }
@@ -216,12 +261,10 @@ namespace Bit.App.Pages
             PerformSearchIfPopulated();
         }
 
-        private async void CipherOptionsAsync(CipherView cipher)
+        private async Task AddCipherAsync()
         {
-            if ((Page as BaseContentPage).DoOnce())
-            {
-                await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
-            }
+            var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: _appOptions?.OtpData?.Issuer ?? _appOptions?.OtpData?.AccountName, appOptions: _appOptions);
+            await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
         }
     }
 }
diff --git a/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs b/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs
new file mode 100644
index 000000000..537823194
--- /dev/null
+++ b/src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs
@@ -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));
+        }
+    }
+}
diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs
index 19717e7ca..16763104f 100644
--- a/src/App/Resources/AppResources.Designer.cs
+++ b/src/App/Resources/AppResources.Designer.cs
@@ -1,7 +1,6 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
 // <auto-generated>
 //     This code was generated by a tool.
-//     Runtime Version:4.0.30319.42000
 //
 //     Changes to this file may cause incorrect behavior and will be lost if
 //     the code is regenerated.
@@ -14,12 +13,10 @@ namespace Bit.App.Resources {
     
     /// <summary>
     ///   A strongly-typed resource class, for looking up localized strings, etc.
+    ///   This class was generated by MSBuild using the GenerateResource task.
+    ///   To add or remove a member, edit your .resx file then rerun MSBuild.
     /// </summary>
-    // This class was auto-generated by the StronglyTypedResourceBuilder
-    // class via a tool like ResGen or Visual Studio.
-    // To add or remove a member, edit your .ResX file then rerun ResGen
-    // with the /str option, or rebuild your VS project.
-    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
     public class AppResources {
@@ -385,6 +382,15 @@ namespace Bit.App.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Add the key to an existing or new item.
+        /// </summary>
+        public static string AddTheKeyToAnExistingOrNewItem {
+            get {
+                return ResourceManager.GetString("AddTheKeyToAnExistingOrNewItem", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Add TOTP.
         /// </summary>
@@ -5339,6 +5345,15 @@ namespace Bit.App.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Search for an item or add a new item.
+        /// </summary>
+        public static string SearchForAnItemOrAddANewItem {
+            get {
+                return ResourceManager.GetString("SearchForAnItemOrAddANewItem", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Search {0}.
         /// </summary>
@@ -6032,6 +6047,24 @@ namespace Bit.App.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to There are no items in your vault that match &quot;{0}&quot;.
+        /// </summary>
+        public static string ThereAreNoItemsInYourVaultThatMatchX {
+            get {
+                return ResourceManager.GetString("ThereAreNoItemsInYourVaultThatMatchX", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to There are no items that match the search.
+        /// </summary>
+        public static string ThereAreNoItemsThatMatchTheSearch {
+            get {
+                return ResourceManager.GetString("ThereAreNoItemsThatMatchTheSearch", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to 30 days.
         /// </summary>
diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx
index 3f40e4bcd..61742c5f6 100644
--- a/src/App/Resources/AppResources.resx
+++ b/src/App/Resources/AppResources.resx
@@ -2592,4 +2592,16 @@ Do you want to switch to this account?</value>
   <data name="OrganizationSsoIdentifierRequired" xml:space="preserve">
     <value>Organization SSO identifier required.</value>
   </data>
+  <data name="AddTheKeyToAnExistingOrNewItem" xml:space="preserve">
+    <value>Add the key to an existing or new item</value>
+  </data>
+  <data name="ThereAreNoItemsInYourVaultThatMatchX" xml:space="preserve">
+    <value>There are no items in your vault that match "{0}"</value>
+  </data>
+  <data name="SearchForAnItemOrAddANewItem" xml:space="preserve">
+    <value>Search for an item or add a new item</value>
+  </data>
+  <data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
+    <value>There are no items that match the search</value>
+  </data>
 </root>
diff --git a/src/App/Services/DeepLinkContext.cs b/src/App/Services/DeepLinkContext.cs
new file mode 100644
index 000000000..6c696aa70
--- /dev/null
+++ b/src/App/Services/DeepLinkContext.cs
@@ -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;
+        }
+    }
+}
diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs
index c8cf16a38..b08714b19 100644
--- a/src/App/Utilities/AccountManagement/AccountsManager.cs
+++ b/src/App/Utilities/AccountManagement/AccountsManager.cs
@@ -7,7 +7,6 @@ using Bit.Core.Abstractions;
 using Bit.Core.Enums;
 using Bit.Core.Models.Domain;
 using Bit.Core.Utilities;
-using Xamarin.Essentials;
 using Xamarin.Forms;
 
 namespace Bit.App.Utilities.AccountManagement
@@ -58,6 +57,13 @@ namespace Bit.App.Utilities.AccountManagement
             _broadcasterService.Subscribe(nameof(AccountsManager), OnMessage);
         }
 
+        public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
+        {
+            appOptionsAction(Options);
+
+            await NavigateOnAccountChangeAsync();
+        }
+
         public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
         {
             // TODO: this could be improved by doing chain of responsability pattern
@@ -89,6 +95,10 @@ namespace Bit.App.Utilities.AccountManagement
                 {
                     _accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
                 }
+                else if (Options.OtpData != null)
+                {
+                    _accountsManagerHost.Navigate(NavigationTarget.OtpCipherSelection);
+                }
                 else if (Options.CreateSend != null)
                 {
                     _accountsManagerHost.Navigate(NavigationTarget.SendAddEdit);
diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs
index 07870b0e1..48c0fb73a 100644
--- a/src/App/Utilities/AppHelpers.cs
+++ b/src/App/Utilities/AppHelpers.cs
@@ -430,9 +430,11 @@ namespace Bit.App.Utilities
                     Application.Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
                     return true;
                 }
-                if (appOptions.Uri != null)
+                if (appOptions.Uri != null
+                    ||
+                    appOptions.OtpData != null)
                 {
-                    Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
+                    Application.Current.MainPage = new NavigationPage(new CipherSelectionPage(appOptions));
                     return true;
                 }
                 if (appOptions.CreateSend != null)
diff --git a/src/App/Utilities/AppSetup.cs b/src/App/Utilities/AppSetup.cs
index ff7d7eea6..02ae89eea 100644
--- a/src/App/Utilities/AppSetup.cs
+++ b/src/App/Utilities/AppSetup.cs
@@ -1,4 +1,6 @@
-using Bit.App.Lists.ItemViewModels.CustomFields;
+using Bit.App.Abstractions;
+using Bit.App.Lists.ItemViewModels.CustomFields;
+using Bit.App.Services;
 using Bit.Core.Abstractions;
 using Bit.Core.Utilities;
 
@@ -18,6 +20,7 @@ namespace Bit.App.Utilities
 
             // TODO: This could be further improved by Lazy Registration since it may not be needed at all
             ServiceContainer.Register<ICustomFieldItemFactory>("customFieldItemFactory", new CustomFieldItemFactory(i18nService, eventService));
+            ServiceContainer.Register<IDeepLinkContext>(new DeepLinkContext(ServiceContainer.Resolve<IMessagingService>()));
         }
     }
 }
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index beb8c6d99..5db98b756 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -46,6 +46,7 @@
         /// which is used to handle Apple Watch state logic
         /// </summary>
         public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch";
+        public const string OtpAuthScheme = "otpauth";
         public const string AppLocaleKey = "appLocale";
         public const string ClearSensitiveFields = "clearSensitiveFields";
         public const int SelectFileRequestCode = 42;
diff --git a/src/Core/Enums/NavigationTarget.cs b/src/Core/Enums/NavigationTarget.cs
index d226ff283..8cb83ebf5 100644
--- a/src/Core/Enums/NavigationTarget.cs
+++ b/src/Core/Enums/NavigationTarget.cs
@@ -8,6 +8,7 @@
         Home,
         AddEditCipher,
         AutofillCiphers,
-        SendAddEdit
+        SendAddEdit,
+        OtpCipherSelection
     }
 }
diff --git a/src/Core/Services/TotpService.cs b/src/Core/Services/TotpService.cs
index fe0b83649..4f1e453ec 100644
--- a/src/Core/Services/TotpService.cs
+++ b/src/Core/Services/TotpService.cs
@@ -33,39 +33,22 @@ namespace Bit.Core.Services
             var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false;
             if (isOtpAuth)
             {
-                var qsParams = CoreHelpers.GetQueryParams(key);
-                if (qsParams.ContainsKey("digits") && qsParams["digits"] != null &&
-                    int.TryParse(qsParams["digits"].Trim(), out var digitParam))
+                var otpData = new OtpData(key.ToLowerInvariant());
+                if (otpData.Digits > 0)
                 {
-                    if (digitParam > 10)
-                    {
-                        digits = 10;
-                    }
-                    else if (digitParam > 0)
-                    {
-                        digits = digitParam;
-                    }
+                    digits = Math.Min(otpData.Digits.Value, 10);
                 }
-                if (qsParams.ContainsKey("period") && qsParams["period"] != null &&
-                    int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0)
+                if (otpData.Period.HasValue)
                 {
-                    period = periodParam;
+                    period = otpData.Period.Value;
                 }
-                if (qsParams.ContainsKey("secret") && qsParams["secret"] != null)
+                if (otpData.Secret != null)
                 {
-                    keyB32 = qsParams["secret"];
+                    keyB32 = otpData.Secret;
                 }
-                if (qsParams.ContainsKey("algorithm") && qsParams["algorithm"] != null)
+                if (otpData.Algorithm.HasValue)
                 {
-                    var algParam = qsParams["algorithm"].ToLowerInvariant();
-                    if (algParam == "sha256")
-                    {
-                        alg = CryptoHashAlgorithm.Sha256;
-                    }
-                    else if (algParam == "sha512")
-                    {
-                        alg = CryptoHashAlgorithm.Sha512;
-                    }
+                    alg = otpData.Algorithm.Value;
                 }
             }
             else if (isSteamAuth)
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index 8c63374e3..2fa4f4ad0 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -168,10 +168,27 @@ namespace Bit.Core.Utilities
         {
             try
             {
-                if (!Uri.TryCreate(urlString, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Query))
+                if (Uri.TryCreate(urlString, UriKind.Absolute, out var uri))
+                {
+                    return GetQueryParams(uri);
+                }
+            }
+            catch (Exception ex)
+            {
+                LoggerHelper.LogEvenIfCantBeResolved(ex);
+            }
+            return new Dictionary<string, string>();
+        }
+
+        public static Dictionary<string, string> GetQueryParams(Uri uri)
+        {
+            try
+            {
+                if (string.IsNullOrWhiteSpace(uri.Query))
                 {
                     return new Dictionary<string, string>();
                 }
+
                 var queryStringNameValueCollection = HttpUtility.ParseQueryString(uri.Query);
                 return queryStringNameValueCollection.AllKeys.Where(k => k != null).ToDictionary(k => k, k => queryStringNameValueCollection[k]);
             }
diff --git a/src/Core/Utilities/OtpData.cs b/src/Core/Utilities/OtpData.cs
new file mode 100644
index 000000000..5306f8adf
--- /dev/null
+++ b/src/Core/Utilities/OtpData.cs
@@ -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; }
+    }
+}
diff --git a/src/Core/Utilities/UriExtensions.cs b/src/Core/Utilities/UriExtensions.cs
new file mode 100644
index 000000000..aee97136e
--- /dev/null
+++ b/src/Core/Utilities/UriExtensions.cs
@@ -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;
+        }
+    }
+}
diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs
index 05f294df5..284f47299 100644
--- a/src/iOS/AppDelegate.cs
+++ b/src/iOS/AppDelegate.cs
@@ -43,6 +43,8 @@ namespace Bit.iOS
         private IStateService _stateService;
         private IEventService _eventService;
 
+        private LazyResolve<IDeepLinkContext> _deepLinkContext = new LazyResolve<IDeepLinkContext>();
+
         public override bool FinishedLaunching(UIApplication app, NSDictionary options)
         {
             Forms.Init();
@@ -239,7 +241,7 @@ namespace Bit.iOS
 
         public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
         {
-            return Xamarin.Essentials.Platform.OpenUrl(app, url, options);
+            return _deepLinkContext.Value.OnNewUri(url) || Xamarin.Essentials.Platform.OpenUrl(app, url, options);
         }
 
         public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist
index 2a07b3ba4..86e7604fe 100644
--- a/src/iOS/Info.plist
+++ b/src/iOS/Info.plist
@@ -29,6 +29,14 @@
 			<key>CFBundleURLName</key>
 			<string>com.8bit.bitwarden.url</string>
 		</dict>
+		<dict>
+			<key>CFBundleURLName</key>
+			<string>com.8bit.bitwarden</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>otpauth</string>
+			</array>
+		</dict>
 	</array>
 	<key>CFBundleLocalizations</key>
 	<array>
diff --git a/src/iOS/Resources/Assets.xcassets/Contents.json b/src/iOS/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/src/iOS/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json b/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
new file mode 100644
index 000000000..2c8771189
--- /dev/null
+++ b/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
@@ -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
+  }
+}
diff --git a/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf b/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..f68a9a3b66fd8447ebe1690f4e7d1d90bd5e3542
GIT binary patch
literal 1891
zcmZWq3p5mK9KR{9c9js)gRkUu+1(lYNRf&~QXWesQjIm%vUW5(D{@XRi(7iAl*lDS
zIt~&}w-+LMOLfcbA{9y~ZYp}vomr9NerL}2&G(z{|Ns3TGruwM<U2E9CWmHH{<i2V
zjRPQn1%}e>>;P*uDiskSfJ;IG8UTRhES3-`POcJ!K>4T;3qoo3_B0uRqewU{D!p*s
z9jVXa87*HezfBI`xFg1P&ccOyRoW%9S`IJIFWutX_ch%c-S*IRiPfI$=At(L`?5P|
z;-Nv?sfG*S(_M#b2Wo0PWgg|*)dv|m4sI9o-Yl#5hz%bY?)FF-Vwnxk8?L;3fAcA=
zp`As81H%dW@-Pn@iE&xlmFv6JYg*^8n^t-g?dVzdS(91)RHx~1&91jrS?hh7QQ?Ev
zL+WQ79^(1+d6fazP@!1B-<|^nX59Mz{c+{p<@{S7d-Ue-JgXL;e56`Mv`hQ6!I5fF
zD3Z+3v^%Jpr`VL{ZCtxJ$xK$YeU*QOzRi`|fWDr74Y{=QeM3qYdh2|TM(BrrwY1~W
z4U*n4{M=w<K-;pqm*yL2ov!UnuPWC0A>%|9R%OPEty5ATzn;6$RFLX_Q`Vyj9kmUV
ztmgE{dk69rdOAm+S?0vAtFvy5)7zC&`rv_Tn|9cWq_A6mmPdTbO=xC*$&HI|d=)yG
zPDmtY8+=OqQ<*LW`?|A_b-8eo!lR`LE8Qji*&DNJ*Ho0SkK?*6aR)Z*yl0^OdACaw
z^w-|Q5<i^O9&)J2-M_=@o<Yh3qbv1aAA;7qdI7<g2A8l3(pv2ao4$c3V6XnwqQag2
zx~+nbRbJocu<Po&*)r2(Nyf8t687>g_0t=#ZK>c(Uc88znj73|C7zsMlpXu`x&B9f
zk`2tVZ-#T|k#6oy$uUpT_qj9ZuV)tWk6)9g^>p6YTN$~-4t4fU*FAZ7v&!06)pcv;
zwdQo?xp>V>j16<k_~cVw(-aXAV%T$;YXIABRUoGLs`__x{-NLY4DU2NWz}4I@u5@O
zC;fL#8uT)!{E(@fjSdeOC0hBGyKDE{=BaBlCN0>rOOUL&_KztA<|eD|tE$I(r#lAk
zR(af0e0zO$@#iQ;t6k>XcH5;3h84n8#x--ZxkdpU>~<|rt#%zX%k`pP8tDtQOFE|h
z>b_=XtiqznzT0lY!D&?ufwL7Y=4H_D3Zjg`QsXFQ&z@N>h|WcsC*Bs_aKD<+arH|>
zj8u{1YWjg_kVbdf+hfho8G4bbF;fbwFZlkoEq?lq(mb~ox+wBRNVM>j@63|<)|gfw
zbpDyzbEAO$=W<uy2%8jhIXhK0aJEgQ_S=Uik$o9SQ)X?mZ{L5RuwUJ{cBy9Znaqw?
zS9B3E8?`)>^zX}Fy4ihA@nCli()UoH#na80AvY5JR!FGzE{hXS-;`K(Ftu6|h;FfT
za`k(3-6^YJeuBY(Z&nSoAVx#K$lzqL)6<S)^s*%Xdcza@v+rK0SF3Tjmz|07oZpr9
zdZ=A5yjlsdC2!u>a<|!Zsbm}Jt>hKcO*4&}v6@t$N*XE+`tk9Tca&^UT0yO(TEGwt
zQ3t3kdrVc7I^q;XxOfw!h5$P|3LpmyfyfPDIj#UOU<na&CY6PJK+fd#!;kftAHed7
zjz9s+6A_`n1i(tao(PUg3DR^_u-2eKV#EQ90{$cf0hr5UF5&_hhM3k|5TLYi#H0ji
zBm<Q4jmVq}j>#j&g*L*ZL`S3bCi!thDvO{Ph0&w1D<E(=I=XWtPw~-6u@DuwI8fk7
zMl1m;!{oRSl>w?Qf^qp#k&mkH&-`SREz3<DBqQ^nN+1}ET~4Jt&H-<n15X?ak_$<_
zHWwmd90M>D<}zVxU>-sc5ldKPkx5j9BM~8Dp^S;)A`1&8kt3<*gcy_ZsRo(zmsmsA
zY={jlf^FDr4&-Msj&9L7x{-Jt5dx85BBdj$H?kky_}RlK91W&Hz=j5m4Z!1a*<26|
zeqb=inydshfOHIl7n5ugF|v&lFbIPGhmj2@%QnH5V?!2e0>&NBLEwm3g5t_wp|^ND
rN`_4JL4aWdpb{T>SzM*T7@*3o?4yi8a6*|O%;PMknV2|vIMe<Ew%*VF

literal 0
HcmV?d00001

diff --git a/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf b/src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..1aa4dc22fd4f083f775ca4ea6a65bdcb4e71ead6
GIT binary patch
literal 1875
zcmZWq4Ok3m9B0w7r8rzyhep}#s&;1EomPa~cI`?!wLB;m(>Ar6+Sz7j(?(L+)XGOG
z$rEwD>*(`*Q*20x;!bpTLc-;<l9F^Y+lL<aKJ&~w@9&-e$M64t=Qj*rk=TX9=Fu6y
zzdq4P=K&Ca1%}c+JOFS$stO?#08BzsIsgEmSQ$p3I5~$Q1S&%1SP)8|HH)qxa1;rr
zM`a&hd`p$hw>aO#YNh2FKHY~iX@l{(xJC5vi!_$u!=;y#@^fsYMHg1&CzR<^M@sCx
zQXSIL+LKCm)JrBOF6xSS{zhgo^OpT};n8#;rB-tN%iWwW+w!YpJ8z@i#^l&-vE66E
zSb@W*QrUaE&ljw<t4r0jVg;;A9x}rscGHE>efKZmZ8=>M>&LGuBO9PqGtBMdp2cS-
zE)i=Imo_C-$7hObC&WG~?YO^VOU0YY*wNCA(0yxIEN8VDQ?Ppy>$8tbO3|~p9C;jT
ze`e#oxe=BJ%f~~lH9Ox9v&1^tQEj@!>3l->Lh_*~$Uo9d`?1B$cBk%+!cK!1v_E&f
zw#H-P#H`lFeErHI=DX&n!zN0$`^QuX0_`)-=sm8j7{ooZDV^_V+ji!oWRd6QWWn1E
zKcSO6tI5;6?8bu^g0T-)n8z<Szj|C>49;w5+qW`6(H?fftzgU6EiPksPmOe{d8R-k
zvPz^~DT!+W_JX{GhH?YDa$N3%hM#;2((@EXc&}r_vdTuEj1a5*+})3Uc*$pc(y#OO
zHz=!4r>uXhopso(Be;a)EX%HH%56MlwP(oZ?murPSw+jdyH8H7l71-j>%J8cC~VQE
zrp>Du4c{?}uKd-Zcbeyr48Du=Y+|JEjwx=p7{5Mg>k?ztg1p1%nLEvE!<-c<*(X_b
zce4$a%(^O8K%8^J4aooe?IE#EM^6Wfh1ug`BsHlh`?u<Qf|x(Ph@}%YC#?3~+m;u)
z>6ClD?-WHLJhTvGJEqLGEPoIqWC(0>N9t+m`qx=bGG64_f;n7g-@*HzwE8U@;dN@`
zh)MS{poR7s;iD!8&_rSJ<2m>D=lYbU83f{Lr$LwN%NjlW_x%iaZf?zZ=RND6N<Jn#
z*YL~jq}h*)P%m}n7?t5fvrY5xis8*8xaoP^4Nb$oPp)m=IOK?<L2x-eJV#47Uo%^!
zSZllSZ)ORUw1F_VfM$d_%wvWgYNp><4n5qnO{QLOcWp~oTTO{L^`>k1VU<o{m%~@>
zk{1?CUFyIbw?4f3qRsU2`5A02nC-7W@N7$5pkH}>#X6|WSP}i+I6Ls_$wTw6Sr1x$
z?$=$KkH?<+m$$dNM85OdKJ-V>y*wrVc*{xqD>JR~t@m7MJkLN7Zjkqp?eCx1jhS+L
zfwKM4CS!f)!N9sjQ8xEtG{;IV+pPO{Y@&NRt;QpmUYvX=Zm}VacO;oFSR%CKwk_Q+
z6qVT;>q4(KuJ()UI7@SB*jfCY;{j&N@#3-q-{KdAH?QvLeAQX8q4r_tMjOQ2YRZX1
zqo4JtE}o!Bmj!ivezBgGCYgVUj;exszjpG8(g{i&sF{=pcXycDr&ip46*1|?oG3!F
zkRUY!@bI7ja)1a#E&xy{05||BN60&=B;*EpC$C;VuKj)haFI@p0-!Gvf&vVHWAf{Z
z;HZiqP5T7O2Mtmpo>&yHjD#S71G};LFu>tJY*!eNnQVlZiXe?N0Ht(2vL?YXt$ILe
zJ&{akU$zU$_&B1{sHuo@T_5ZP5V#iYTM5axL};W^j!Gq-6xhQEl%g6;i_1|BK-EP$
zfFG54pW1%qr=e^?Z)K2%<Uy4{+Mm0YqC3EW+W-f?I2NRpllsh*BT5_tIBX8g=C}e(
z1wp8%gJh9OGz3S~3Z-1b#_$jp%M?gRsyCs;R3fTDOwn{#$dwCmAwFjsm&=0!SOdcG
z2ZZa%SBMZu7#3o3MD<3~+l{Y1jKa}iIs{CkL;VAAgJCWV1OvSohX;}8h#G*ZALC3T
ztMe@exqpj65a<84&@?jBZ)|yd@<@Dx!2@FuIHC+ganrAFp)wjJQ>OYL#V`V(i1!>8
dFI6xGP-QpuQ9~d&VPeQ}<8tW?#+(IW`hVw|&?*1`

literal 0
HcmV?d00001

diff --git a/src/iOS/Resources/Assets.xcassets/ic_warning.imageset/Contents.json b/src/iOS/Resources/Assets.xcassets/ic_warning.imageset/Contents.json
index 3c275c296..5a1234c62 100644
--- a/src/iOS/Resources/Assets.xcassets/ic_warning.imageset/Contents.json
+++ b/src/iOS/Resources/Assets.xcassets/ic_warning.imageset/Contents.json
@@ -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"
-    },
-    {
-      "scale": "1x",
-      "idiom": "mac"
-    },
-    {
-      "scale": "2x",
-      "idiom": "mac"
-    },
-    {
-      "idiom": "car"
-    },
-    {
-      "scale": "2x",
-      "idiom": "car"
-    },
-    {
-      "scale": "3x",
-      "idiom": "car"
-    },
-    {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "idiom": "universal"
+      "idiom" : "universal"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "1x",
-      "idiom": "universal"
+      "idiom" : "universal"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "universal"
+      "idiom" : "universal",
+      "scale" : "1x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "3x",
-      "idiom": "universal"
+      "idiom" : "universal",
+      "scale" : "1x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "iphone"
+      "idiom" : "universal",
+      "scale" : "1x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "1x",
-      "idiom": "iphone"
+      "idiom" : "universal",
+      "scale" : "2x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "iphone"
+      "idiom" : "universal",
+      "scale" : "2x"
     },
     {
-      "subtype": "retina4",
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "2x",
-      "idiom": "iphone"
+      "idiom" : "universal",
+      "scale" : "2x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "3x",
-      "idiom": "iphone"
+      "idiom" : "universal",
+      "scale" : "3x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "idiom": "ipad"
+      "idiom" : "universal",
+      "scale" : "3x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "1x",
-      "idiom": "ipad"
+      "idiom" : "universal",
+      "scale" : "3x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "ipad"
+      "idiom" : "iphone"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "idiom": "watch"
+      "idiom" : "iphone"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "iphone"
     },
     {
-      "screenWidth": "{130,145}",
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "iphone",
+      "scale" : "1x"
     },
     {
-      "screenWidth": "{146,165}",
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "iphone",
+      "scale" : "1x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "mac"
+      "idiom" : "iphone",
+      "scale" : "1x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "1x",
-      "idiom": "mac"
+      "idiom" : "iphone",
+      "scale" : "2x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "mac"
+      "idiom" : "iphone",
+      "scale" : "2x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "car"
+      "idiom" : "iphone",
+      "scale" : "2x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "dark"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "car"
+      "idiom" : "iphone",
+      "scale" : "3x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "dark"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "3x",
-      "idiom": "car"
+      "idiom" : "iphone",
+      "scale" : "3x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "universal"
+      "idiom" : "iphone",
+      "scale" : "3x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "scale": "1x",
-      "idiom": "universal"
+      "idiom" : "iphone",
+      "scale" : "1x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "universal"
+      "idiom" : "iphone",
+      "scale" : "1x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "3x",
-      "idiom": "universal"
+      "idiom" : "iphone",
+      "scale" : "1x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "idiom": "iphone"
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "1x",
-      "idiom": "iphone"
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "2x",
-      "idiom": "iphone"
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "subtype" : "retina4"
     },
     {
-      "subtype": "retina4",
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "iphone"
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "3x",
-      "idiom": "iphone"
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "ipad"
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "subtype" : "retina4"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "scale": "1x",
-      "idiom": "ipad"
+      "idiom" : "ipad"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "ipad"
+      "idiom" : "ipad"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "idiom": "watch"
+      "idiom" : "ipad"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "ipad",
+      "scale" : "1x"
     },
     {
-      "screenWidth": "{130,145}",
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "ipad",
+      "scale" : "1x"
     },
     {
-      "screenWidth": "{146,165}",
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "2x",
-      "idiom": "watch"
+      "idiom" : "ipad",
+      "scale" : "1x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "idiom": "mac"
+      "idiom" : "ipad",
+      "scale" : "2x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "1x",
-      "idiom": "mac"
+      "idiom" : "ipad",
+      "scale" : "2x"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "2x",
-      "idiom": "mac"
+      "idiom" : "ipad",
+      "scale" : "2x"
     },
     {
-      "appearances": [
-        {
-          "appearance": "luminosity",
-          "value": "light"
-        }
-      ],
-      "idiom": "car"
+      "idiom" : "car"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "light"
         }
       ],
-      "scale": "2x",
-      "idiom": "car"
+      "idiom" : "car"
     },
     {
-      "appearances": [
+      "appearances" : [
         {
-          "appearance": "luminosity",
-          "value": "light"
+          "appearance" : "luminosity",
+          "value" : "dark"
         }
       ],
-      "scale": "3x",
-      "idiom": "car"
+      "idiom" : "car"
+    },
+    {
+      "idiom" : "car",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "car",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "car",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "car",
+      "scale" : "3x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "car",
+      "scale" : "3x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "car",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "mac"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "mac"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "mac"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "mac",
+      "scale" : "1x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "mac",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "mac",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "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"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "watch"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "watch"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">145"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">145"
     }
   ],
-  "info": {
-    "version": 1,
-    "author": "xcode"
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
   }
-}
\ No newline at end of file
+}
diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj
index f197763bb..ba4127910 100644
--- a/src/iOS/iOS.csproj
+++ b/src/iOS/iOS.csproj
@@ -179,6 +179,10 @@
     <BundleResource Include="Resources\generate.png" />
     <BundleResource Include="Resources\generate%402x.png" />
     <BundleResource Include="Resources\generate%403x.png" />
+    <ImageAsset Include="Resources\Assets.xcassets\Contents.json" />
+    <ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Empty-items-state-dark.pdf" />
+    <ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Empty-items-state.pdf" />
+    <ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Contents.json" />
   </ItemGroup>
   <ItemGroup>
     <InterfaceDefinition Include="LaunchScreen.storyboard" />