diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index ecb9ca277..e985e2cf9 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -20,6 +20,7 @@ using System.Net; using Bit.App.Utilities; using Bit.App.Pages; using Bit.App.Utilities.AccountManagement; +using Bit.App.Utilities.Helpers; #if !FDROID using Android.Gms.Security; #endif @@ -71,6 +72,15 @@ namespace Bit.Droid ServiceContainer.Resolve("platformUtilsService"), ServiceContainer.Resolve("authService")); ServiceContainer.Register("accountsManager", accountsManager); + + var cipherHelper = new CipherHelper( + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("eventService"), + ServiceContainer.Resolve("vaultTimeoutService"), + ServiceContainer.Resolve("clipboardService"), + ServiceContainer.Resolve("passwordRepromptService") + ); + ServiceContainer.Register("cipherHelper", cipherHelper); } #if !FDROID if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat) diff --git a/src/App/App.csproj b/src/App/App.csproj index a6ba8249d..f12a402cf 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -129,6 +129,7 @@ + @@ -422,5 +423,6 @@ + diff --git a/src/App/Controls/IconFontImageSource.cs b/src/App/Controls/IconFontImageSource.cs new file mode 100644 index 000000000..f3813d79f --- /dev/null +++ b/src/App/Controls/IconFontImageSource.cs @@ -0,0 +1,20 @@ +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class IconFontImageSource : FontImageSource + { + public IconFontImageSource() + { + switch (Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "bwi-font"; + break; + case Device.Android: + FontFamily = "bwi-font.ttf#bwi-font"; + break; + } + } + } +} diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs index 80e9caf09..3855acdfc 100644 --- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs +++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs @@ -6,6 +6,7 @@ using Bit.App.Controls; using Bit.App.Models; using Bit.App.Resources; using Bit.App.Utilities; +using Bit.App.Utilities.Helpers; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; @@ -25,6 +26,7 @@ namespace Bit.App.Pages private readonly IStateService _stateService; private readonly IPasswordRepromptService _passwordRepromptService; private readonly IMessagingService _messagingService; + private readonly ICipherHelper _cipherHelper; private readonly ILogger _logger; private bool _showNoData; @@ -40,6 +42,7 @@ namespace Bit.App.Pages _stateService = ServiceContainer.Resolve("stateService"); _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); _messagingService = ServiceContainer.Resolve("messagingService"); + _cipherHelper = ServiceContainer.Resolve("cipherHelper"); _logger = ServiceContainer.Resolve("logger"); GroupedItems = new ObservableRangeCollection(); @@ -180,7 +183,7 @@ namespace Bit.App.Pages } if (_deviceActionService.SystemMajorVersion() < 21) { - await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + await _cipherHelper.ShowCipherOptionsAsync(Page, cipher); } else { @@ -241,7 +244,7 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + await _cipherHelper.ShowCipherOptionsAsync(Page, cipher); } } } diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs index c1c8fcd03..b38bc000c 100644 --- a/src/App/Pages/Vault/CiphersPageViewModel.cs +++ b/src/App/Pages/Vault/CiphersPageViewModel.cs @@ -5,12 +5,12 @@ using System.Threading; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Resources; +using Bit.App.Utilities.Helpers; 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 @@ -25,6 +25,7 @@ namespace Bit.App.Pages private readonly IPasswordRepromptService _passwordRepromptService; private readonly IOrganizationService _organizationService; private readonly IPolicyService _policyService; + private readonly ICipherHelper _cipherHelper; private CancellationTokenSource _searchCancellationTokenSource; private readonly ILogger _logger; @@ -42,6 +43,7 @@ namespace Bit.App.Pages _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); _organizationService = ServiceContainer.Resolve("organizationService"); _policyService = ServiceContainer.Resolve("policyService"); + _cipherHelper = ServiceContainer.Resolve("cipherHelper"); _logger = ServiceContainer.Resolve("logger"); Ciphers = new ExtendedObservableCollection(); @@ -193,7 +195,7 @@ namespace Bit.App.Pages } if (_deviceActionService.SystemMajorVersion() < 21) { - await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + await _cipherHelper.ShowCipherOptionsAsync(Page, cipher); } else { @@ -219,7 +221,7 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + await _cipherHelper.ShowCipherOptionsAsync(Page, cipher); } } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml index 0ba3203b3..cf339ebf3 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml @@ -53,6 +53,45 @@ WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" /> + + + + + + + + + + + + + + + + + + + + + + + + + GroupTemplate="{StaticResource groupTemplate}" + LoginCipherTemplate="{StaticResource loginCipherTemplate}" /> ("platformUtilsService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); - _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); _organizationService = ServiceContainer.Resolve("organizationService"); _policyService = ServiceContainer.Resolve("policyService"); + _cipherHelper = ServiceContainer.Resolve("cipherHelper"); _logger = ServiceContainer.Resolve("logger"); - + Loading = true; GroupedItems = new ObservableRangeCollection(); RefreshCommand = new Command(async () => @@ -74,6 +75,8 @@ namespace Bit.App.Pages await LoadAsync(); }); CipherOptionsCommand = new Command(CipherOptionsAsync); + CopyUsernameItemCommand = new AsyncCommand(CopyUsernameItemAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + CopyPasswordItemCommand = new AsyncCommand(CopyPasswordItemAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) { @@ -157,6 +160,9 @@ namespace Bit.App.Pages public ObservableRangeCollection GroupedItems { get; set; } public Command RefreshCommand { get; set; } public Command CipherOptionsCommand { get; set; } + public IAsyncCommand CopyUsernameItemCommand { get; } + public IAsyncCommand CopyPasswordItemCommand { get; } + public bool LoadedOnce { get; set; } public async Task LoadAsync() @@ -628,7 +634,23 @@ namespace Bit.App.Pages { if ((Page as BaseContentPage).DoOnce()) { - await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService); + await _cipherHelper.ShowCipherOptionsAsync(Page, cipher); + } + } + + private async Task CopyUsernameItemAsync(IGroupingsPageListItem listItem) + { + if (listItem is GroupingsPageListItem groupPageListItem && groupPageListItem.Cipher?.Type == CipherType.Login) + { + await _cipherHelper.CopyUsernameAsync(groupPageListItem.Cipher); + } + } + + private async Task CopyPasswordItemAsync(IGroupingsPageListItem listItem) + { + if (listItem is GroupingsPageListItem groupPageListItem && groupPageListItem.Cipher?.Type == CipherType.Login) + { + await _cipherHelper.CopyPasswordAsync(groupPageListItem.Cipher); } } } diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index fd61c4320..e258e1519 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -24,132 +24,6 @@ namespace Bit.App.Utilities { public static class AppHelpers { - public static async Task CipherListOptions(ContentPage page, CipherView cipher, IPasswordRepromptService passwordRepromptService) - { - var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); - var eventService = ServiceContainer.Resolve("eventService"); - var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); - var clipboardService = ServiceContainer.Resolve("clipboardService"); - - var options = new List { AppResources.View }; - if (!cipher.IsDeleted) - { - options.Add(AppResources.Edit); - } - if (cipher.Type == Core.Enums.CipherType.Login) - { - if (!string.IsNullOrWhiteSpace(cipher.Login.Username)) - { - options.Add(AppResources.CopyUsername); - } - if (!string.IsNullOrWhiteSpace(cipher.Login.Password) && cipher.ViewPassword) - { - options.Add(AppResources.CopyPassword); - } - if (!string.IsNullOrWhiteSpace(cipher.Login.Totp)) - { - var stateService = ServiceContainer.Resolve("stateService"); - var canAccessPremium = await stateService.CanAccessPremiumAsync(); - if (canAccessPremium || cipher.OrganizationUseTotp) - { - options.Add(AppResources.CopyTotp); - } - } - if (cipher.Login.CanLaunch) - { - options.Add(AppResources.Launch); - } - } - else if (cipher.Type == Core.Enums.CipherType.Card) - { - if (!string.IsNullOrWhiteSpace(cipher.Card.Number)) - { - options.Add(AppResources.CopyNumber); - } - if (!string.IsNullOrWhiteSpace(cipher.Card.Code)) - { - options.Add(AppResources.CopySecurityCode); - } - } - else if (cipher.Type == Core.Enums.CipherType.SecureNote) - { - if (!string.IsNullOrWhiteSpace(cipher.Notes)) - { - options.Add(AppResources.CopyNotes); - } - } - var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, options.ToArray()); - if (await vaultTimeoutService.IsLockedAsync()) - { - platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked); - } - else if (selection == AppResources.View) - { - await page.Navigation.PushModalAsync(new NavigationPage(new ViewPage(cipher.Id))); - } - else if (selection == AppResources.Edit) - { - if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync()) - { - await page.Navigation.PushModalAsync(new NavigationPage(new AddEditPage(cipher.Id))); - } - } - else if (selection == AppResources.CopyUsername) - { - await clipboardService.CopyTextAsync(cipher.Login.Username); - platformUtilsService.ShowToastForCopiedValue(AppResources.Username); - } - else if (selection == AppResources.CopyPassword) - { - if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync()) - { - await clipboardService.CopyTextAsync(cipher.Login.Password); - platformUtilsService.ShowToastForCopiedValue(AppResources.Password); - var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id); - } - } - else if (selection == AppResources.CopyTotp) - { - if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync()) - { - var totpService = ServiceContainer.Resolve("totpService"); - var totp = await totpService.GetCodeAsync(cipher.Login.Totp); - if (!string.IsNullOrWhiteSpace(totp)) - { - await clipboardService.CopyTextAsync(totp); - platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp); - } - } - } - else if (selection == AppResources.Launch) - { - platformUtilsService.LaunchUri(cipher.Login.LaunchUri); - } - else if (selection == AppResources.CopyNumber) - { - if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync()) - { - await clipboardService.CopyTextAsync(cipher.Card.Number); - platformUtilsService.ShowToastForCopiedValue(AppResources.Number); - } - } - else if (selection == AppResources.CopySecurityCode) - { - if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync()) - { - await clipboardService.CopyTextAsync(cipher.Card.Code); - platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode); - var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id); - } - } - else if (selection == AppResources.CopyNotes) - { - await clipboardService.CopyTextAsync(cipher.Notes); - platformUtilsService.ShowToastForCopiedValue(AppResources.Notes); - } - return selection; - } - public static async Task SendListOptions(ContentPage page, SendView send) { var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); diff --git a/src/App/Utilities/Helpers/CipherHelper.cs b/src/App/Utilities/Helpers/CipherHelper.cs new file mode 100644 index 000000000..0f3623f73 --- /dev/null +++ b/src/App/Utilities/Helpers/CipherHelper.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Pages; +using Bit.App.Resources; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Xamarin.Forms; + +namespace Bit.App.Utilities.Helpers +{ + public class CipherHelper : ICipherHelper + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IEventService _eventService; + private readonly IVaultTimeoutService _vaultTimeoutService; + private readonly IClipboardService _clipboardService; + private readonly IPasswordRepromptService _passwordRepromptService; + + public CipherHelper(IPlatformUtilsService platformUtilsService, + IEventService eventService, + IVaultTimeoutService vaultTimeoutService, + IClipboardService clipboardService, + IPasswordRepromptService passwordRepromptService) + { + _platformUtilsService = platformUtilsService; + _eventService = eventService; + _vaultTimeoutService = vaultTimeoutService; + _clipboardService = clipboardService; + _passwordRepromptService = passwordRepromptService; + } + + public async Task ShowCipherOptionsAsync(Page page, CipherView cipher) + { + var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, await GetCipherOptionsAsync(cipher)); + + if (await _vaultTimeoutService.IsLockedAsync()) + { + _platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked); + } + else if (selection == AppResources.View) + { + await page.Navigation.PushModalAsync(new NavigationPage(new ViewPage(cipher.Id))); + } + else if (selection == AppResources.Edit) + { + if (await RepromptPasswordIfNeededAsync(cipher)) + { + await page.Navigation.PushModalAsync(new NavigationPage(new AddEditPage(cipher.Id))); + } + } + else if (selection == AppResources.CopyUsername) + { + await CopyUsernameAsync(cipher); + } + else if (selection == AppResources.CopyPassword) + { + await CopyPasswordAsync(cipher); + } + else if (selection == AppResources.CopyTotp) + { + if (await RepromptPasswordIfNeededAsync(cipher)) + { + var totpService = ServiceContainer.Resolve("totpService"); + var totp = await totpService.GetCodeAsync(cipher.Login.Totp); + if (!string.IsNullOrWhiteSpace(totp)) + { + await _clipboardService.CopyTextAsync(totp); + _platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp); + } + } + } + else if (selection == AppResources.Launch) + { + _platformUtilsService.LaunchUri(cipher.Login.LaunchUri); + } + else if (selection == AppResources.CopyNumber) + { + if (await RepromptPasswordIfNeededAsync(cipher)) + { + await _clipboardService.CopyTextAsync(cipher.Card.Number); + _platformUtilsService.ShowToastForCopiedValue(AppResources.Number); + } + } + else if (selection == AppResources.CopySecurityCode) + { + if (await RepromptPasswordIfNeededAsync(cipher)) + { + await _clipboardService.CopyTextAsync(cipher.Card.Code); + _platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode); + _eventService.CollectAsync(EventType.Cipher_ClientCopiedCardCode, cipher.Id).FireAndForget(); + } + } + else if (selection == AppResources.CopyNotes) + { + await _clipboardService.CopyTextAsync(cipher.Notes); + _platformUtilsService.ShowToastForCopiedValue(AppResources.Notes); + } + return selection; + } + + public async Task CopyUsernameAsync(CipherView cipher) + { + await _clipboardService.CopyTextAsync(cipher.Login.Username); + _platformUtilsService.ShowToastForCopiedValue(AppResources.Username); + } + + public async Task CopyPasswordAsync(CipherView cipher) + { + if (await RepromptPasswordIfNeededAsync(cipher)) + { + await _clipboardService.CopyTextAsync(cipher.Login.Password); + _platformUtilsService.ShowToastForCopiedValue(AppResources.Password); + _eventService.CollectAsync(EventType.Cipher_ClientCopiedPassword, cipher.Id).FireAndForget(); + } + } + + private async Task GetCipherOptionsAsync(CipherView cipher) + { + var options = new List { AppResources.View }; + if (!cipher.IsDeleted) + { + options.Add(AppResources.Edit); + } + if (cipher.Type == CipherType.Login) + { + if (!string.IsNullOrWhiteSpace(cipher.Login.Username)) + { + options.Add(AppResources.CopyUsername); + } + if (!string.IsNullOrWhiteSpace(cipher.Login.Password) && cipher.ViewPassword) + { + options.Add(AppResources.CopyPassword); + } + if (!string.IsNullOrWhiteSpace(cipher.Login.Totp)) + { + var stateService = ServiceContainer.Resolve("stateService"); + var canAccessPremium = await stateService.CanAccessPremiumAsync(); + if (canAccessPremium || cipher.OrganizationUseTotp) + { + options.Add(AppResources.CopyTotp); + } + } + if (cipher.Login.CanLaunch) + { + options.Add(AppResources.Launch); + } + } + else if (cipher.Type == CipherType.Card) + { + if (!string.IsNullOrWhiteSpace(cipher.Card.Number)) + { + options.Add(AppResources.CopyNumber); + } + if (!string.IsNullOrWhiteSpace(cipher.Card.Code)) + { + options.Add(AppResources.CopySecurityCode); + } + } + else if (cipher.Type == CipherType.SecureNote) + { + if (!string.IsNullOrWhiteSpace(cipher.Notes)) + { + options.Add(AppResources.CopyNotes); + } + } + + return options.ToArray(); + } + + private async Task RepromptPasswordIfNeededAsync(CipherView cipher) + { + return cipher.Reprompt == CipherRepromptType.None || await _passwordRepromptService.ShowPasswordPromptAsync(); + } + } +} diff --git a/src/App/Utilities/Helpers/ICipherHelper.cs b/src/App/Utilities/Helpers/ICipherHelper.cs new file mode 100644 index 000000000..13a8ef81c --- /dev/null +++ b/src/App/Utilities/Helpers/ICipherHelper.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Bit.Core.Models.View; +using Xamarin.Forms; + +namespace Bit.App.Utilities.Helpers +{ + public interface ICipherHelper + { + Task ShowCipherOptionsAsync(Page page, CipherView cipher); + Task CopyUsernameAsync(CipherView cipher); + Task CopyPasswordAsync(CipherView cipher); + } +} diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index cfd0bf1bb..a8a724e27 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -8,6 +8,7 @@ using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; using Bit.App.Utilities.AccountManagement; +using Bit.App.Utilities.Helpers; using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities; @@ -182,6 +183,15 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve("authService")); ServiceContainer.Register("accountsManager", accountsManager); + var cipherHelper = new CipherHelper( + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("eventService"), + ServiceContainer.Resolve("vaultTimeoutService"), + ServiceContainer.Resolve("clipboardService"), + ServiceContainer.Resolve("passwordRepromptService") + ); + ServiceContainer.Register("cipherHelper", cipherHelper); + if (postBootstrapFunc != null) { await postBootstrapFunc.Invoke();