diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7dad4798d..8c0bb6a69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,12 +1,14 @@ -# Please sort lines alphabetically, this will ensure we don't accidentally add duplicates. +# Please sort into logical groups with comment headers. Sort groups in order of specificity. +# For example, default owners should always be the first group. +# Sort lines alphabetically within these groups to avoid accidentally adding duplicates. # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -# The following owners will be the default owners for everything in the repo. -# Unless a later match takes precedence -# @bitwarden/tech-leads +# Default file owners +* @bitwarden/dept-development-mobile -@bitwarden/dept-development-mobile +# DevOps for Actions and other workflow changes +.github/workflows @bitwarden/dept-devops ## Auth team files ## diff --git a/.github/renovate.json b/.github/renovate.json index 52e80afcf..cd01c8c00 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -8,16 +8,14 @@ ":pinAllExceptPeerDependencies", ":prConcurrentLimit10", ":rebaseStalePrs", - "schedule:weekends", - ":separateMajorReleases" + ":separateMajorReleases", + "group:monorepos", + "schedule:weekends" ], - "enabledManagers": ["cargo", "github-actions", "npm", "nuget"], + "enabledManagers": ["github-actions", "npm", "nuget"], + "commitMessagePrefix": "[deps]:", + "commitMessageTopic": "{{depName}}", "packageRules": [ - { - "groupName": "cargo minor", - "matchManagers": ["cargo"], - "matchUpdateTypes": ["minor", "patch"] - }, { "groupName": "gh minor", "matchManagers": ["github-actions"], @@ -32,6 +30,6 @@ "groupName": "nuget minor", "matchManagers": ["nuget"], "matchUpdateTypes": ["minor", "patch"] - }, + } ] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe6f6a176..7e7680d5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -495,7 +495,7 @@ jobs: - name: Set XCode version uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: - xcode-version: 15.0.1 + xcode-version: 15.1 - name: Setup NuGet uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0 diff --git a/package-lock.json b/package-lock.json index 32f51b928..b92128bf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "bitwarden-mobile", "version": "0.0.0", "devDependencies": { - "gh-pages": "^3.2.3" + "gh-pages": "3.2.3" } }, "node_modules/array-union": { diff --git a/package.json b/package.json index 37d87bd2e..acaf22c51 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,6 @@ "clean:l10n": "git push origin --delete l10n_master" }, "devDependencies": { - "gh-pages": "^3.2.3" + "gh-pages": "3.2.3" } } diff --git a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs index 8d863e003..1a734eeb6 100644 --- a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs +++ b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs @@ -1,5 +1,6 @@ using AndroidX.AppCompat.View.Menu; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; using Google.Android.Material.BottomNavigation; using Microsoft.Maui.Handlers; @@ -90,7 +91,17 @@ namespace Bit.App.Handlers if(e.Item is MenuItemImpl item) { System.Diagnostics.Debug.WriteLine($"Tab '{item.Title}' was reselected so we'll PopToRoot."); - MainThread.BeginInvokeOnMainThread(async () => await _tabbedPage.CurrentPage.Navigation.PopToRootAsync()); + MainThread.BeginInvokeOnMainThread(async () => + { + try + { + await _tabbedPage.CurrentPage.Navigation.PopToRootAsync(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); } } diff --git a/src/App/Platforms/iOS/AppDelegate.cs b/src/App/Platforms/iOS/AppDelegate.cs index 4961ab97b..066eb54bc 100644 --- a/src/App/Platforms/iOS/AppDelegate.cs +++ b/src/App/Platforms/iOS/AppDelegate.cs @@ -15,6 +15,7 @@ using CoreNFC; using Foundation; using Microsoft.Maui.Platform; using UIKit; +using UserNotifications; using WatchConnectivity; namespace Bit.iOS @@ -41,73 +42,78 @@ namespace Bit.iOS private IStateService _stateService; private IEventService _eventService; - private LazyResolve _deepLinkContext = new LazyResolve(); + private readonly LazyResolve _deepLinkContext = new LazyResolve(); public override bool FinishedLaunching(UIApplication app, NSDictionary options) { - InitApp(); - - _deviceActionService = ServiceContainer.Resolve("deviceActionService"); - _messagingService = ServiceContainer.Resolve("messagingService"); - _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _storageService = ServiceContainer.Resolve("storageService"); - _stateService = ServiceContainer.Resolve("stateService"); - _eventService = ServiceContainer.Resolve("eventService"); - - ConnectToWatchIfNeededAsync().FireAndForget(); - - _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => + try { - try + InitApp(); + + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + _storageService = ServiceContainer.Resolve("storageService"); + _stateService = ServiceContainer.Resolve("stateService"); + _eventService = ServiceContainer.Resolve("eventService"); + + ConnectToWatchIfNeededAsync().FireAndForget(); + + _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { - if (message.Command == "startEventTimer") + try { - StartEventTimer(); - } - else if (message.Command == "stopEventTimer") - { - var task = StopEventTimerAsync(); - } - else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) - { - MainThread.BeginInvokeOnMainThread(() => + if (message.Command == "startEventTimer") { - iOSCoreHelpers.AppearanceAdjustments(); - }); - } - else if (message.Command == "listenYubiKeyOTP") - { - iOSCoreHelpers.ListenYubiKey((bool)message.Data, _deviceActionService, _nfcSession, _nfcDelegate); - } - else if (message.Command == "unlocked") - { - var needsAutofillReplacement = await _storageService.GetAsync( - Core.Constants.AutofillNeedsIdentityReplacementKey); - if (needsAutofillReplacement.GetValueOrDefault()) - { - await ASHelpers.ReplaceAllIdentitiesAsync(); + StartEventTimer(); } - } - else if (message.Command == "showAppExtension") - { - MainThread.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); - } - else if (message.Command == "syncCompleted") - { - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + else if (message.Command == "stopEventTimer") { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + var task = StopEventTimerAsync(); + } + else if (message.Command is ThemeManager.UPDATED_THEME_MESSAGE_KEY) + { + await MainThread.InvokeOnMainThreadAsync(() => + { + iOSCoreHelpers.AppearanceAdjustments(); + }); + } + else if (message.Command == "listenYubiKeyOTP" && message.Data is bool listen) + { + iOSCoreHelpers.ListenYubiKey(listen, _deviceActionService, _nfcSession, _nfcDelegate); + } + else if (message.Command == "unlocked") + { + var needsAutofillReplacement = await _storageService.GetAsync( + Core.Constants.AutofillNeedsIdentityReplacementKey); + if (needsAutofillReplacement.GetValueOrDefault()) { await ASHelpers.ReplaceAllIdentitiesAsync(); } } - } - else if (message.Command == "addedCipher" || message.Command == "editedCipher" || - message.Command == "restoredCipher") - { - if (_deviceActionService.SystemMajorVersion() >= 12) + else if (message.Command == "showAppExtension") { + await MainThread.InvokeOnMainThreadAsync(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); + } + else if (message.Command == "syncCompleted") + { + if (message.Data is Dictionary data && data.TryGetValue("successfully", out var value)) + { + var success = value as bool?; + if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + { + await ASHelpers.ReplaceAllIdentitiesAsync(); + } + } + } + else if (message.Command == "addedCipher" || message.Command == "editedCipher" || + message.Command == "restoredCipher") + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + return; + } + if (await ASHelpers.IdentitiesSupportIncrementalAsync()) { var cipherId = message.Data as string; @@ -124,11 +130,13 @@ namespace Bit.iOS } await ASHelpers.ReplaceAllIdentitiesAsync(); } - } - else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") - { - if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") { + if (!UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + return; + } + if (await ASHelpers.IdentitiesSupportIncrementalAsync()) { var identity = ASHelpers.ToPasswordCredentialIdentity( @@ -142,105 +150,145 @@ namespace Bit.iOS } await ASHelpers.ReplaceAllIdentitiesAsync(); } - } - else if (message.Command == "logout") - { - if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync(); } - } - else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") - && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) - { - await ASHelpers.ReplaceAllIdentitiesAsync(); - } - else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) - { - var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); - if (timeoutAction == VaultTimeoutAction.Logout && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) - { - await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync(); - } - else + else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") + && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { await ASHelpers.ReplaceAllIdentitiesAsync(); } + else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) + { + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) + { + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync(); + } + } + else + { + await ASHelpers.ReplaceAllIdentitiesAsync(); + } + } } - } - catch (Exception ex) - { - LoggerHelper.LogEvenIfCantBeResolved(ex); - } - }); + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); - var finishedLaunching = base.FinishedLaunching(app, options); + var finishedLaunching = base.FinishedLaunching(app, options); - ThemeManager.SetTheme(Microsoft.Maui.Controls.Application.Current.Resources); - iOSCoreHelpers.AppearanceAdjustments(); + ThemeManager.SetTheme(Microsoft.Maui.Controls.Application.Current.Resources); + iOSCoreHelpers.AppearanceAdjustments(); - return finishedLaunching; + return finishedLaunching; + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } public override void OnResignActivation(UIApplication uiApplication) { - if (UIApplication.SharedApplication.KeyWindow is null) + try { + if (UIApplication.SharedApplication.KeyWindow != null) + { + var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + { + Tag = SPLASH_VIEW_TAG + }; + var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + { + BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform() + }; + var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png"); + var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices. + var imageView = new UIImageView(frame) + { + Image = logo, + Center = new CGPoint(view.Center.X, view.Center.Y - 30), + ContentMode = UIViewContentMode.ScaleAspectFit + }; + view.AddSubview(backgroundView); + view.AddSubview(imageView); + UIApplication.SharedApplication.KeyWindow.AddSubview(view); + UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); + UIApplication.SharedApplication.KeyWindow.EndEditing(true); + } base.OnResignActivation(uiApplication); - return; } - - var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) + catch (Exception ex) { - Tag = SPLASH_VIEW_TAG - }; - var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame) - { - BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform() - }; - var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png"); - var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices. - var imageView = new UIImageView(frame) - { - Image = logo, - Center = new CGPoint(view.Center.X, view.Center.Y - 30), - ContentMode = UIViewContentMode.ScaleAspectFit - }; - view.AddSubview(backgroundView); - view.AddSubview(imageView); - UIApplication.SharedApplication.KeyWindow.AddSubview(view); - UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); - UIApplication.SharedApplication.KeyWindow.EndEditing(true); - - base.OnResignActivation(uiApplication); + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } public override void DidEnterBackground(UIApplication uiApplication) { - if (_stateService != null && _deviceActionService != null) + try { - _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); - } + if (_stateService != null && _deviceActionService != null) + { + _stateService.SetLastActiveTimeAsync(_deviceActionService.GetActiveTime()); + } - _messagingService?.Send("slept"); - base.DidEnterBackground(uiApplication); + _messagingService?.Send("slept"); + base.DidEnterBackground(uiApplication); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } - public override void OnActivated(UIApplication uiApplication) + public override async void OnActivated(UIApplication uiApplication) { - base.OnActivated(uiApplication); - UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; - UIApplication.SharedApplication.KeyWindow? - .ViewWithTag(SPLASH_VIEW_TAG)? - .RemoveFromSuperview(); + try + { + base.OnActivated(uiApplication); - ThemeManager.UpdateThemeOnPagesAsync(); + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + await UNUserNotificationCenter.Current.SetBadgeCountAsync(0); + } + else + { + UIApplication.SharedApplication.ApplicationIconBadgeNumber = 0; + } + + UIApplication.SharedApplication.KeyWindow? + .ViewWithTag(SPLASH_VIEW_TAG)? + .RemoveFromSuperview(); + + ThemeManager.UpdateThemeOnPagesAsync(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } public override void WillEnterForeground(UIApplication uiApplication) { - _messagingService?.Send(AppHelpers.RESUMED_MESSAGE_COMMAND); - base.WillEnterForeground(uiApplication); + try + { + _messagingService?.Send(AppHelpers.RESUMED_MESSAGE_COMMAND); + base.WillEnterForeground(uiApplication); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:openURL:sourceApplication:annotation:")] @@ -251,15 +299,30 @@ namespace Bit.iOS public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options) { - return _deepLinkContext.Value.OnNewUri(url) || base.OpenUrl(app, url, options); + try + { + return _deepLinkContext.Value.OnNewUri(url) || base.OpenUrl(app, url, options); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + return false; + } } public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler) { - if (Microsoft.Maui.ApplicationModel.Platform.ContinueUserActivity(application, userActivity, completionHandler)) + try { - return true; + if (Microsoft.Maui.ApplicationModel.Platform.ContinueUserActivity(application, userActivity, completionHandler)) + { + return true; + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } return base.ContinueUserActivity(application, userActivity, completionHandler); } @@ -267,33 +330,68 @@ namespace Bit.iOS [Export("application:didFailToRegisterForRemoteNotificationsWithError:")] public void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error) { - _pushHandler?.OnErrorReceived(error); + try + { + _pushHandler?.OnErrorReceived(error); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")] public void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { - _pushHandler?.OnRegisteredSuccess(deviceToken); + try + { + _pushHandler?.OnRegisteredSuccess(deviceToken); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didRegisterUserNotificationSettings:")] public void DidRegisterUserNotificationSettings(UIApplication application, UIUserNotificationSettings notificationSettings) { - application.RegisterForRemoteNotifications(); + try + { + application.RegisterForRemoteNotifications(); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")] public void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action completionHandler) { - _pushHandler?.OnMessageReceived(userInfo); + try + { + _pushHandler?.OnMessageReceived(userInfo); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } [Export("application:didReceiveRemoteNotification:")] public void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo) { - _pushHandler?.OnMessageReceived(userInfo); + try + { + _pushHandler?.OnMessageReceived(userInfo); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } public void InitApp() @@ -319,7 +417,7 @@ namespace Bit.iOS _nfcDelegate = new Core.NFCReaderDelegate((success, message) => _messagingService.Send("gotYubiKeyOTP", message)); - iOSCoreHelpers.Bootstrap(async () => await ApplyManagedSettingsAsync()); + iOSCoreHelpers.Bootstrap(ApplyManagedSettingsAsync); } private void RegisterPush() @@ -364,31 +462,45 @@ namespace Bit.iOS _eventTimer = null; MainThread.BeginInvokeOnMainThread(() => { - _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => + try { - var task = Task.Run(() => _eventService.UploadEventsAsync()); - }); + _eventTimer = NSTimer.CreateScheduledTimer(60, true, timer => + { + _eventService?.UploadEventsAsync().FireAndForget(); + }); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } }); } private async Task StopEventTimerAsync() { - _eventTimer?.Invalidate(); - _eventTimer?.Dispose(); - _eventTimer = null; - if (_eventBackgroundTaskId > 0) + try { + _eventTimer?.Invalidate(); + _eventTimer?.Dispose(); + _eventTimer = null; + if (_eventBackgroundTaskId > 0) + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + } + _eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => + { + UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); + _eventBackgroundTaskId = 0; + }); + await _eventService.UploadEventsAsync(); UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); _eventBackgroundTaskId = 0; } - _eventBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => + catch (Exception ex) { - UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - _eventBackgroundTaskId = 0; - }); - await _eventService.UploadEventsAsync(); - UIApplication.SharedApplication.EndBackgroundTask(_eventBackgroundTaskId); - _eventBackgroundTaskId = 0; + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task ApplyManagedSettingsAsync() diff --git a/src/Core/App.xaml.cs b/src/Core/App.xaml.cs index 7deed659d..4f2fa1b3a 100644 --- a/src/Core/App.xaml.cs +++ b/src/Core/App.xaml.cs @@ -81,7 +81,7 @@ namespace Bit.App { get { - return Application.Current.Windows.OfType().FirstOrDefault(w => w.IsActive); + return Application.Current?.Windows.OfType().FirstOrDefault(w => w.IsActive); } } @@ -145,11 +145,14 @@ namespace Bit.App { get { - return Application.Current.MainPage; + return Application.Current?.MainPage; } set { - Application.Current.MainPage = value; + if (Application.Current != null) + { + Application.Current.MainPage = value; + } } } #endif diff --git a/src/Core/Pages/Accounts/HomePage.xaml.cs b/src/Core/Pages/Accounts/HomePage.xaml.cs index 11c5b36ba..9a418ac4d 100644 --- a/src/Core/Pages/Accounts/HomePage.xaml.cs +++ b/src/Core/Pages/Accounts/HomePage.xaml.cs @@ -153,7 +153,7 @@ namespace Bit.App.Pages private async Task StartEnvironmentAsync() { - await _accountListOverlay.HideAsync(); + await _accountListOverlay.HideAsync(); var page = new EnvironmentPage(); await Navigation.PushModalAsync(new NavigationPage(page)); } diff --git a/src/Core/Pages/Accounts/LockPage.xaml.cs b/src/Core/Pages/Accounts/LockPage.xaml.cs index 6861f7bc2..bd3be1858 100644 --- a/src/Core/Pages/Accounts/LockPage.xaml.cs +++ b/src/Core/Pages/Accounts/LockPage.xaml.cs @@ -81,7 +81,7 @@ namespace Bit.App.Pages { if (message.Command == Constants.ClearSensitiveFields) { - MainThread.BeginInvokeOnMainThread(_vm.ResetPinPasswordFields); + MainThread.BeginInvokeOnMainThread(() => _vm?.ResetPinPasswordFields()); } }); if (_appeared) diff --git a/src/Core/Pages/Accounts/LockPageViewModel.cs b/src/Core/Pages/Accounts/LockPageViewModel.cs index 2464524cf..b3bd61015 100644 --- a/src/Core/Pages/Accounts/LockPageViewModel.cs +++ b/src/Core/Pages/Accounts/LockPageViewModel.cs @@ -245,9 +245,9 @@ namespace Bit.App.Pages public async Task SubmitAsync() { - ShowPassword = false; try { + ShowPassword = false; var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); if (PinEnabled) { @@ -257,12 +257,15 @@ namespace Bit.App.Pages { await UnlockWithMasterPasswordAsync(kdfConfig); } - } catch (LegacyUserException) { await HandleLegacyUserAsync(); } + catch (Exception ex) + { + HandleException(ex); + } } private async Task UnlockWithPinAsync(KdfConfig kdfConfig) diff --git a/src/Core/Pages/Accounts/LoginPage.xaml.cs b/src/Core/Pages/Accounts/LoginPage.xaml.cs index 84f62879f..7139e57b1 100644 --- a/src/Core/Pages/Accounts/LoginPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginPage.xaml.cs @@ -3,6 +3,7 @@ using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -74,7 +75,7 @@ namespace Bit.App.Pages { if (message.Command == Constants.ClearSensitiveFields) { - MainThread.BeginInvokeOnMainThread(_vm.ResetPasswordField); + MainThread.BeginInvokeOnMainThread(() => _vm?.ResetPasswordField()); } }); _mainContent.Content = _mainLayout; @@ -188,12 +189,20 @@ namespace Bit.App.Pages private async Task LogInSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } private async Task UpdateTempPasswordAsync() diff --git a/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs b/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs index ad5e3aba3..a88f27a03 100644 --- a/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginPasswordlessRequestPage.xaml.cs @@ -1,6 +1,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Enums; +using Bit.Core.Services; namespace Bit.App.Pages { @@ -48,12 +49,20 @@ namespace Bit.App.Pages private async Task LogInSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } private async Task UpdateTempPasswordAsync() diff --git a/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs b/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs index a5809659c..95033c7e1 100644 --- a/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs +++ b/src/Core/Pages/Accounts/LoginPasswordlessViewModel.cs @@ -77,6 +77,7 @@ namespace Bit.App.Pages { _requestTimeCts?.Cancel(); _requestTimeCts?.Dispose(); + _requestTimeCts = null; } catch (Exception ex) { diff --git a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs index 3760f961e..1c1402a8b 100644 --- a/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs +++ b/src/Core/Pages/Accounts/LoginSsoPage.xaml.cs @@ -1,6 +1,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -89,16 +90,30 @@ namespace Bit.App.Pages private async Task StartTwoFactorAsync() { - RestoreAppOptionsFromCopy(); - var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier); - await Navigation.PushModalAsync(new NavigationPage(page)); + try + { + RestoreAppOptionsFromCopy(); + var page = new TwoFactorPage(true, _appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task StartSetPasswordAsync() { - RestoreAppOptionsFromCopy(); - var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier); - await Navigation.PushModalAsync(new NavigationPage(page)); + try + { + RestoreAppOptionsFromCopy(); + var page = new SetPasswordPage(_appOptions, _vm.OrgIdentifier); + await Navigation.PushModalAsync(new NavigationPage(page)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task UpdateTempPasswordAsync() @@ -115,16 +130,23 @@ namespace Bit.App.Pages private async Task SsoAuthSuccessAsync() { - RestoreAppOptionsFromCopy(); - await AppHelpers.ClearPreviousPage(); + try + { + RestoreAppOptionsFromCopy(); + await AppHelpers.ClearPreviousPage(); - if (await _vaultTimeoutService.IsLockedAsync()) - { - App.MainPage = new NavigationPage(new LockPage(_appOptions)); + if (await _vaultTimeoutService.IsLockedAsync()) + { + App.MainPage = new NavigationPage(new LockPage(_appOptions)); + } + else + { + App.MainPage = new TabsPage(_appOptions, null); + } } - else + catch (Exception ex) { - App.MainPage = new TabsPage(_appOptions, null); + LoggerHelper.LogEvenIfCantBeResolved(ex); } } } diff --git a/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs b/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs index 5504b9d43..19013dba4 100644 --- a/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs +++ b/src/Core/Pages/Accounts/SetPasswordPage.xaml.cs @@ -1,5 +1,6 @@ using Bit.App.Models; using Bit.App.Utilities; +using Bit.Core.Services; namespace Bit.App.Pages { @@ -64,12 +65,19 @@ namespace Bit.App.Pages private async Task SetPasswordSuccessAsync() { - if (AppHelpers.SetAlternateMainPage(_appOptions)) + try { - return; + if (AppHelpers.SetAlternateMainPage(_appOptions)) + { + return; + } + var previousPage = await AppHelpers.ClearPreviousPage(); + App.MainPage = new TabsPage(_appOptions, previousPage); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } - var previousPage = await AppHelpers.ClearPreviousPage(); - App.MainPage = new TabsPage(_appOptions, previousPage); } } } diff --git a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs index 1b218dc3d..9322936ce 100644 --- a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs @@ -2,6 +2,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.App.Pages @@ -63,11 +64,11 @@ namespace Bit.App.Pages if (_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) && token.Length == 44 && !token.Contains(" ")) { - MainThread.BeginInvokeOnMainThread(async () => + MainThread.BeginInvokeOnMainThread(() => { _vm.Token = token; - await _vm.SubmitAsync(); }); + _vm.SubmitCommand.Execute(null); } } else if (message.Command == "resumeYubiKey") @@ -124,12 +125,9 @@ namespace Bit.App.Pages return base.OnBackButtonPressed(); } - private async void Continue_Clicked(object sender, EventArgs e) + private void Continue_Clicked(object sender, EventArgs e) { - if (DoOnce()) - { - await _vm.SubmitAsync(); - } + _vm.SubmitCommand.Execute(null); } private async void Methods_Clicked(object sender, EventArgs e) @@ -158,17 +156,24 @@ namespace Bit.App.Pages private async void TryAgain_Clicked(object sender, EventArgs e) { - if (DoOnce()) + try { - if (_vm.Fido2Method) + if (DoOnce()) { - await _vm.Fido2AuthenticateAsync(); - } - else if (_vm.YubikeyMethod) - { - _messagingService.Send("listenYubiKeyOTP", true); + if (_vm.Fido2Method) + { + await _vm.Fido2AuthenticateAsync(); + } + else if (_vm.YubikeyMethod) + { + _messagingService.Send("listenYubiKeyOTP", true); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private async Task StartSetPasswordAsync() diff --git a/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs b/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs index 0952c5be4..319d55160 100644 --- a/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/Core/Pages/Accounts/TwoFactorPageViewModel.cs @@ -1,25 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; +using System.Net; using System.Windows.Input; using Bit.App.Abstractions; -using Bit.Core.Resources.Localization; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; -using Bit.Core.Services; +using Bit.Core.Resources.Localization; using Bit.Core.Utilities; using Newtonsoft.Json; -using Microsoft.Maui.Authentication; -using Microsoft.Maui.Controls; -using Microsoft.Maui; - namespace Bit.App.Pages { public class TwoFactorPageViewModel : CaptchaProtectedViewModel @@ -62,7 +53,7 @@ namespace Bit.App.Pages _deviceTrustCryptoService = ServiceContainer.Resolve(); PageTitle = AppResources.TwoStepLogin; - SubmitCommand = new Command(async () => await SubmitAsync()); + SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false); MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false); } @@ -91,8 +82,7 @@ namespace Bit.App.Pages public bool TotpMethod => AuthenticatorMethod || EmailMethod; - public bool ShowTryAgain => (YubikeyMethod && // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes -Device.RuntimePlatform == Device.iOS) || Fido2Method; + public bool ShowTryAgain => (YubikeyMethod && DeviceInfo.Platform == DevicePlatform.iOS) || Fido2Method; public bool ShowContinue { @@ -106,9 +96,11 @@ Device.RuntimePlatform == Device.iOS) || Fido2Method; set => SetProperty(ref _enableContinue, value); } - public string YubikeyInstruction => // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes -Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : - AppResources.YubiKeyInstruction; +#if IOS + public string YubikeyInstruction => AppResources.YubiKeyInstructionIos; +#else + public string YubikeyInstruction => AppResources.YubiKeyInstruction; +#endif public TwoFactorProviderType? SelectedProviderType { @@ -124,7 +116,7 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : nameof(ShowTryAgain), }); } - public Command SubmitCommand { get; } + public ICommand SubmitCommand { get; } public ICommand MoreCommand { get; } public Action TwoFactorAuthSuccessAction { get; set; } public Action LockAction { get; set; } @@ -186,7 +178,7 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : page.DuoWebView.RegisterAction(sig => { Token = sig; - Device.BeginInvokeOnMainThread(async () => await SubmitAsync()); + SubmitCommand.Execute(null); }); break; case TwoFactorProviderType.Email: @@ -213,68 +205,76 @@ Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : public async Task Fido2AuthenticateAsync(Dictionary providerData = null) { - await _deviceActionService.ShowLoadingAsync(AppResources.Validating); - - if (providerData == null) - { - providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; - } - - var callbackUri = "bitwarden://webauthn-callback"; - var data = AppHelpers.EncodeDataParameter(new - { - callbackUri = callbackUri, - data = JsonConvert.SerializeObject(providerData), - headerText = AppResources.Fido2Title, - btnText = AppResources.Fido2AuthenticateWebAuthn, - btnReturnText = AppResources.Fido2ReturnToApp, - }); - - var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + - "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; - - WebAuthenticatorResult authResult = null; try { - var options = new WebAuthenticatorOptions - { - Url = new Uri(url), - CallbackUrl = new Uri(callbackUri), - PrefersEphemeralWebBrowserSession = true, - }; - authResult = await WebAuthenticator.AuthenticateAsync(options); - } - catch (TaskCanceledException) - { - // user canceled - await _deviceActionService.HideLoadingAsync(); - return; - } + await _deviceActionService.ShowLoadingAsync(AppResources.Validating); - string response = null; - if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) - { - response = Uri.UnescapeDataString(resultData); - } - if (!string.IsNullOrWhiteSpace(response)) - { - Token = response; - await SubmitAsync(false); - } - else - { - await _deviceActionService.HideLoadingAsync(); - if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) + if (providerData == null) { - var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; - await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, - AppResources.Ok); + providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; + } + + var callbackUri = "bitwarden://webauthn-callback"; + var data = AppHelpers.EncodeDataParameter(new + { + callbackUri = callbackUri, + data = JsonConvert.SerializeObject(providerData), + headerText = AppResources.Fido2Title, + btnText = AppResources.Fido2AuthenticateWebAuthn, + btnReturnText = AppResources.Fido2ReturnToApp, + }); + + var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + + "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; + + WebAuthenticatorResult authResult = null; + try + { + var options = new WebAuthenticatorOptions + { + Url = new Uri(url), + CallbackUrl = new Uri(callbackUri), + PrefersEphemeralWebBrowserSession = true, + }; + authResult = await WebAuthenticator.AuthenticateAsync(options); + } + catch (TaskCanceledException) + { + // user canceled + await _deviceActionService.HideLoadingAsync(); + return; + } + + string response = null; + if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) + { + response = Uri.UnescapeDataString(resultData); + } + if (!string.IsNullOrWhiteSpace(response)) + { + Token = response; + await SubmitAsync(false); } else { - await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, - AppResources.AnErrorHasOccurred, AppResources.Ok); + await _deviceActionService.HideLoadingAsync(); + if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) + { + var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; + await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, + AppResources.Ok); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, + AppResources.AnErrorHasOccurred, AppResources.Ok); + } } + + } + catch (Exception ex) + { + HandleException(ex); } } diff --git a/src/Core/Pages/BaseContentPage.cs b/src/Core/Pages/BaseContentPage.cs index d04c8a852..8aa3b453c 100644 --- a/src/Core/Pages/BaseContentPage.cs +++ b/src/Core/Pages/BaseContentPage.cs @@ -170,8 +170,15 @@ namespace Bit.App.Pages { Task.Run(async () => { - await Task.Delay(ShowModalAnimationDelay); - MainThread.BeginInvokeOnMainThread(() => input.Focus()); + try + { + await Task.Delay(ShowModalAnimationDelay); + MainThread.BeginInvokeOnMainThread(() => input.Focus()); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } }); } diff --git a/src/Core/Pages/Vault/ScanPage.xaml b/src/Core/Pages/Vault/ScanPage.xaml index 99c71665f..d7cc34de9 100644 --- a/src/Core/Pages/Vault/ScanPage.xaml +++ b/src/Core/Pages/Vault/ScanPage.xaml @@ -47,6 +47,9 @@ Camera="{Binding Camera}" AutoStartPreview="{Binding AutoStartPreview}" NumCamerasDetected="{Binding NumCameras, Mode=OneWayToSource}" + WidthRequest="{OnPlatform Android=150}" + HeightRequest="{OnPlatform Android=150}" + Scale="{OnPlatform Android=4}" Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" /> diff --git a/src/Core/Resources/Localization/AppResources.az.resx b/src/Core/Resources/Localization/AppResources.az.resx index a30511650..57a001456 100644 --- a/src/Core/Resources/Localization/AppResources.az.resx +++ b/src/Core/Resources/Localization/AppResources.az.resx @@ -156,7 +156,7 @@ The button text that allows a user to copy the login's password to their clipboard. - İstifadəçi adını kopyalayın + İstifadəçi adını kopyala The button text that allows a user to copy the login's username to their clipboard. @@ -176,7 +176,7 @@ Confirmation alert message when deleteing something. - Redaktə edin + Düzəliş et Qovluğa düzəliş et diff --git a/src/Core/Resources/Localization/AppResources.cy.resx b/src/Core/Resources/Localization/AppResources.cy.resx index a7a01eb33..93c580219 100644 --- a/src/Core/Resources/Localization/AppResources.cy.resx +++ b/src/Core/Resources/Localization/AppResources.cy.resx @@ -265,7 +265,7 @@ The login button text (verb). - Login + Mewngofnodi Title for login page. (noun) @@ -308,7 +308,7 @@ Label for an entity name. - No + Na Nodiadau @@ -327,7 +327,7 @@ Button text for a save operation (verb). - Move + Symud Yn cadw... @@ -391,7 +391,7 @@ Fersiwn - View + Golwg Visit our website @@ -401,7 +401,7 @@ Label for a website. - Yes + Ydw Cyfrif @@ -715,7 +715,7 @@ Verification code - View item + Gweld yr Eitem Cell we Bitwarden diff --git a/src/Core/Resources/Localization/AppResources.zh-Hant.resx b/src/Core/Resources/Localization/AppResources.zh-Hant.resx index 1455e4e0b..65723cd56 100644 --- a/src/Core/Resources/Localization/AppResources.zh-Hant.resx +++ b/src/Core/Resources/Localization/AppResources.zh-Hant.resx @@ -2862,12 +2862,12 @@ 帳戶已登出。 - Your organization permissions were updated, requiring you to set a master password. + 您的組織權限已更新,需要您設定主密碼。 - Your organization requires you to set a master password. + 您的組織要求您設定主密碼。 - Set up an unlock option to change your vault timeout action. + 設定一個解鎖方式來變更您的密碼庫逾時動作。 diff --git a/src/Core/Utilities/TimerTask.cs b/src/Core/Utilities/TimerTask.cs index 27e0b81af..f2ac98778 100644 --- a/src/Core/Utilities/TimerTask.cs +++ b/src/Core/Utilities/TimerTask.cs @@ -1,9 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Bit.Core.Abstractions; -using Microsoft.Maui.Controls; -using Microsoft.Maui; +using Bit.Core.Abstractions; namespace Bit.App.Utilities { @@ -37,7 +32,7 @@ namespace Bit.App.Utilities { while (!_cancellationTokenSource.IsCancellationRequested) { - await Device.InvokeOnMainThreadAsync(async () => + await MainThread.InvokeOnMainThreadAsync(async () => { if (!_cancellationTokenSource.IsCancellationRequested) { diff --git a/src/iOS.Core/Handlers/CustomTabbedHandler.cs b/src/iOS.Core/Handlers/CustomTabbedHandler.cs index aa3a6f4ad..694720534 100644 --- a/src/iOS.Core/Handlers/CustomTabbedHandler.cs +++ b/src/iOS.Core/Handlers/CustomTabbedHandler.cs @@ -1,5 +1,4 @@ -using Bit.App.Abstractions; -using Bit.App.Pages; +using Bit.App.Pages; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Utilities; @@ -13,7 +12,7 @@ namespace Bit.iOS.Core.Handlers public partial class CustomTabbedHandler : TabbedRenderer { private IBroadcasterService _broadcasterService; - private UITabBarItem _previousSelectedItem; + private UITabBarItem? _previousSelectedItem; public CustomTabbedHandler() { @@ -73,8 +72,7 @@ namespace Bit.iOS.Core.Handlers private void UpdateTabBarAppearance() { // https://developer.apple.com/forums/thread/682420 - var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - if (deviceActionService.SystemMajorVersion() >= 15) + if (UIDevice.CurrentDevice.CheckSystemVersion(15,0)) { var appearance = new UITabBarAppearance(); appearance.ConfigureWithOpaqueBackground(); diff --git a/src/iOS.Core/Handlers/CustomWindowHandler.cs b/src/iOS.Core/Handlers/CustomWindowHandler.cs deleted file mode 100644 index 6927c7a9b..000000000 --- a/src/iOS.Core/Handlers/CustomWindowHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Maui.Handlers; -using UIKit; - -namespace Bit.iOS.Core.Handlers -{ - public class CustomWindowHandler : ElementHandler, IWindowHandler - { - public static IPropertyMapper Mapper = new PropertyMapper(ElementHandler.ElementMapper) - { - }; - - public CustomWindowHandler() : base(Mapper) - { - } - - protected override UIWindow CreatePlatformElement() - { - // Haven't tested - //return UIApplication.SharedApplication.Delegate.GetWindow(); - return Platform.GetCurrentUIViewController().View.Window; - } - } -} - diff --git a/src/iOS.Core/Services/LocalizeService.cs b/src/iOS.Core/Services/LocalizeService.cs index 88f8dde94..287e44e2a 100644 --- a/src/iOS.Core/Services/LocalizeService.cs +++ b/src/iOS.Core/Services/LocalizeService.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using Bit.App.Abstractions; using Bit.App.Models; @@ -20,7 +19,7 @@ namespace Bit.iOS.Core.Services } // This gets called a lot - try/catch can be expensive so consider caching or something - CultureInfo ci = null; + CultureInfo? ci; try { ci = new CultureInfo(netLanguage); @@ -108,7 +107,7 @@ namespace Bit.iOS.Core.Services { df.Locale = NSLocale.CurrentLocale; df.DateStyle = NSDateFormatterStyle.Short; - return df.StringFor((NSDate)date); + return df.StringFor((NSDate?)date); } } @@ -118,7 +117,7 @@ namespace Bit.iOS.Core.Services { df.Locale = NSLocale.CurrentLocale; df.TimeStyle = NSDateFormatterStyle.Short; - return df.StringFor((NSDate)time); + return df.StringFor((NSDate?)time); } } } diff --git a/src/iOS.Core/Utilities/DictionaryExtensions.cs b/src/iOS.Core/Utilities/DictionaryExtensions.cs index de3387237..b88e7411d 100644 --- a/src/iOS.Core/Utilities/DictionaryExtensions.cs +++ b/src/iOS.Core/Utilities/DictionaryExtensions.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bit.Core.Models.Domain; -using Foundation; +using Foundation; using Newtonsoft.Json; namespace Bit.iOS.Core.Utilities @@ -15,6 +11,7 @@ namespace Bit.iOS.Core.Utilities } public static NSDictionary ToNSDictionary(this Dictionary dict, Func keyConverter, Func valueConverter) + where KFrom : notnull where KTo : NSObject where VTo : NSObject { @@ -23,19 +20,20 @@ namespace Bit.iOS.Core.Utilities return NSDictionary.FromObjectsAndKeys(NSValues, NSKeys, NSKeys.Count()); } - public static Dictionary ToDictionary(this NSDictionary nsDict) + public static Dictionary ToDictionary(this NSDictionary nsDict) { - return nsDict.ToDictionary(v => v?.ToString() as object); + return nsDict.ToDictionary(v => v?.ToString()); } - public static Dictionary ToDictionary(this NSDictionary nsDict, Func valueTransformer) + public static Dictionary ToDictionary(this NSDictionary nsDict, Func valueTransformer) { return nsDict.ToDictionary(k => k.ToString(), v => valueTransformer(v)); } - public static Dictionary ToDictionary(this NSDictionary nsDict, Func keyConverter, Func valueConverter) + public static Dictionary ToDictionary(this NSDictionary nsDict, Func keyConverter, Func valueConverter) where KFrom : NSObject where VFrom : NSObject + where KTo : notnull { var keys = nsDict.Keys.Select(k => keyConverter(k)).ToArray(); var values = nsDict.Values.Select(v => valueConverter(v)).ToArray(); diff --git a/src/iOS.Core/Utilities/WCSessionManager.cs b/src/iOS.Core/Utilities/WCSessionManager.cs index e3e0375cd..d4d116b04 100644 --- a/src/iOS.Core/Utilities/WCSessionManager.cs +++ b/src/iOS.Core/Utilities/WCSessionManager.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Bit.Core.Models.Domain; +using System.Diagnostics; using Bit.Core.Services; using Bit.iOS.Core.Utilities; using Foundation; using Newtonsoft.Json; -using ObjCRuntime; namespace WatchConnectivity { @@ -17,35 +11,45 @@ namespace WatchConnectivity // Setup is converted from https://www.natashatherobot.com/watchconnectivity-say-hello-to-wcsession/ // with some extra bits private static readonly WCSessionManager sharedManager = new WCSessionManager(); - private static WCSession session = WCSession.IsSupported ? WCSession.DefaultSession : null; + private static WCSession? session = WCSession.IsSupported ? WCSession.DefaultSession : null; - public event WCSessionReceiveDataHandler OnApplicationContextUpdated; - public event WCSessionReceiveDataHandler OnMessagedReceived; - public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary data); + public event WCSessionReceiveDataHandler? OnApplicationContextUpdated; + public event WCSessionReceiveDataHandler? OnMessagedReceived; + public delegate void WCSessionReceiveDataHandler(WCSession session, Dictionary data); - WCSessionUserInfoTransfer _transf; + WCSessionUserInfoTransfer? _transf; - private WCSession validSession + private WCSession? validSession { get { + if (session is null) + { + return null; + } + Debug.WriteLine($"Paired status:{(session.Paired ? '✓' : '✗')}\n"); Debug.WriteLine($"Watch App Installed status:{(session.WatchAppInstalled ? '✓' : '✗')}\n"); return (session.Paired && session.WatchAppInstalled) ? session : null; } } - private WCSession validReachableSession + private WCSession? validReachableSession { get { + if (session is null) + { + return null; + } + return session.Reachable ? validSession : null; } } public bool IsValidSession => validSession != null; - public bool IsSessionReachable => session.Reachable; + public bool IsSessionReachable => session?.Reachable ?? false; public bool IsSessionActivated => validSession?.ActivationState == WCSessionActivationState.Activated; @@ -71,7 +75,7 @@ namespace WatchConnectivity public override void SessionReachabilityDidChange(WCSession session) { - Debug.WriteLine($"Watch connectivity Reachable:{(session.Reachable ? '✓' : '✗')}"); + Debug.WriteLine($"Watch connectivity Reachable:{(session?.Reachable == true ? '✓' : '✗')}"); } public void SendBackgroundHighPriorityMessage(NSDictionary applicationContext) @@ -102,7 +106,7 @@ namespace WatchConnectivity public void SendBackgroundFifoHighPriorityMessage(Dictionary message) { - if(validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) + if (session is null || validSession is null || validSession.ActivationState != WCSessionActivationState.Activated) { return; } @@ -112,6 +116,10 @@ namespace WatchConnectivity Debug.WriteLine("Started transferring user info"); _transf = session.TransferUserInfo(message.ToNSDictionary()); + if (_transf is null) + { + return; + } Task.Run(async () => { @@ -136,7 +144,7 @@ namespace WatchConnectivity if (OnApplicationContextUpdated != null) { var keys = applicationContext.Keys.Select(k => k.ToString()).ToArray(); - var values = applicationContext.Values.Select(v => JsonConvert.DeserializeObject(v.ToString())).ToArray(); + var values = applicationContext.Values.Select(v => v != null ? JsonConvert.DeserializeObject(v.ToString()) : null).ToArray(); var dictionary = keys.Zip(values, (k, v) => new { Key = k, Value = v }) .ToDictionary(x => x.Key, x => x.Value); diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index dd2efa399..c2efab343 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -29,8 +29,6 @@ namespace Bit.iOS.Core.Utilities { var builder = Bit.Core.MauiProgram.ConfigureMauiAppBuilder(ConfigureMAUIEffects, handlers => { - // WORKAROUND: This is needed to make TapGestureRecognizer work on extensions. - handlers.AddHandler(typeof(Window), typeof(Handlers.CustomWindowHandler)); ConfigureMAUIHandlers(handlers); }) .UseMauiEmbedding(); @@ -116,9 +114,13 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("nativeLogService", new ConsoleLogService()); } - ILogger logger = null; - if (ServiceContainer.Resolve("logger", true) == null) + ILogger? logger = null; + if (ServiceContainer.TryResolve(out var resolvedLogger)) { + logger = resolvedLogger; + } + else + { #if DEBUG logger = DebugLogger.Instance; #else @@ -129,6 +131,12 @@ namespace Bit.iOS.Core.Utilities var preferencesStorage = new PreferencesStorageService(AppGroupId); var appGroupContainer = new NSFileManager().GetContainerUrl(AppGroupId); + if (appGroupContainer?.Path is null) + { + var nreAppGroupContainer = new NullReferenceException("appGroupContainer or its Path is null when registering local services"); + logger!.Exception(nreAppGroupContainer); + throw nreAppGroupContainer; + } var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); @@ -187,14 +195,14 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve())); } - public static void Bootstrap(Func postBootstrapFunc = null) + public static void Bootstrap(Func? postBootstrapFunc = null) { var locale = ServiceContainer.Resolve().GetLocale(); (ServiceContainer.Resolve("i18nService") as MobileI18nService) - .Init(locale != null ? new System.Globalization.CultureInfo(locale) : null); + ?.Init(locale != null ? new System.Globalization.CultureInfo(locale) : null); ServiceContainer.Resolve("authService").Init(); (ServiceContainer. - Resolve("platformUtilsService") as MobilePlatformUtilsService).Init(); + Resolve("platformUtilsService") as MobilePlatformUtilsService)?.Init(); var accountsManager = new AccountsManager( ServiceContainer.Resolve("broadcasterService"), @@ -231,20 +239,31 @@ namespace Bit.iOS.Core.Utilities if (message.Command == "showDialog") { var details = message.Data as DialogDetails; + if (details is null) + { + return; + } var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? AppResources.Ok : details.ConfirmText; NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - var result = await deviceActionService.DisplayAlertAsync(details.Title, details.Text, - details.CancelText, confirmText); - var confirmed = result == details.ConfirmText; - messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + try + { + var result = await deviceActionService.DisplayAlertAsync(details.Title, details.Text, + details.CancelText, confirmText); + var confirmed = result == details.ConfirmText; + messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } }); } - else if (message.Command == "listenYubiKeyOTP") + else if (message.Command == "listenYubiKeyOTP" && message.Data is bool listen) { - ListenYubiKey((bool)message.Data, deviceActionService, nfcSession, nfcDelegate); + ListenYubiKey(listen, deviceActionService, nfcSession, nfcDelegate); } }); } @@ -268,29 +287,36 @@ namespace Bit.iOS.Core.Utilities } } - private static async Task BootstrapAsync(Func postBootstrapFunc = null) + private static async Task BootstrapAsync(Func? postBootstrapFunc = null) { - await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); - - InitializeAppSetup(); - // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged - var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner( - ServiceContainer.Resolve("apiService"), - ServiceContainer.Resolve("messagingService"), - ServiceContainer.Resolve("platformUtilsService"), - ServiceContainer.Resolve("deviceActionService"), - ServiceContainer.Resolve("logger")); - ServiceContainer.Register("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner); - - var verificationActionsFlowHelper = new VerificationActionsFlowHelper( - ServiceContainer.Resolve("passwordRepromptService"), - ServiceContainer.Resolve("cryptoService"), - ServiceContainer.Resolve()); - ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); - - if (postBootstrapFunc != null) + try { - await postBootstrapFunc.Invoke(); + await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); + + InitializeAppSetup(); + // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged + var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner( + ServiceContainer.Resolve("apiService"), + ServiceContainer.Resolve("messagingService"), + ServiceContainer.Resolve("platformUtilsService"), + ServiceContainer.Resolve("deviceActionService"), + ServiceContainer.Resolve("logger")); + ServiceContainer.Register("deleteAccountActionFlowExecutioner", deleteAccountActionFlowExecutioner); + + var verificationActionsFlowHelper = new VerificationActionsFlowHelper( + ServiceContainer.Resolve("passwordRepromptService"), + ServiceContainer.Resolve("cryptoService"), + ServiceContainer.Resolve()); + ServiceContainer.Register("verificationActionsFlowHelper", verificationActionsFlowHelper); + + if (postBootstrapFunc != null) + { + await postBootstrapFunc.Invoke(); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } } diff --git a/src/iOS.Core/Views/ExtensionSearchDelegate.cs b/src/iOS.Core/Views/ExtensionSearchDelegate.cs index 3fa4ff64a..b3753d511 100644 --- a/src/iOS.Core/Views/ExtensionSearchDelegate.cs +++ b/src/iOS.Core/Views/ExtensionSearchDelegate.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using Bit.Core.Services; using Foundation; using UIKit; @@ -9,7 +7,7 @@ namespace Bit.iOS.Core.Views public class ExtensionSearchDelegate : UISearchBarDelegate { private readonly UITableView _tableView; - private CancellationTokenSource _filterResultsCancellationTokenSource; + private CancellationTokenSource? _filterResultsCancellationTokenSource; public ExtensionSearchDelegate(UITableView tableView) { @@ -23,25 +21,34 @@ namespace Bit.iOS.Core.Views { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (!string.IsNullOrWhiteSpace(searchText)) - { - await Task.Delay(300); - if (searchText != searchBar.Text) - { - return; - } - else - { - _filterResultsCancellationTokenSource?.Cancel(); - } - } try { - ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); - _tableView.ReloadData(); + if (!string.IsNullOrWhiteSpace(searchText)) + { + await Task.Delay(300); + if (searchText != searchBar.Text) + { + return; + } + else + { + _filterResultsCancellationTokenSource?.Cancel(); + } + } + try + { + ((ExtensionTableSource)_tableView.Source).FilterResults(searchText, cts.Token); + _tableView.ReloadData(); + } + catch (OperationCanceledException) { } + _filterResultsCancellationTokenSource = cts; + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + _filterResultsCancellationTokenSource?.Cancel(); + cts?.Cancel(); } - catch (OperationCanceledException) { } - _filterResultsCancellationTokenSource = cts; }); }, cts.Token); } diff --git a/src/iOS.Extension/LoadingViewController.TapGestureHack.cs b/src/iOS.Extension/LoadingViewController.TapGestureHack.cs deleted file mode 100644 index f5e09ca22..000000000 --- a/src/iOS.Extension/LoadingViewController.TapGestureHack.cs +++ /dev/null @@ -1,54 +0,0 @@ -#if ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - -using System; -using System.Linq; -using Bit.iOS.Core.Utilities; -using Bit.iOS.Extension.Models; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform; -using UIKit; - -namespace Bit.iOS.Extension -{ - public partial class LoadingViewController : UIViewController - { - const string STORYBOARD_NAME = "MainInterface"; - Lazy _storyboard = new Lazy(() => UIStoryboard.FromName(STORYBOARD_NAME, null)); - - public void InitWithContext(Context context) - { - _context = context; - _shouldInitialize = false; - } - - public void DismissLockAndContinue() - { - if (UIApplication.SharedApplication.KeyWindow is null) - { - return; - } - - UIApplication.SharedApplication.KeyWindow.RootViewController = _storyboard.Value.InstantiateInitialViewController(); - - if (UIApplication.SharedApplication.KeyWindow?.RootViewController is UINavigationController navContr) - { - var rootVC = navContr.ViewControllers.FirstOrDefault(); - if (rootVC is LoadingViewController loadingVC) - { - loadingVC.InitWithContext(_context); - loadingVC.ContinueOn(); - } - } - } - - private void NavigateToPage(ContentPage page) - { - var navigationPage = new NavigationPage(page); - - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - } - } -} - -#endif \ No newline at end of file diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 4daeb7670..b3874fa64 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -18,6 +18,8 @@ using Bit.iOS.Core.Views; using Bit.iOS.Extension.Models; using CoreNFC; using Foundation; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using MobileCoreServices; using UIKit; @@ -151,7 +153,6 @@ namespace Bit.iOS.Extension } } -#if !ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND public void DismissLockAndContinue() { Debug.WriteLine("BW Log, Dismissing lock controller."); @@ -166,7 +167,6 @@ namespace Bit.iOS.Extension PresentViewController(uiController, true, null); } -#endif private void ContinueOn() { @@ -479,16 +479,23 @@ namespace Bit.iOS.Extension { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (await IsAuthed()) + try { - var stateService = ServiceContainer.Resolve("stateService"); - await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); - var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - if (deviceActionService.SystemMajorVersion() >= 12) + if (await IsAuthed()) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + var stateService = ServiceContainer.Resolve("stateService"); + await AppHelpers.LogOutAsync(await stateService.GetActiveUserIdAsync()); + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } }); } diff --git a/src/iOS.Extension/iOS.Extension.csproj b/src/iOS.Extension/iOS.Extension.csproj index f374b47be..8829d9423 100644 --- a/src/iOS.Extension/iOS.Extension.csproj +++ b/src/iOS.Extension/iOS.Extension.csproj @@ -9,8 +9,6 @@ 1 False - - $(DefineConstants);ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND 11.0 Bit.iOS.Extension @@ -75,7 +73,6 @@ - diff --git a/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs b/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs deleted file mode 100644 index 7dc69428c..000000000 --- a/src/iOS.ShareExtension/LoadingViewController.TapGestureHack.cs +++ /dev/null @@ -1,22 +0,0 @@ -#if ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - -using Bit.iOS.Core.Utilities; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform; -using UIKit; - -namespace Bit.iOS.ShareExtension -{ - public partial class LoadingViewController : UIViewController - { - private void NavigateToPage(ContentPage page) - { - var navigationPage = new NavigationPage(page); - - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - } - } -} - -#endif diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index c77582ca1..22cb86fb8 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -137,23 +137,16 @@ namespace Bit.iOS.ShareExtension } } -#if !ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND - private void NavigateToPage(ContentPage page) { var navigationPage = new NavigationPage(page); - var window = new Window(navigationPage); - window.ToHandler(MauiContextSingleton.Instance.MauiContext); - _currentModalController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext); _currentModalController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; _presentingOnNavigationPage = true; PresentViewController(_currentModalController, true, null); } -#endif - public void DismissLockAndContinue() { Debug.WriteLine("BW Log, Dismissing lock controller."); @@ -274,14 +267,22 @@ namespace Bit.iOS.ShareExtension { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (await IsAuthed()) + try { - await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); - if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + if (await IsAuthed()) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } }); } diff --git a/src/iOS.ShareExtension/iOS.ShareExtension.csproj b/src/iOS.ShareExtension/iOS.ShareExtension.csproj index b8718e428..1f357ce2b 100644 --- a/src/iOS.ShareExtension/iOS.ShareExtension.csproj +++ b/src/iOS.ShareExtension/iOS.ShareExtension.csproj @@ -9,8 +9,6 @@ 1 False - - $(DefineConstants);ENABLED_TAP_GESTURE_RECOGNIZER_MAUI_EMBEDDED_WORKAROUND 11.0 Bit.iOS.ShareExtension @@ -67,7 +65,6 @@ ExtensionNavigationController.cs - diff --git a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings index cad65e2b8..42c4107ef 100644 --- a/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings +++ b/src/watchOS/bitwarden/bitwarden WatchKit Extension/Localization/cy.lproj/Localizable.strings @@ -5,6 +5,6 @@ "SyncingItemsContainingVerificationCodes" = "Syncing items containing verification codes"; "UnlockBitwardenOnYourIPhoneToViewVerificationCodes" = "Unlock Bitwarden on your iPhone to view verification codes"; "SetUpBitwardenToViewItemsContainingVerificationCodes" = "Set up Bitwarden to view items containing verification codes"; -"Search" = "Search"; +"Search" = "Chwilio"; "NoItemsFound" = "No items found"; "SetUpAppleWatchPasscodeInOrderToUseBitwarden" = "Set up Apple Watch passcode in order to use Bitwarden";