diff --git a/src/Core/App.xaml.cs b/src/Core/App.xaml.cs index a3d2d1406..0b5b90d8f 100644 --- a/src/Core/App.xaml.cs +++ b/src/Core/App.xaml.cs @@ -10,6 +10,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Response; +using Bit.Core.Pages; using Bit.Core.Services; using Bit.Core.Utilities; @@ -74,6 +75,8 @@ namespace Bit.App } public static Window CurrentWindow { get; private set; } + public static Window AutofillWindow { get; private set; } + public static Window MainWindow { get; private set; } public void SetOptions(AppOptions appOptions) { @@ -84,28 +87,28 @@ namespace Bit.App { if (activationState != null && activationState.State.TryGetValue("autofillFramework", out string autofillFramework) - && autofillFramework == "true") //TODO: There are likely better ways to filter this. Maybe using Options. + && autofillFramework == "true" + && activationState.State.ContainsKey("autofillFrameworkCipherId")) //AutofillExternalActivity { - if (activationState.State.ContainsKey("autofillFrameworkCipherId")) //TODO: There are likely better ways to filter this. Maybe using Options. - { - return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally) - } - else - { - //TODO: IMPORTANT Question: this works if we want to show the CipherSelection, but we are skipping all our ASYNC Navigation workflows where we (for example) check if the user is logged in and/or needs to enter auth again. - var autofillWindow = new Window(new NavigationPage(new CipherSelectionPage(Options))); - return autofillWindow; - } + return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally) + } + else if (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null) //"Internal" Autofill and Uri/Otp/CreateSend + { + AutofillWindow = new Window(new NavigationPage(new AndroidExtSplashPage(Options))); + CurrentWindow = AutofillWindow; + return CurrentWindow; } else if(CurrentWindow != null) { //TODO: This likely crashes if we try to have two apps side-by-side on Android //TODO: Question: In these scenarios should a new Window be created or can the same one be reused? + CurrentWindow = MainWindow; return CurrentWindow; } else { - CurrentWindow = new Window(new NavigationPage(new HomePage(Options))); + MainWindow = new Window(new NavigationPage(new HomePage(Options))); + CurrentWindow = MainWindow; return CurrentWindow; } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 3471f8e48..d0426ef8e 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -64,7 +64,6 @@ - @@ -92,4 +91,9 @@ True + + + MSBuild:Compile + + \ No newline at end of file diff --git a/src/Core/Pages/AndroidExtSplashPage.xaml b/src/Core/Pages/AndroidExtSplashPage.xaml new file mode 100644 index 000000000..e54fc5a37 --- /dev/null +++ b/src/Core/Pages/AndroidExtSplashPage.xaml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/Core/Pages/AndroidExtSplashPage.xaml.cs b/src/Core/Pages/AndroidExtSplashPage.xaml.cs new file mode 100644 index 000000000..90485c534 --- /dev/null +++ b/src/Core/Pages/AndroidExtSplashPage.xaml.cs @@ -0,0 +1,105 @@ +using Bit.App.Models; +using Bit.App.Pages; +using Bit.App.Utilities.AccountManagement; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; + +namespace Bit.Core.Pages; + +public partial class AndroidExtSplashPage : ContentPage +{ + private IConditionedAwaiterManager _conditionedAwaiterManager; + private IVaultTimeoutService _vaultTimeoutService; + private IStateService _stateService; + private AppOptions _appOptions; + + public AndroidExtSplashPage(AppOptions appOptions) + { + InitializeComponent(); + _appOptions = appOptions; + _conditionedAwaiterManager = ServiceContainer.Resolve(); + _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _stateService = ServiceContainer.Resolve(); + } + + private async Task GetNavigationToExecuteAsync(AppOptions options, Window window) + { + await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited); + + var authed = await _stateService.IsAuthenticatedAsync(); + if (authed) + { + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + var navParams = new LoginNavigationParams(email); + if (navParams is LoginNavigationParams loginParams) + { + window.Page = new NavigationPage(new LoginPage(loginParams.Email, options)); + } + } + else if (await _vaultTimeoutService.IsLockedAsync() || + await _vaultTimeoutService.ShouldLockAsync()) + { + /* //TODO: is lockParams needed here? + if (navParams is LockNavigationParams lockParams) + { + return () => new Window(new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric))); + } + else + {*/ + window.Page = new NavigationPage(new LockPage(options)); + /*}*/ + } + else if (options.FromAutofillFramework && options.SaveType.HasValue) + { + window.Page = new NavigationPage(new CipherAddEditPage(appOptions: options)); + } + else if (options.Uri != null) + { + window.Page = new NavigationPage(new CipherSelectionPage(options)); + } + else if (options.OtpData != null) + { + window.Page = new NavigationPage(new CipherSelectionPage(options)); + } + else if (options.CreateSend != null) + { + window.Page = new NavigationPage(new SendAddEditPage(options)); + } + else + { + window.Page = new TabsPage(options); + } + } + else + { + options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null; + if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() || + await _vaultTimeoutService.ShouldLogOutByTimeoutAsync()) + { + // TODO implement orgIdentifier flow to SSO Login page, same as email flow below + // var orgIdentifier = await _stateService.GetOrgIdentifierAsync(); + + var email = await _stateService.GetEmailAsync(); + await _stateService.SetRememberedEmailAsync(email); + + window.Page = new NavigationPage(new HomePage(options)); + } + else + { + window.Page = new NavigationPage(new HomePage(options)); + } + } + } + + private async void AndroidExtSplashPage_OnLoaded(object sender, EventArgs e) + { + await GetNavigationToExecuteAsync(_appOptions, this.Window); + } +} diff --git a/src/Core/Pages/TabsPage.cs b/src/Core/Pages/TabsPage.cs index 6c2c64ec2..c9d02e320 100644 --- a/src/Core/Pages/TabsPage.cs +++ b/src/Core/Pages/TabsPage.cs @@ -144,7 +144,17 @@ namespace Bit.App.Pages private void OnUnloaded(object sender, EventArgs e) { - Handler?.DisconnectHandler(); + try + { + Handler?.DisconnectHandler(); + } + catch (Exception ex) + { + //Workaround: Currently the Disconnect Handler needs to be manually called from the App: https://github.com/dotnet/maui/issues/3604 + // In some specific edges cases the MauiContext can be gone when we call this. (for example filling a field using Accessibility) + // In those scenarios the app should just be "closing" anyway, so we just want to avoid the exception. + System.Diagnostics.Debug.WriteLine(ex.Message); + } } public void ResetToVaultPage()