1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-04 09:33:16 +00:00

Compare commits

..

43 Commits

Author SHA1 Message Date
github-actions[bot]
efe128b932 Bumped version to 2022.6.2 (#1981)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 7802da2b9c)
2022-07-21 11:16:57 -04:00
Federico Maccaroni
df2e52f82c EC-308 Fix crash produced by creating avatar image on AccountSwitchingOverlayHelper and also added more logging to see when it happens. (#1983)
(cherry picked from commit 846d3a85a2)
2022-07-20 17:51:00 -04:00
github-actions[bot]
2d224f5e22 Bumped version to 2022.6.1 (#1969)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit a6ddc2496f)
2022-07-06 18:56:39 -04:00
Matt Gibson
4831097c0b Add user verification to reset password request (#1980)
We only need master password hash because this is currently
only used for sso password setting after auto-provisioning. Key
Connector is not involved in these accounts

(cherry picked from commit 58a3662d0f)
2022-07-06 18:24:33 -04:00
github-actions[bot]
72f2983885 Bumped version to 2022.6.0 (#1968)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit d9a818279f)
2022-06-29 09:12:27 -07:00
Matt Gibson
ee68dcf3b0 Add ssoToken to limit lifetime of SSO redirect (#1965)
(cherry picked from commit 6e2e613fee)
2022-06-27 16:05:11 -04:00
Federico Andrés Maccaroni
daab473cbb SG-396 Fix tappable area after hiding account switching 2022-06-21 09:33:59 -03:00
Joseph Flinn
e6b99270cf Pin NuGet version (#1957)
* Pinning the version of NuGet to 5.x

* pinning NuGet verison to 5.9.x

* pinning NuGet to 5.9.0.7134

* pinning NuGet to 5.9.0

(cherry picked from commit c892e9fa57)
2022-06-16 14:48:47 -07:00
Federico Andrés Maccaroni
a8d9aaa7fe [SG-386] iOS Update user state when coming from background 2022-06-16 14:01:28 -03:00
mp-bw
542ef5f31a fix for missing personal items added prior to joining org with personal ownership policy (#1955) 2022-06-16 09:56:03 -04:00
mp-bw
0b2fc2a647 separate init and showVaultFilter property set (#1954) 2022-06-15 15:19:34 -04:00
mp-bw
a95fdf67a1 [SG-390] Fix for missing org items with single org & personal ownership enabled (#1953)
* fix for missing org items with single org & personal ownership enabled

* fix for ui issue with vault filter state change on pull to refresh
2022-06-15 10:45:01 -04:00
mp-bw
73890162bf Additional logic around filter display (#1951) 2022-06-14 13:05:28 -04:00
Joseph Flinn
ef14a8f850 Updating the release version check to use the new action (#1934)
* Updating the release version check to use the new action

* Update .github/workflows/release.yml

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
(cherry picked from commit 5579817f9f)
2022-06-13 12:59:24 -07:00
mp-bw
51a5f58258 Fixed vault filter display issue with personal ownership policy (#1950) 2022-06-13 10:30:32 -03:00
mp-bw
388ad4e840 lib updates (#1949) 2022-06-10 12:24:02 -04:00
mp-bw
48a8d9ae35 Clipboard handling adjustments for Android 13 (#1947)
* Android 13 clipboard tweaks

* adjustments

* adjustments round 2
2022-06-10 12:02:17 -04:00
mp-bw
dd6003bd4f alphabetize org list in vault filter (#1945) 2022-06-08 17:20:23 -04:00
Federico Maccaroni
fba407f3b6 SG-210 Account Switching in Autofill (iOS) (#1909)
* SG-210 Set up account switching on Autofill iOS

* SG-210 Fix refresh after sync on autofill ciphers, also added account switching on lock view on autofill. Also fix possible crash when scrolling when no items were displayed and also fixed navigation when login in on an automatically logged out account.

* SG-210 Added reference on iOS.Core project

* Fix formatting on AccountManager

* SG-210 Fix background color for dark theme
2022-06-08 14:24:01 -03:00
mp-bw
88b406544b [SG-79] Add filter to search and preselect org in new cipher (#1944)
* Add filter to search and preselect org in new cipher

* formatting

* fixes
2022-06-08 09:39:53 -04:00
dwbit
3438ed94ce Changing Report crash logs to Submit crash logs (#1937)
* Changing Report crash logs to Submit crash logs

* Updated resources keys to match the new value

Co-authored-by: Federico Andrés Maccaroni <fedemkr@gmail.com>
2022-06-07 21:47:44 -03:00
Federico Maccaroni
ec71b21264 PS-785 Added logs for exceptions on UpdateTemplatedCell and the extra data on each ExtendedCollectionView usage (#1943) 2022-06-07 15:57:53 -03:00
Federico Maccaroni
b223f5f16e EC-255 fix crash when scanning TOTP; BeginInvokeOnMainThread doesn't bubble up the exception, just crashes because it throws the exception to the current main thread context; so it was changed to InvokeOnMainThreadAsync which does bubble up the exception. (#1942) 2022-06-07 10:46:48 -04:00
Carlos Gonçalves
0a64e4c918 PS-587 - Fix searchbar search textfield and icon colors for dark themes (#1941)
* PS-587 Fix searchbar search textfield and icon colors for dark themes

* PS-587 - PR corrections
2022-06-07 15:43:25 +01:00
mp-bw
9b41db962e Added monochrome element to adaptive icon for theme support in Android 13 (#1940) 2022-06-06 14:50:52 -03:00
Federico Maccaroni
43d3c7b5d7 Update PULL_REQUEST_TEMPLATE.md (#1938) 2022-06-06 10:30:59 -03:00
CarleyDiaz-Bitwarden-zz
8168089591 Updating icons for Linked and Boolean fields (#1935)
Update to icons used for Boolean and Linked to use real icons

Co-authored-by: CarleyDiaz-Bitwarden <103955722+CarleyDiaz-Bitwarden@users.noreply.github.com>
2022-06-02 16:43:14 -04:00
github-actions[bot]
6b55fc3032 Bumped version to 2022.05.1 (#1933)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-02 09:30:39 -04:00
github-actions[bot]
87ab42b155 Bumped version to 2022.05.0 (#1931)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-06-01 16:18:06 -04:00
André Filipe da Silva Bispo
98130e89de PS-689 Android: Accessibility - back buttons in search and vault > bin lack appropriate accessible name (#1929)
* PS-689 Added back buttons accessibility text

* PS-689 Changed resource key from "GoBack" to "TapToGoBack"

* PS-689: class rename
2022-06-01 20:50:19 +01:00
André Filipe da Silva Bispo
121f0e3628 PS-675 Added accessibility text to password show/hide toggles (#1926)
* PS-675 Added accessibility text to password show hide toggles

* PS-675 refactor string resource key name
2022-06-01 16:02:28 +01:00
mp-bw
8a3d88b3ce [SG-79] Mobile Vault Filter (#1928)
* [SG-79] Vault Filter

* Update vault button text after sync

* formatting

* cleanup

* cleanup
2022-05-31 13:34:54 -04:00
Federico Maccaroni
b8b41fe847 [PS-536] Fix vault blank after unlocking and back navigation (#1930)
* PS-536 Fix by hack vault blank after unlocking and back navigate when previous page has value on iOS

* PS-536 Added platform check to the hack so it doesn't affect Android performance given that's an issue particular for iOS
2022-05-27 17:17:08 -04:00
André Filipe da Silva Bispo
5bbef3ee16 [PS-676] Accessibility - "Options" expand/collapse control does not announce state (#1925)
* PS-676: Accessibility - "Options" expand/collapse control does not announce state
- Moved to click event to the stacklayout
- Added accessibility text to stacklayout
- Removed accessibility on views inside stacklayout

* PS-676 Changed event to command

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-27 14:52:19 +01:00
Carlos Gonçalves
9a2b6c8ec9 PS-593 - View model properties are now updated on main thread (#1927) 2022-05-27 14:16:45 +01:00
André Filipe da Silva Bispo
5272c99643 PS-674: Accessibility password generator toggles (#1924)
- Additional information for VoiceOver and TalkBack
- Added new labels

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-25 17:25:21 +01:00
André Filipe da Silva Bispo
43e9515a03 [PS-672] Accessibility - File/Text controls unintuitive, "Text" accessible name incorrect (#1923)
* PS-672: Accessibility - File/Text controls unintuitive, "Text" accessible name incorrect
- Added new text to help identify File / Text segmented button
- Added missing text for Text button accessibility

* PS-672: refactor code with pr suggestions

* PS-672: removed unnecessary code

* PS-672: change text from "click" to "tap"

* PS-672: removed comma

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-25 17:20:51 +01:00
André Filipe da Silva Bispo
7e9b7398c8 PS-90: App keeps asking for 2FA even though I've checked the "remember me" checkbox (#1921)
- Logout was removing the 2FA token, portion of code deleted.
- Clear saved token when 2FA error is returned by the server.

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-24 15:09:24 +01:00
Jake Fink
58d7b001a5 add master password reprompt to share sheet extension (#1922) 2022-05-23 16:01:04 -04:00
André Filipe da Silva Bispo
a259560d29 PS-592 Mobile - Name field is not prioritised in search results (#1907)
- Search method now gives priority to matches in the Name property

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-23 10:20:48 +01:00
André Filipe da Silva Bispo
22c746543a PS-518 - Add setting to block AppCenter / Analytics - Mobile (#1905)
* PS-518 - Add setting to block AppCenter / Analytics - Mobile
- Added another entry into Settings page under the Others section
- Added prompt to ask user to enable / disable Crash Reports
- Added compilation tags to remove if the build is FDroid 

* PS-518 Add setting to block AppCenter / Analytics - Mobile
- Reduced FDroid compilation tags throughout the code
- Added Init, Enable and State methods to Logger
- Simplified SettingsPageViewModel Enable/Disable code

* PS-518 Add setting to block AppCenter / Analytics - Mobile
- Appcenter references were removed from App project, 
- Removed FDroid build.yml code that was deleting Appcenter packages from App.csproj

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-18 17:59:19 +01:00
André Filipe da Silva Bispo
bcbc2738ca PS-591 Fix avoid ambiguous characters #1664 (#1906)
* PS-591 - iOS - Avoid ambiguous characters is activated inside the main client, but is deactivated when creating a vault item from the autofill prompt. #1664
- Refactor the name of the property Ambiguous to AvoidAmbiguous, this naming was misleading.
- Fixed bug where the boolean value for the AvoidAmbiguous property was being stored inverted.

* PS-591 - iOS - Avoid ambiguous characters is activated inside the main client, but is deactivated when creating a vault item from the autofill prompt. #1664
- Changed AvoidAmbiguous to AllowAmbiguous
- Added wrapper Prop to bind UI Toggle to VM

Co-authored-by: André Bispo <abispo@bitwarden.com>
2022-05-18 14:58:49 +01:00
Thomas Rittson
604e3b6892 Remove testing requirements from pr template (#1901) 2022-05-10 17:02:40 -04:00
131 changed files with 8455 additions and 4543 deletions

View File

@@ -21,12 +21,8 @@
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -60,6 +60,11 @@ jobs:
runs-on: windows-2019
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
@@ -209,6 +214,11 @@ jobs:
name: F-Droid Build
runs-on: windows-2019
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
@@ -304,18 +314,6 @@ jobs:
$xml.Save($androidPath);
Write-Output "########################################"
Write-Output "##### Uninstall from App.csproj"
Write-Output "########################################"
$xml=New-Object XML;
$xml.Load($appPath);
$appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']");
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
$xml.Save($appPath);
Write-Output "########################################"
Write-Output "##### Uninstall from Core.csproj"
Write-Output "########################################"
@@ -380,6 +378,11 @@ jobs:
runs-on: macos-11
needs: setup
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
with:
nuget-version: 5.9.0
- name: Print environment
run: |
nuget help | grep Version

View File

@@ -34,29 +34,13 @@ jobs:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Retrieve Mobile release version
id: retrieve-mobile-version
run: |
ver=$(sed -E -n '/^<manifest/s/^.*[ ]android:versionName="([^"]+)".*$/\1/p' \
./src/Android/Properties/AndroidManifest.xml | tr -d '"')
echo "::set-output name=mobile_version::${ver}"
shell: bash
- name: Check to make sure Mobile release version has been bumped
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_ver=$(hub release -L 1 -f '%T')
latest_ver=${latest_ver:1}
echo "Latest version: $latest_ver"
ver=${{ steps.retrieve-mobile-version.outputs.mobile_version }}
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
with:
release-type: ${{ github.event.inputs.release_type }}
project-type: xamarin
file: src/Android/Properties/AndroidManifest.xml
- name: Get branch name
id: branch
@@ -83,8 +67,8 @@ jobs:
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
./Bitwarden iOS.zip"
commit: ${{ github.sha }}
tag: v${{ steps.retrieve-mobile-version.outputs.mobile_version }}
name: Version ${{ steps.retrieve-mobile-version.outputs.mobile_version }}
tag: v${{ steps.version.outputs.version }}
name: Version ${{ steps.version.outputs.version }}
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true

View File

@@ -84,7 +84,7 @@
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.2</Version>
<Version>1.7.3</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>122.0.0</Version>
@@ -145,12 +145,12 @@
<Compile Include="Tiles\GeneratorTileService.cs" />
<Compile Include="Tiles\MyVaultTileService.cs" />
<Compile Include="Utilities\AndroidHelpers.cs" />
<Compile Include="Utilities\AppCenterHelper.cs" />
<Compile Include="Utilities\ThemeHelpers.cs" />
<Compile Include="WebAuthCallbackActivity.cs" />
<Compile Include="Renderers\SelectableLabelRenderer.cs" />
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Utilities\IntentExtensions.cs" />
<Compile Include="Renderers\CustomPageRenderer.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />

View File

@@ -69,10 +69,7 @@ namespace Bit.Droid
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
}
#if !DEBUG && !FDROID
var appCenterHelper = new AppCenterHelper(_appIdService, _stateService);
var appCenterTask = appCenterHelper.InitAsync();
#endif
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
var toplayout = Window?.DecorView?.RootView;
if (toplayout != null)
@@ -85,6 +82,7 @@ namespace Bit.Droid
_appOptions = GetOptions();
LoadApplication(new App.App(_appOptions));
_broadcasterService.Subscribe(_activityKey, (message) =>
{
if (message.Command == "startEventTimer")

View File

@@ -12,7 +12,6 @@ using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Droid.Services;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Plugin.Fingerprint;
using Xamarin.Android.Net;
@@ -20,6 +19,7 @@ using System.Net.Http;
using System.Net;
using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
#if !FDROID
using Android.Gms.Security;
#endif
@@ -62,6 +62,15 @@ namespace Bit.Droid
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
var accountsManager = new AccountsManager(
ServiceContainer.Resolve<IBroadcasterService>("broadcasterService"),
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -121,13 +130,14 @@ namespace Bit.Droid
var secureStorageService = new SecureStorageService();
var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
var stateService = new StateService(mobileStorageService, secureStorageService);
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var deviceActionService = new DeviceActionService(stateService, messagingService,
var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var biometricService = new BiometricService();
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
@@ -142,7 +152,7 @@ namespace Bit.Droid
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
ServiceContainer.Register<IStateService>("stateService", stateService);
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IClipboardService>("clipboardService", new ClipboardService(stateService));
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.05.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.6.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>

View File

@@ -0,0 +1,31 @@
using System;
using Android.App;
using Android.Content;
using AndroidX.AppCompat.Widget;
using Bit.App.Resources;
using Bit.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(ContentPage), typeof(CustomPageRenderer))]
namespace Bit.Droid.Renderers
{
public class CustomPageRenderer : PageRenderer
{
public CustomPageRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
Activity context = (Activity)this.Context;
var toolbar = context.FindViewById<Toolbar>(Resource.Id.toolbar);
if(toolbar != null)
{
toolbar.NavigationContentDescription = AppResources.TapToGoBack;
}
}
}
}

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -3,11 +3,10 @@ using System.Threading.Tasks;
using Android.OS;
using Android.Security.Keystore;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Java.Security;
using Javax.Crypto;
#if !FDROID
using Microsoft.AppCenter.Crashes;
#endif
namespace Bit.Droid.Services
{
@@ -74,9 +73,7 @@ namespace Bit.Droid.Services
catch (InvalidKeyException e)
{
// Fallback for old bitwarden users without a key
#if !FDROID
Crashes.TrackError(e);
#endif
LoggerHelper.LogEvenIfCantBeResolved(e);
CreateKey();
}
@@ -101,9 +98,7 @@ namespace Bit.Droid.Services
{
// Catch silently to allow biometrics to function on devices that are in a state where key generation
// is not functioning
#if !FDROID
Crashes.TrackError(e);
#endif
LoggerHelper.LogEvenIfCantBeResolved(e);
}
}
}

View File

@@ -2,7 +2,7 @@
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Bit.Core;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Plugin.CurrentActivity;
@@ -26,13 +26,41 @@ namespace Bit.Droid.Services
PendingIntentFlags.UpdateCurrent));
}
public async Task CopyTextAsync(string text, int expiresInMs = -1)
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
{
await Clipboard.SetTextAsync(text);
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
if ((int)Build.VERSION.SdkInt < 33)
{
await Clipboard.SetTextAsync(text);
}
else
{
CopyToClipboard(text, isSensitive);
}
await ClearClipboardAlarmAsync(expiresInMs);
}
public bool IsCopyNotificationHandledByPlatform()
{
// Android 13+ provides built-in notification when text is copied to the clipboard
return (int)Build.VERSION.SdkInt >= 33;
}
private void CopyToClipboard(string text, bool isSensitive = true)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
var clipData = ClipData.NewPlainText("bitwarden", text);
if (isSensitive)
{
clipData.Description.Extras ??= new PersistableBundle();
clipData.Description.Extras.PutBoolean("android.content.extra.IS_SENSITIVE", true);
}
clipboardManager.PrimaryClip = clipData;
}
private async Task ClearClipboardAlarmAsync(int expiresInMs = -1)
{
var clearMs = expiresInMs;

View File

@@ -35,6 +35,7 @@ namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
private readonly IClipboardService _clipboardService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
@@ -47,11 +48,13 @@ namespace Bit.Droid.Services
private string _userAgent;
public DeviceActionService(
IClipboardService clipboardService,
IStateService stateService,
IMessagingService messagingService,
IBroadcasterService broadcasterService,
Func<IEventService> eventServiceFunc)
{
_clipboardService = clipboardService;
_stateService = stateService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
@@ -929,20 +932,12 @@ namespace Bit.Droid.Services
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (totp != null)
{
CopyToClipboard(totp);
await _clipboardService.CopyTextAsync(totp);
}
}
}
}
private void CopyToClipboard(string text)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text);
}
public float GetSystemFontSizeScale()
{
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;

View File

@@ -1,58 +0,0 @@
#if !FDROID
using Bit.Core.Abstractions;
using System.Threading.Tasks;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Crashes;
using Newtonsoft.Json;
namespace Bit.Droid.Utilities
{
public class AppCenterHelper
{
private const string AppSecret = "d3834185-b4a6-4347-9047-b86c65293d42";
private readonly IAppIdService _appIdService;
private readonly IStateService _stateService;
private string _userId;
private string _appId;
public AppCenterHelper(
IAppIdService appIdService,
IStateService stateService)
{
_appIdService = appIdService;
_stateService = stateService;
}
public async Task InitAsync()
{
_userId = await _stateService.GetActiveUserIdAsync();
_appId = await _appIdService.GetAppIdAsync();
AppCenter.Start(AppSecret, typeof(Crashes));
AppCenter.SetUserId(_userId);
Crashes.GetErrorAttachments = (ErrorReport report) =>
{
return new ErrorAttachmentLog[]
{
ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"),
};
};
}
public string Description
{
get
{
return JsonConvert.SerializeObject(new
{
AppId = _appId,
UserId = _userId
}, Formatting.Indented);
}
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions
{
public interface IAccountsManager
{
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.App.Abstractions
{
public interface INavigationParams { }
public interface IAccountsManagerHost
{
Task SetPreviousPageInfoAsync();
void Navigate(NavigationTarget navTarget, INavigationParams navParams = null);
Task UpdateThemeAsync();
}
}

View File

@@ -13,13 +13,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.4.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.2" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2401" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
</ItemGroup>
@@ -129,6 +128,7 @@
<Folder Include="Resources\" />
<Folder Include="Behaviors\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
</ItemGroup>
<ItemGroup>
@@ -421,5 +421,6 @@
<None Remove="Behaviors\" />
<None Remove="Xamarin.CommunityToolkit" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,7 @@ using Bit.App.Pages;
using Bit.App.Resources;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@@ -16,7 +17,7 @@ using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Bit.App
{
public partial class App : Application
public partial class App : Application, IAccountsManagerHost
{
private readonly IBroadcasterService _broadcasterService;
private readonly IMessagingService _messagingService;
@@ -27,6 +28,7 @@ namespace Bit.App
private readonly IAuthService _authService;
private readonly IStorageService _secureStorageService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAccountsManager _accountsManager;
private static bool _isResumed;
@@ -47,6 +49,9 @@ namespace Bit.App
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_accountsManager.Init(() => Options, this);
Bootstrap();
_broadcasterService.Subscribe(nameof(App), async (message) =>
@@ -71,30 +76,6 @@ namespace Bit.App
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
});
}
else if (message.Command == "locked")
{
var extras = message.Data as Tuple<string, bool>;
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? false;
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
}
else if (message.Command == "lockVault")
{
await _vaultTimeoutService.LockAsync(true);
}
else if (message.Command == "logout")
{
var extras = message.Data as Tuple<string, bool, bool>;
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false;
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
}
else if (message.Command == "loggedOut")
{
// Clean up old migrated key if they ever log out.
await _secureStorageService.RemoveAsync("oldKey");
}
else if (message.Command == "resumed")
{
if (Device.RuntimePlatform == Device.iOS)
@@ -109,22 +90,10 @@ namespace Bit.App
await SleptAsync();
}
}
else if (message.Command == "addAccount")
{
await AddAccount();
}
else if (message.Command == "accountAdded")
{
await UpdateThemeAsync();
}
else if (message.Command == "switchedAccount")
{
await SwitchedAccountAsync();
}
else if (message.Command == "migrated")
{
await Task.Delay(1000);
await SetMainPageAsync();
await _accountsManager.NavigateOnAccountChangeAsync();
}
else if (message.Command == "popAllAndGoToTabGenerator" ||
message.Command == "popAllAndGoToTabMyVault" ||
@@ -168,7 +137,6 @@ namespace Bit.App
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
});
}
@@ -234,6 +202,7 @@ namespace Bit.App
private async Task ResumedAsync()
{
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
_messagingService.Send("startEventTimer");
await UpdateThemeAsync();
@@ -263,102 +232,6 @@ namespace Bit.App
new System.Globalization.UmAlQuraCalendar();
}
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{
await AppHelpers.LogOutAsync(userId, userInitiated);
await SetMainPageAsync();
_authService.LogOut(() =>
{
if (expired)
{
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
}
});
}
private async Task AddAccount()
{
Device.BeginInvokeOnMainThread(async () =>
{
Options.HideAccountSwitcher = false;
Current.MainPage = new NavigationPage(new HomePage(Options));
});
}
private async Task SwitchedAccountAsync()
{
await AppHelpers.OnAccountSwitchAsync();
Device.BeginInvokeOnMainThread(async () =>
{
if (await _vaultTimeoutService.ShouldTimeoutAsync())
{
await _vaultTimeoutService.ExecuteTimeoutActionAsync();
}
else
{
await SetMainPageAsync();
}
await Task.Delay(50);
await UpdateThemeAsync();
});
}
private async Task SetMainPageAsync()
{
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;
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
}
else if (await _vaultTimeoutService.IsLockedAsync() ||
await _vaultTimeoutService.ShouldLockAsync())
{
Current.MainPage = new NavigationPage(new LockPage(Options));
}
else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
{
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
}
else if (Options.Uri != null)
{
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
}
else if (Options.CreateSend != null)
{
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
}
else
{
Current.MainPage = 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();
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
}
else
{
Current.MainPage = new NavigationPage(new HomePage(Options));
}
}
}
private async Task ClearCacheIfNeededAsync()
{
var lastClear = await _stateService.GetLastFileCacheClearAsync();
@@ -420,7 +293,7 @@ namespace Bit.App
UpdateThemeAsync();
};
Current.MainPage = new NavigationPage(new HomePage(Options));
var mainPageTask = SetMainPageAsync();
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
}
@@ -441,23 +314,8 @@ namespace Bit.App
});
}
private async Task LockedAsync(string userId, bool userInitiated)
public async Task SetPreviousPageInfoAsync()
{
if (!await _stateService.IsActiveAccountAsync(userId))
{
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
return;
}
var autoPromptBiometric = !userInitiated;
if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
{
var vaultTimeout = await _stateService.GetVaultTimeoutAsync();
if (vaultTimeout == 0)
{
autoPromptBiometric = false;
}
}
PreviousPageInfo lastPageBeforeLock = null;
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
{
@@ -483,8 +341,44 @@ namespace Bit.App
}
}
await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock);
var lockPage = new LockPage(Options, autoPromptBiometric);
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
}
public void Navigate(NavigationTarget navTarget, INavigationParams navParams)
{
switch (navTarget)
{
case NavigationTarget.HomeLogin:
Current.MainPage = new NavigationPage(new HomePage(Options));
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)
{
Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options));
}
break;
case NavigationTarget.Lock:
if (navParams is LockNavigationParams lockParams)
{
Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric));
}
else
{
Current.MainPage = new NavigationPage(new LockPage(Options));
}
break;
case NavigationTarget.Home:
Current.MainPage = new TabsPage(Options);
break;
case NavigationTarget.AddEditCipher:
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
break;
case NavigationTarget.AutofillCiphers:
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
break;
case NavigationTarget.SendAddEdit:
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
break;
}
}
}
}

View File

@@ -63,6 +63,10 @@ namespace Bit.App.Controls
public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70;
public bool LongPressAccountEnabled { get; set; } = true;
public Action AfterHide { get; set; }
public async Task ToggleVisibilityAsync()
{
if (IsVisible)
@@ -135,6 +139,8 @@ namespace Bit.App.Controls
// remove overlay
IsVisible = false;
AfterHide?.Invoke();
});
}
@@ -167,7 +173,7 @@ namespace Bit.App.Controls
private async Task LongPressAccountAsync(AccountViewCellViewModel item)
{
if (!item.IsAccount)
if (!LongPressAccountEnabled || !item.IsAccount)
{
return;
}

View File

@@ -45,23 +45,28 @@ namespace Bit.App.Controls
public ICommand LongPressAccountCommand { get; }
public bool FromIOSExtension { get; set; }
private async Task SelectAccountAsync(AccountViewCellViewModel item)
{
if (item.AccountView.IsAccount)
if (!item.AccountView.IsAccount)
{
if (!item.AccountView.IsActive)
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
return;
}
if (!item.AccountView.IsActive)
{
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
if (FromIOSExtension)
{
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
_messagingService.Send("switchedAccount");
}
else if (AllowActiveAccountSelection)
{
_messagingService.Send("switchedAccount");
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
}
}
else
else if (AllowActiveAccountSelection)
{
_messagingService.Send("addAccount");
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
}

View File

@@ -108,7 +108,7 @@
<controls:MiButton
Grid.Row="0"
Grid.Column="2"
Text="&#xe5d3;"
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
Clicked="MoreButton_Clicked"
VerticalOptions="CenterAndExpand"

View File

@@ -4,5 +4,6 @@ namespace Bit.App.Controls
{
public class ExtendedCollectionView : CollectionView
{
public string ExtraDataForLogging { get; set; }
}
}

View File

@@ -122,7 +122,7 @@
<controls:MiButton
Grid.Row="0"
Grid.Column="2"
Text="&#xe5d3;"
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
IsVisible="{Binding ShowOptions, Mode=OneWay}"
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
Clicked="MoreButton_Clicked"

View File

@@ -46,7 +46,11 @@ namespace Bit.App.Pages
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
additionalPropertyNames: new[]
{
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
public bool IsPolicyInEffect
@@ -68,6 +72,7 @@ namespace Bit.App.Pages
}
public string ShowPasswordIcon => ShowPassword ? "" : "";
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }

View File

@@ -80,7 +80,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
<Grid
x:Name="_passwordGrid"
@@ -119,7 +120,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout
StyleClass="box-row"

View File

@@ -72,7 +72,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText),
});
}
@@ -128,6 +129,7 @@ namespace Bit.App.Pages
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string Pin { get; set; }
public Action UnlockedAction { get; set; }

View File

@@ -101,7 +101,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
</StackLayout>
<StackLayout Padding="10, 0">

View File

@@ -58,7 +58,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
@@ -85,6 +86,7 @@ namespace Bit.App.Pages
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public Action StartTwoFactorAction { get; set; }
public Action LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }

View File

@@ -81,10 +81,12 @@ namespace Bit.App.Pages
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
string ssoToken;
try
{
await _apiService.PreValidateSso(OrgIdentifier);
var response = await _apiService.PreValidateSso(OrgIdentifier);
ssoToken = response.Token;
}
catch (ApiException e)
{
@@ -112,7 +114,8 @@ namespace Bit.App.Pages
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
"ssoToken=" + Uri.EscapeDataString(ssoToken);
WebAuthenticatorResult authResult = null;
try

View File

@@ -68,7 +68,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
@@ -106,7 +107,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label

View File

@@ -51,7 +51,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
@@ -73,6 +74,7 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string Name { get; set; }
public string Email { get; set; }
public string MasterPassword { get; set; }

View File

@@ -107,7 +107,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<Label
Text="{u:I18n MasterPasswordDescription}"
@@ -145,7 +146,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label

View File

@@ -55,7 +55,11 @@ namespace Bit.App.Pages
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
additionalPropertyNames: new[]
{
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
public bool IsPolicyInEffect
@@ -86,6 +90,7 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; }
public Command ToggleConfirmPasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string MasterPassword { get; set; }
public string ConfirmMasterPassword { get; set; }
public string Hint { get; set; }
@@ -214,7 +219,8 @@ namespace Bit.App.Pages
// Request
var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest
{
ResetPasswordKey = encryptedKey.EncryptedString
ResetPasswordKey = encryptedKey.EncryptedString,
MasterPasswordHash = masterPasswordHash,
};
var userId = await _stateService.GetActiveUserIdAsync();
// Enroll user

View File

@@ -105,7 +105,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
</StackLayout>
<StackLayout StyleClass="box">
@@ -140,7 +141,8 @@
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</Grid>
<StackLayout StyleClass="box-row">
<Label

View File

@@ -48,7 +48,8 @@
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding History}"
VerticalOptions="FillAndExpand"
StyleClass="list, list-platform">
StyleClass="list, list-platform"
ExtraDataForLogging="Generator History Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="domain:GeneratedPasswordHistory">
<Grid

View File

@@ -58,8 +58,7 @@ namespace Bit.App.Pages
private async void CopyAsync(GeneratedPasswordHistory ph)
{
await _clipboardService.CopyTextAsync(ph.Password);
_platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}
public async Task UpdateOnThemeChanged()

View File

@@ -183,52 +183,68 @@
<Label
Text="A-Z"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
<Switch
IsToggled="{Binding Uppercase}"
IsEnabled="{Binding EnforcedPolicyOptions.UseUppercase,
Converter={StaticResource inverseBool}}"
StyleClass="box-value"
HorizontalOptions="End" />
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UppercaseAtoZ}"/>
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="a-z"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LowercaseAtoZ}"/>
<Switch
IsToggled="{Binding Lowercase}"
IsEnabled="{Binding EnforcedPolicyOptions.UseLowercase,
Converter={StaticResource inverseBool}}"
StyleClass="box-value"
HorizontalOptions="End" />
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n LowercaseAtoZ}" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="0-9"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
<Switch
IsToggled="{Binding Number}"
IsEnabled="{Binding EnforcedPolicyOptions.UseNumbers,
Converter={StaticResource inverseBool}}"
StyleClass="box-value"
HorizontalOptions="End" />
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n NumbersZeroToNine}"/>
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="!@#$%^&amp;*"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
<Switch
IsToggled="{Binding Special}"
IsEnabled="{Binding EnforcedPolicyOptions.UseSpecial,
Converter={StaticResource inverseBool}}"
StyleClass="box-value"
HorizontalOptions="End" />
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n SpecialCharacters}"/>
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-stepper">
@@ -277,7 +293,7 @@
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding AvoidAmbiguous}"
IsToggled="{Binding AvoidAmbiguousChars}"
StyleClass="box-value"
HorizontalOptions="End" />
</StackLayout>

View File

@@ -5,7 +5,6 @@ using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -23,7 +22,7 @@ namespace Bit.App.Pages
private bool _lowercase;
private bool _number;
private bool _special;
private bool _avoidAmbiguous;
private bool _allowAmbiguousChars;
private int _minNumber;
private int _minSpecial;
private int _length = 5;
@@ -130,19 +129,29 @@ namespace Bit.App.Pages
}
}
public bool AvoidAmbiguous
public bool AllowAmbiguousChars
{
get => _avoidAmbiguous;
get => _allowAmbiguousChars;
set
{
if (SetProperty(ref _avoidAmbiguous, value))
if (SetProperty(ref _allowAmbiguousChars, value,
additionalPropertyNames: new string[]
{
nameof(AvoidAmbiguousChars)
}))
{
_options.Ambiguous = !value;
_options.AllowAmbiguousChar = value;
var task = SaveOptionsAsync();
}
}
}
public bool AvoidAmbiguousChars
{
get => !AllowAmbiguousChars;
set => AllowAmbiguousChars = !value;
}
public int MinNumber
{
get => _minNumber;
@@ -309,13 +318,12 @@ namespace Bit.App.Pages
public async Task CopyAsync()
{
await _clipboardService.CopyTextAsync(Password);
_platformUtilsService.ShowToast("success", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}
private void LoadFromOptions()
{
AvoidAmbiguous = !_options.Ambiguous.GetValueOrDefault();
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = TypeSelectedIndex == 0;
MinNumber = _options.MinNumber.GetValueOrDefault();
@@ -333,7 +341,7 @@ namespace Bit.App.Pages
private void SetOptions()
{
_options.Ambiguous = !AvoidAmbiguous;
_options.AllowAmbiguousChar = AllowAmbiguousChars;
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password";
_options.MinNumber = MinNumber;
_options.MinSpecial = MinSpecial;

View File

@@ -2,6 +2,7 @@
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="Bit.App.Pages.SendAddEditPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
@@ -121,6 +122,7 @@
Clicked="FileType_Clicked"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n File}"
AutomationProperties.HelpText="{Binding FileTypeAccessibilityLabel}"
Grid.Column="0">
</Button>
<Button
@@ -132,6 +134,7 @@
Clicked="TextType_Clicked"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Text}"
AutomationProperties.HelpText="{Binding TextTypeAccessibilityLabel}"
Grid.Column="1">
</Button>
</Grid>
@@ -250,28 +253,31 @@
</StackLayout>
<StackLayout
Orientation="Horizontal"
Spacing="0">
Spacing="0"
xct:TouchEffect.Command="{Binding ToggleOptionsCommand}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding OptionsAccessilibityText}">
<Button
Text="{u:I18n Options}"
x:Name="_btnOptions"
StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}"
Margin="0"
Clicked="ToggleOptions_Clicked"/>
AutomationProperties.IsInAccessibleTree="False"/>
<controls:IconButton
x:Name="_btnOptionsUp"
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}"
Clicked="ToggleOptions_Clicked"
IsVisible="{Binding ShowOptions}" />
IsVisible="{Binding ShowOptions}"
AutomationProperties.IsInAccessibleTree="False"/>
<controls:IconButton
x:Name="_btnOptionsDown"
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}"
Clicked="ToggleOptions_Clicked"
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}" />
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}"
AutomationProperties.IsInAccessibleTree="False"/>
</StackLayout>
<StackLayout IsVisible="{Binding ShowOptions}">
<StackLayout
@@ -438,7 +444,8 @@
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"

View File

@@ -209,11 +209,6 @@ namespace Bit.App.Pages
}
}
private void ToggleOptions_Clicked(object sender, EventArgs e)
{
_vm.ToggleOptions();
}
private void ClearExpirationDate_Clicked(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -44,6 +44,8 @@ namespace Bit.App.Pages
{
nameof(IsText),
nameof(IsFile),
nameof(FileTypeAccessibilityLabel),
nameof(TextTypeAccessibilityLabel)
};
private bool _disableHideEmail;
private bool _sendOptionsPolicyInEffect;
@@ -59,6 +61,7 @@ namespace Bit.App.Pages
_logger = ServiceContainer.Resolve<ILogger>("logger");
TogglePasswordCommand = new Command(TogglePassword);
ToggleOptionsCommand = new Command(ToggleOptions);
TypeOptions = new List<KeyValuePair<string, SendType>>
{
@@ -89,6 +92,7 @@ namespace Bit.App.Pages
}
public Command TogglePasswordCommand { get; set; }
public Command ToggleOptionsCommand { get; set; }
public string SendId { get; set; }
public int SegmentedButtonHeight { get; set; }
public int SegmentedButtonFontSize { get; set; }
@@ -102,6 +106,7 @@ namespace Bit.App.Pages
public bool DisableHideEmailControl { get; set; }
public bool IsAddFromShare { get; set; }
public string ShareOnSaveText => CopyInsteadOfShareAfterSaving ? AppResources.CopySendLinkOnSave : AppResources.ShareOnSave;
public string OptionsAccessilibityText => ShowOptions ? AppResources.OptionsExpanded : AppResources.OptionsCollapsed;
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
@@ -134,7 +139,11 @@ namespace Bit.App.Pages
public bool ShowOptions
{
get => _showOptions;
set => SetProperty(ref _showOptions, value);
set => SetProperty(ref _showOptions, value,
additionalPropertyNames: new[]
{
nameof(OptionsAccessilibityText)
});
}
public int ExpirationDateTypeSelectedIndex
{
@@ -211,7 +220,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
public bool DisableHideEmail
@@ -231,6 +241,9 @@ namespace Bit.App.Pages
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected;
public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected;
public async Task InitAsync()
{

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendGroupingsPage"
@@ -138,7 +138,8 @@
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform" />
StyleClass="list, list-platform"
ExtraDataForLogging="Send Groupings Page" />
</RefreshView>
</StackLayout>
</ResourceDictionary>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
@@ -66,7 +66,8 @@
VerticalOptions="FillAndExpand"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform">
StyleClass="list, list-platform"
ExtraDataForLogging="Sends Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:SendView">
<controls:SendViewCell

View File

@@ -105,6 +105,7 @@
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding UseOTPVerification, Converter={StaticResource inverseBool}}"/>
<Label
Text="{u:I18n ConfirmYourIdentity}"

View File

@@ -109,7 +109,11 @@ namespace Bit.App.Pages
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[] { nameof(ShowPasswordIcon) });
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText),
});
}
public bool UseOTPVerification
@@ -139,6 +143,7 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public void TogglePassword()
{

View File

@@ -38,7 +38,8 @@
VerticalOptions="FillAndExpand"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform">
StyleClass="list, list-platform"
ExtraDataForLogging="Folders Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:FolderView">
<controls:ExtendedStackLayout

View File

@@ -113,6 +113,7 @@
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform" />
StyleClass="list, list-platform"
ExtraDataForLogging="Settings Page" />
</pages:BaseContentPage>

View File

@@ -167,6 +167,10 @@ namespace Bit.App.Pages
{
await _vm.UpdatePinAsync();
}
else if (item.Name == AppResources.SubmitCrashLogs)
{
await _vm.LoggerReportingAsync();
}
else
{
var biometricName = AppResources.Biometrics;

View File

@@ -7,10 +7,10 @@ using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
using ZXing.Client.Result;
namespace Bit.App.Pages
{
@@ -29,7 +29,7 @@ namespace Bit.App.Pages
private readonly ILocalizeService _localizeService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
@@ -39,6 +39,7 @@ namespace Bit.App.Pages
private string _vaultTimeoutDisplayValue;
private string _vaultTimeoutActionDisplayValue;
private bool _showChangeMasterPassword;
private bool _reportLoggingEnabled;
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
@@ -79,6 +80,7 @@ namespace Bit.App.Pages
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
PageTitle = AppResources.Settings;
@@ -123,7 +125,7 @@ namespace Bit.App.Pages
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
BuildList();
}
@@ -286,6 +288,26 @@ namespace Bit.App.Pages
}
}
public async Task LoggerReportingAsync()
{
var options = new[]
{
CreateSelectableOption(AppResources.Yes, _reportLoggingEnabled),
CreateSelectableOption(AppResources.No, !_reportLoggingEnabled),
};
var selection = await Page.DisplayActionSheet(AppResources.SubmitCrashLogsDescription, AppResources.Cancel, null, options);
if (selection == null || selection == AppResources.Cancel)
{
return;
}
await _loggerService.SetEnabled(CompareSelection(selection, AppResources.Yes));
_reportLoggingEnabled = await _loggerService.IsEnabled();
BuildList();
}
public async Task VaultTimeoutActionAsync()
{
var options = _vaultTimeoutActions.Select(o =>
@@ -494,11 +516,19 @@ namespace Bit.App.Pages
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
}
var otherItems = new List<SettingsPageListItem>
{
new SettingsPageListItem { Name = AppResources.Options },
new SettingsPageListItem { Name = AppResources.About },
new SettingsPageListItem { Name = AppResources.HelpAndFeedback },
#if !FDROID
new SettingsPageListItem
{
Name = AppResources.SubmitCrashLogs,
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
},
#endif
new SettingsPageListItem { Name = AppResources.RateTheApp },
new SettingsPageListItem { Name = AppResources.DeleteAccount }
};
@@ -576,5 +606,9 @@ namespace Bit.App.Pages
{
return _vaultTimeouts.FirstOrDefault(o => o.Key == key).Value;
}
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
}
}

View File

@@ -1,4 +1,6 @@
using Bit.App.Effects;
using System;
using System.Threading.Tasks;
using Bit.App.Effects;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core.Abstractions;
@@ -10,8 +12,10 @@ namespace Bit.App.Pages
{
public class TabsPage : TabbedPage
{
private readonly IBroadcasterService _broadcasterService;
private readonly IMessagingService _messagingService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private NavigationPage _groupingsPage;
private NavigationPage _sendGroupingsPage;
@@ -19,6 +23,7 @@ namespace Bit.App.Pages
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
{
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
@@ -78,12 +83,26 @@ namespace Bit.App.Pages
protected override async void OnAppearing()
{
base.OnAppearing();
_broadcasterService.Subscribe(nameof(TabsPage), async (message) =>
{
if (message.Command == "syncCompleted")
{
Device.BeginInvokeOnMainThread(async () => await UpdateVaultButtonTitleAsync());
}
});
await UpdateVaultButtonTitleAsync();
if (await _keyConnectorService.UserNeedsMigration())
{
_messagingService.Send("convertAccountToKeyConnector");
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_broadcasterService.Unsubscribe(nameof(TabsPage));
}
public void ResetToVaultPage()
{
CurrentPage = _groupingsPage;
@@ -131,5 +150,19 @@ namespace Bit.App.Pages
groupingsPage.HideAccountSwitchingOverlayAsync().FireAndForget();
}
}
private async Task UpdateVaultButtonTitleAsync()
{
try
{
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
var isShowingVaultFilter = await policyService.ShouldShowVaultFilterAsync();
_groupingsPage.Title = isShowingVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
}
}
}

View File

@@ -162,6 +162,7 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"

View File

@@ -30,6 +30,7 @@ namespace Bit.App.Pages
CipherType? type = null,
string folderId = null,
string collectionId = null,
string organizationId = null,
string name = null,
string uri = null,
bool fromAutofill = false,
@@ -51,6 +52,7 @@ namespace Bit.App.Pages
_vm.CipherId = cipherId;
_vm.FolderId = folderId == "none" ? null : folderId;
_vm.CollectionIds = collectionId != null ? new HashSet<string>(new List<string> { collectionId }) : null;
_vm.OrganizationId = organizationId;
_vm.Type = type;
_vm.DefaultName = name ?? appOptions?.SaveName;
_vm.DefaultUri = uri ?? appOptions?.Uri;

View File

@@ -249,7 +249,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
public bool ShowCardNumber
@@ -298,6 +299,7 @@ namespace Bit.App.Pages
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
public bool AllowPersonal { get; set; }
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public void Init()
{

View File

@@ -85,7 +85,8 @@
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform" />
StyleClass="list, list-platform"
ExtraDataForLogging="Autofill Ciphers Page" />
</StackLayout>
</ResourceDictionary>
</ContentPage.Resources>

View File

@@ -33,7 +33,9 @@
Text="&#xe5c4;"
VerticalOptions="CenterAndExpand"
Clicked="BackButton_Clicked"
x:Name="_backButton" />
x:Name="_backButton"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n TapToGoBack}"/>
<controls:ExtendedSearchBar
x:Name="_searchBar"
HorizontalOptions="FillAndExpand"
@@ -47,6 +49,31 @@
</ContentPage.Resources>
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
<StackLayout
IsVisible="{Binding ShowVaultFilter}"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
Margin="0,5,0,0">
<Label
Text="{Binding VaultFilterDescription}"
LineBreakMode="TailTruncation"
Margin="10,0"
StyleClass="text-md, text-muted"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
<controls:MiButton
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
StyleClass="list-row-button-text, list-row-button-platform"
Command="{Binding VaultFilterCommand}"
VerticalOptions="Center"
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<controls:IconLabel IsVisible="{Binding ShowSearchDirection}"
Text="{Binding Source={x:Static core:BitwardenIcons.Search}}"
StyleClass="text-muted"
@@ -66,7 +93,8 @@
VerticalOptions="FillAndExpand"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform">
StyleClass="list, list-platform"
ExtraDataForLogging="Ciphers Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:CipherView">
<controls:CipherViewCell

View File

@@ -17,7 +17,8 @@ namespace Bit.App.Pages
private CiphersPageViewModel _vm;
private bool _hasFocused;
public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string autofillUrl = null, bool deleted = false)
public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string vaultFilterSelection = null,
string autofillUrl = null, bool deleted = false)
{
InitializeComponent();
_vm = BindingContext as CiphersPageViewModel;
@@ -33,6 +34,7 @@ namespace Bit.App.Pages
{
_vm.PageTitle = AppResources.SearchVault;
}
_vm.VaultFilterDescription = vaultFilterSelection;
if (Device.RuntimePlatform == Device.iOS)
{

View File

@@ -5,17 +5,17 @@ using System.Threading;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class CiphersPageViewModel : BaseViewModel
public class CiphersPageViewModel : VaultFilterViewModel
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ICipherService _cipherService;
@@ -23,7 +23,10 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private CancellationTokenSource _searchCancellationTokenSource;
private readonly ILogger _logger;
private bool _showNoData;
private bool _showList;
@@ -37,6 +40,9 @@ namespace Bit.App.Pages
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Ciphers = new ExtendedObservableCollection<CipherView>();
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
@@ -48,6 +54,11 @@ namespace Bit.App.Pages
public string AutofillUrl { get; set; }
public bool Deleted { get; set; }
protected override ICipherService cipherService => _cipherService;
protected override IPolicyService policyService => _policyService;
protected override IOrganizationService organizationService => _organizationService;
protected override ILogger logger => _logger;
public bool ShowNoData
{
get => _showNoData;
@@ -76,11 +87,9 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
await InitVaultFilterAsync(true);
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
{
Search((Page as CiphersPage).SearchBar.Text, 200);
}
PerformSearchIfPopulated();
}
public void Search(string searchText, int? timeout = null)
@@ -107,8 +116,9 @@ namespace Bit.App.Pages
}
try
{
var vaultFilteredCiphers = await GetAllCiphersAsync();
ciphers = await _searchService.SearchCiphersAsync(searchText,
Filter ?? (c => c.IsDeleted == Deleted), null, cts.Token);
Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
cts.Token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
@@ -192,6 +202,19 @@ namespace Bit.App.Pages
}
}
private void PerformSearchIfPopulated()
{
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
{
Search((Page as CiphersPage).SearchBar.Text, 200);
}
}
protected override async Task OnVaultFilterSelectedAsync()
{
PerformSearchIfPopulated();
}
private async void CipherOptionsAsync(CipherView cipher)
{
if ((Page as BaseContentPage).DoOnce())

View File

@@ -6,6 +6,7 @@
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:GroupingsPageViewModel"
Title="{Binding PageTitle}"
x:Name="_page">
@@ -106,6 +107,30 @@
GroupTemplate="{StaticResource groupTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
<StackLayout
IsVisible="{Binding ShowVaultFilter}"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
Margin="0,5,0,0">
<Label
Text="{Binding VaultFilterDescription}"
LineBreakMode="TailTruncation"
Margin="10,0"
StyleClass="text-md, text-muted"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
<controls:MiButton
Text="{Binding Source={x:Static core:BitwardenIcons.ViewCellMenu}}"
StyleClass="list-row-button-text, list-row-button-platform"
Command="{Binding VaultFilterCommand}"
VerticalOptions="Center"
HorizontalOptions="End"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
</StackLayout>
<StackLayout
VerticalOptions="CenterAndExpand"
Padding="20, 0"
@@ -130,7 +155,8 @@
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform" />
StyleClass="list, list-platform"
ExtraDataForLogging="Groupings Page" />
</RefreshView>
</StackLayout>
</ResourceDictionary>

View File

@@ -27,8 +27,8 @@ namespace Bit.App.Pages
private PreviousPageInfo _previousPage;
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null,
bool deleted = false)
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
PreviousPageInfo previousPage = null, bool deleted = false)
{
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent();
@@ -52,6 +52,10 @@ namespace Bit.App.Pages
{
_vm.PageTitle = pageTitle;
}
if (vaultFilterSelection != null)
{
_vm.VaultFilterDescription = vaultFilterSelection;
}
if (Device.RuntimePlatform == Device.iOS)
{
@@ -259,7 +263,7 @@ namespace Bit.App.Pages
}
if (!_vm.Deleted && DoOnce())
{
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId);
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
await Navigation.PushModalAsync(new NavigationPage(page));
}
}

View File

@@ -16,7 +16,7 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public class GroupingsPageViewModel : BaseViewModel
public class GroupingsPageViewModel : VaultFilterViewModel
{
private const int NoFolderListSize = 100;
@@ -46,6 +46,8 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly ILogger _logger;
public GroupingsPageViewModel()
@@ -60,10 +62,11 @@ namespace Bit.App.Pages
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Loading = true;
PageTitle = AppResources.MyVault;
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
RefreshCommand = new Command(async () =>
{
@@ -87,8 +90,9 @@ namespace Bit.App.Pages
public bool HasCiphers { get; set; }
public bool HasFolders { get; set; }
public bool HasCollections { get; set; }
public bool ShowNoFolderCiphers => (NoFolderCiphers?.Count ?? int.MaxValue) < NoFolderListSize &&
(!Collections?.Any() ?? true);
public bool ShowNoFolderCipherGroup => NoFolderCiphers != null
&& NoFolderCiphers.Count < NoFolderListSize
&& (Collections is null || !Collections.Any());
public List<CipherView> Ciphers { get; set; }
public List<CipherView> FavoriteCiphers { get; set; }
public List<CipherView> NoFolderCiphers { get; set; }
@@ -97,6 +101,11 @@ namespace Bit.App.Pages
public List<Core.Models.View.CollectionView> Collections { get; set; }
public List<TreeNode<Core.Models.View.CollectionView>> NestedCollections { get; set; }
protected override ICipherService cipherService => _cipherService;
protected override IPolicyService policyService => _policyService;
protected override IOrganizationService organizationService => _organizationService;
protected override ILogger logger => _logger;
public bool Refreshing
{
get => _refreshing;
@@ -172,6 +181,12 @@ namespace Bit.App.Pages
return;
}
await InitVaultFilterAsync(MainPage);
if (MainPage)
{
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
@@ -185,9 +200,9 @@ namespace Bit.App.Pages
try
{
await LoadDataAsync();
if (ShowNoFolderCiphers && (NestedFolders?.Any() ?? false))
if (ShowNoFolderCipherGroup && (NestedFolders?.Any() ?? false))
{
// Remove "No Folder" from folder listing
// Remove "No Folder" folder from folders group
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
}
@@ -262,7 +277,7 @@ namespace Bit.App.Pages
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
}
if (ShowNoFolderCiphers)
if (ShowNoFolderCipherGroup)
{
var noFolderCiphersListItems = NoFolderCiphers.Select(
c => new GroupingsPageListItem { Cipher = c }).ToList();
@@ -354,6 +369,11 @@ namespace Bit.App.Pages
SyncRefreshing = false;
}
protected override async Task OnVaultFilterSelectedAsync()
{
await LoadAsync();
}
public async Task SelectCipherAsync(CipherView cipher)
{
var page = new ViewPage(cipher.Id);
@@ -380,25 +400,26 @@ namespace Bit.App.Pages
default:
break;
}
var page = new GroupingsPage(false, type, null, null, title);
var page = new GroupingsPage(false, type, null, null, title, _vaultFilterSelection);
await Page.Navigation.PushAsync(page);
}
public async Task SelectFolderAsync(FolderView folder)
{
var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name);
var page = new GroupingsPage(false, null, folder.Id ?? "none", null, folder.Name, _vaultFilterSelection);
await Page.Navigation.PushAsync(page);
}
public async Task SelectCollectionAsync(Core.Models.View.CollectionView collection)
{
var page = new GroupingsPage(false, null, null, collection.Id, collection.Name);
var page = new GroupingsPage(false, null, null, collection.Id, collection.Name, _vaultFilterSelection);
await Page.Navigation.PushAsync(page);
}
public async Task SelectTrashAsync()
{
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, null, true);
var page = new GroupingsPage(false, null, null, null, AppResources.Trash, _vaultFilterSelection, null,
true);
await Page.Navigation.PushAsync(page);
}
@@ -437,7 +458,7 @@ namespace Bit.App.Pages
private async Task LoadDataAsync()
{
NoDataText = AppResources.NoItems;
_allCiphers = await _cipherService.GetAllDecryptedAsync();
_allCiphers = await GetAllCiphersAsync();
HasCiphers = _allCiphers.Any();
FavoriteCiphers?.Clear();
NoFolderCiphers?.Clear();
@@ -451,12 +472,11 @@ namespace Bit.App.Pages
if (MainPage)
{
Folders = await _folderService.GetAllDecryptedAsync();
NestedFolders = await _folderService.GetAllNestedAsync();
await FillFoldersAndCollectionsAsync();
NestedFolders = await _folderService.GetAllNestedAsync(Folders);
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
Collections = await _collectionService.GetAllDecryptedAsync();
NestedCollections = await _collectionService.GetAllNestedAsync(Collections);
HasCollections = NestedCollections.Any();
NestedCollections = Collections != null ? await _collectionService.GetAllNestedAsync(Collections) : null;
HasCollections = NestedCollections?.Any() ?? false;
}
else
{
@@ -576,6 +596,34 @@ namespace Bit.App.Pages
}
}
private async Task FillFoldersAndCollectionsAsync()
{
var orgId = GetVaultFilterOrgId();
var decFolders = await _folderService.GetAllDecryptedAsync();
var decCollections = await _collectionService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
{
Folders = BuildFolders(decFolders);
Collections = null;
}
else if (IsVaultFilterOrgVault && !string.IsNullOrWhiteSpace(orgId))
{
Folders = BuildFolders(decFolders);
Collections = decCollections?.Where(c => c.OrganizationId == orgId).ToList();
}
else
{
Folders = decFolders;
Collections = decCollections;
}
}
private List<FolderView> BuildFolders(List<FolderView> decFolders)
{
var folders = decFolders.Where(f => _allCiphers.Any(c => c.FolderId == f.Id)).ToList();
return folders.Any() ? folders : null;
}
private async void CipherOptionsAsync(CipherView cipher)
{
if ((Page as BaseContentPage).DoOnce())

View File

@@ -39,7 +39,8 @@
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding History}"
VerticalOptions="FillAndExpand"
StyleClass="list, list-platform">
StyleClass="list, list-platform"
ExtraDataForLogging="Password History Page">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="views:PasswordHistoryView">
<Grid

View File

@@ -41,15 +41,17 @@ namespace Bit.App.Pages
{
var cipher = await _cipherService.GetAsync(CipherId);
var decCipher = await cipher.DecryptAsync();
History.ResetWithRange(decCipher.PasswordHistory ?? new List<PasswordHistoryView>());
ShowNoData = History.Count == 0;
Device.BeginInvokeOnMainThread(() =>
{
History.ResetWithRange(decCipher.PasswordHistory ?? new List<PasswordHistoryView>());
ShowNoData = History.Count == 0;
});
}
private async void CopyAsync(PasswordHistoryView ph)
{
await _clipboardService.CopyTextAsync(ph.Password);
_platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}
}
}

View File

@@ -45,6 +45,8 @@ namespace Bit.App.Pages
_autofocusCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var autofocusCts = _autofocusCts;
// this task is needed to be awaited OnDisappearing to avoid some crashes
// when changing the value of _zxing.IsScanning
_continuousAutofocusTask = Task.Run(async () =>
{
try
@@ -52,7 +54,7 @@ namespace Bit.App.Pages
while (!autofocusCts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(2), autofocusCts.Token);
Device.BeginInvokeOnMainThread(() =>
await Device.InvokeOnMainThreadAsync(() =>
{
if (!autofocusCts.IsCancellationRequested)
{
@@ -73,7 +75,10 @@ namespace Bit.App.Pages
{
_autofocusCts?.Cancel();
await _continuousAutofocusTask;
if (_continuousAutofocusTask != null)
{
await _continuousAutofocusTask;
}
_zxing.IsScanning = false;
base.OnDisappearing();

View File

@@ -145,6 +145,7 @@
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
IsVisible="{Binding Cipher.ViewPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"

View File

@@ -120,7 +120,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new string[]
{
nameof(ShowPasswordIcon)
nameof(ShowPasswordIcon),
nameof(PasswordVisibilityAccessibilityText)
});
}
public bool ShowCardNumber
@@ -213,6 +214,7 @@ namespace Bit.App.Pages
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
@@ -661,7 +663,7 @@ namespace Bit.App.Pages
await _clipboardService.CopyTextAsync(text);
if (!string.IsNullOrWhiteSpace(name))
{
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name));
_platformUtilsService.ShowToastForCopiedValue(name);
}
if (id == "LoginPassword")
{
@@ -754,12 +756,12 @@ namespace Bit.App.Pages
{
if (IsBooleanType)
{
return _field.Value == "true" ? "" : "";
return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare;
}
else if (IsLinkedType)
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return " " + _i18nService.T(i18nKey);
return BitwardenIcons.Link + _i18nService.T(i18nKey);
}
else
{

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public abstract class VaultFilterViewModel : BaseViewModel
{
protected abstract ICipherService cipherService { get; }
protected abstract IPolicyService policyService { get; }
protected abstract IOrganizationService organizationService { get; }
protected abstract ILogger logger { get; }
protected bool _showVaultFilter;
protected bool _personalOwnershipPolicyApplies;
protected string _vaultFilterSelection;
protected List<Organization> _organizations;
public VaultFilterViewModel()
{
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => logger.Exception(ex),
allowsMultipleExecutions: false);
}
public ICommand VaultFilterCommand { get; set; }
public bool ShowVaultFilter
{
get => _showVaultFilter;
set => SetProperty(ref _showVaultFilter, value);
}
public string VaultFilterDescription
{
get
{
if (_vaultFilterSelection == null || _vaultFilterSelection == AppResources.AllVaults)
{
return string.Format(AppResources.VaultFilterDescription, AppResources.All);
}
return string.Format(AppResources.VaultFilterDescription, _vaultFilterSelection);
}
set => SetProperty(ref _vaultFilterSelection, value);
}
public string GetVaultFilterOrgId()
{
return _organizations?.FirstOrDefault(o => o.Name == _vaultFilterSelection)?.Id;
}
protected bool IsVaultFilterMyVault => _vaultFilterSelection == AppResources.MyVault;
protected bool IsVaultFilterOrgVault => _vaultFilterSelection != AppResources.AllVaults &&
_vaultFilterSelection != AppResources.MyVault;
protected async Task InitVaultFilterAsync(bool shouldUpdateShowVaultFilter)
{
_organizations = await organizationService.GetAllAsync();
if (_organizations?.Any() ?? false)
{
_personalOwnershipPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.PersonalOwnership);
var singleOrgPolicyApplies = await policyService.PolicyAppliesToUser(PolicyType.OnlyOrg);
if (_vaultFilterSelection == null || (_personalOwnershipPolicyApplies && singleOrgPolicyApplies))
{
VaultFilterDescription = AppResources.AllVaults;
}
}
if (shouldUpdateShowVaultFilter)
{
await Task.Delay(100);
ShowVaultFilter = await policyService.ShouldShowVaultFilterAsync();
}
}
protected async Task<List<CipherView>> GetAllCiphersAsync()
{
var decCiphers = await cipherService.GetAllDecryptedAsync();
if (IsVaultFilterMyVault)
{
return decCiphers.Where(c => c.OrganizationId == null).ToList();
}
if (IsVaultFilterOrgVault)
{
var orgId = GetVaultFilterOrgId();
return decCiphers.Where(c => c.OrganizationId == orgId).ToList();
}
return decCiphers;
}
protected async Task VaultFilterOptionsAsync()
{
var options = new List<string> { AppResources.AllVaults };
if (!_personalOwnershipPolicyApplies)
{
options.Add(AppResources.MyVault);
}
if (_organizations.Any())
{
options.AddRange(_organizations.OrderBy(o => o.Name).Select(o => o.Name));
}
var selection = await Page.DisplayActionSheet(AppResources.FilterByVault, AppResources.Cancel, null,
options.ToArray());
if (selection == null || selection == AppResources.Cancel ||
(_vaultFilterSelection == null && selection == AppResources.AllVaults) ||
(_vaultFilterSelection != null && _vaultFilterSelection == selection))
{
return;
}
VaultFilterDescription = selection;
await OnVaultFilterSelectedAsync();
}
protected abstract Task OnVaultFilterSelectedAsync();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1861,6 +1861,9 @@
<value>A friendly name to describe this Send.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Text" xml:space="preserve">
<value>Text</value>
</data>
<data name="TypeText" xml:space="preserve">
<value>Text</value>
</data>
@@ -1877,6 +1880,18 @@
<data name="TypeFileInfo" xml:space="preserve">
<value>The file you want to send.</value>
</data>
<data name="FileTypeIsSelected" xml:space="preserve">
<value>File type is selected.</value>
</data>
<data name="FileTypeIsNotSelected" xml:space="preserve">
<value>File type is not selected, tap to select.</value>
</data>
<data name="TextTypeIsSelected" xml:space="preserve">
<value>Text type is selected.</value>
</data>
<data name="TextTypeIsNotSelected" xml:space="preserve">
<value>Text type is not selected, tap to select.</value>
</data>
<data name="DeletionDate" xml:space="preserve">
<value>Deletion Date</value>
</data>
@@ -2181,4 +2196,52 @@
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
<value>Enter the verification code that was sent to your email</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Submit crash logs</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Options are expanded, tap to collapse.</value>
</data>
<data name="OptionsCollapsed" xml:space="preserve">
<value>Options are collapsed, tap to expand.</value>
</data>
<data name="UppercaseAtoZ" xml:space="preserve">
<value>Uppercase (A to Z)</value>
</data>
<data name="LowercaseAtoZ" xml:space="preserve">
<value>Lowercase (A to Z)</value>
</data>
<data name="NumbersZeroToNine" xml:space="preserve">
<value>Numbers (0 to 9)</value>
</data>
<data name="SpecialCharacters" xml:space="preserve">
<value>Special Characters (!@#$%^&amp;*)</value>
</data>
<data name="TapToGoBack" xml:space="preserve">
<value>Tap to go back</value>
</data>
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
<value>Password is visible, tap to hide.</value>
</data>
<data name="PasswordIsNotVisibleTapToShow" xml:space="preserve">
<value>Password is not visible, tap to show.</value>
</data>
<data name="FilterByVault" xml:space="preserve">
<value>Filter items by vault</value>
</data>
<data name="AllVaults" xml:space="preserve">
<value>All Vaults</value>
</data>
<data name="Vaults" xml:space="preserve">
<value>Vaults</value>
</data>
<data name="VaultFilterDescription" xml:space="preserve">
<value>Vault: {0}</value>
</data>
<data name="All" xml:space="preserve">
<value>All</value>
</data>
</root>

View File

@@ -20,6 +20,7 @@ namespace Bit.App.Services
private const int DialogPromiseExpiration = 600000; // 10 minutes
private readonly IDeviceActionService _deviceActionService;
private readonly IClipboardService _clipboardService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
@@ -28,10 +29,12 @@ namespace Bit.App.Services
public MobilePlatformUtilsService(
IDeviceActionService deviceActionService,
IClipboardService clipboardService,
IMessagingService messagingService,
IBroadcasterService broadcasterService)
{
_deviceActionService = deviceActionService;
_clipboardService = clipboardService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
}
@@ -129,6 +132,15 @@ namespace Bit.App.Services
return true;
}
public void ShowToastForCopiedValue(string valueNameCopied)
{
if (!_clipboardService.IsCopyNotificationHandledByPlatform())
{
ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, valueNameCopied));
}
}
public bool SupportsFido2()
{
return _deviceActionService.SupportsFido2();

View File

@@ -25,6 +25,7 @@ namespace Bit.App.Services
Constants.LastBuildKey,
Constants.ClearCiphersCacheKey,
Constants.BiometricIntegrityKey,
Constants.iOSExtensionActiveUserIdKey,
Constants.iOSAutoFillClearCiphersCacheKey,
Constants.iOSAutoFillBiometricIntegrityKey,
Constants.iOSExtensionClearCiphersCacheKey,
@@ -32,7 +33,7 @@ namespace Bit.App.Services
Constants.iOSShareExtensionClearCiphersCacheKey,
Constants.iOSShareExtensionBiometricIntegrityKey,
Constants.RememberedEmailKey,
Constants.RememberedOrgIdentifierKey,
Constants.RememberedOrgIdentifierKey
};
public MobileStorageService(

View File

@@ -268,6 +268,16 @@
<Setter Property="TextColor"
Value="{DynamicResource ButtonColor}" />
</Style>
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="list-row-button-text">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="Padding"
Value="0" />
<Setter Property="TextColor"
Value="{DynamicResource ButtonTextColor}" />
</Style>
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="segmented-button">

View File

@@ -0,0 +1,217 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Utilities.AccountManagement
{
public class AccountsManager : IAccountsManager
{
private readonly IBroadcasterService _broadcasterService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IStorageService _secureStorageService;
private readonly IStateService _stateService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
Func<AppOptions> _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost;
public AccountsManager(IBroadcasterService broadcasterService,
IVaultTimeoutService vaultTimeoutService,
IStorageService secureStorageService,
IStateService stateService,
IPlatformUtilsService platformUtilsService,
IAuthService authService)
{
_broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService;
_secureStorageService = secureStorageService;
_stateService = stateService;
_platformUtilsService = platformUtilsService;
_authService = authService;
}
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
public void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost)
{
_getOptionsFunc = getOptionsFunc;
_accountsManagerHost = accountsManagerHost;
_broadcasterService.Subscribe(nameof(AccountsManager), OnMessage);
}
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
{
// TODO: this could be improved by doing chain of responsability pattern
// but for now it may be an overkill, if logic gets more complex consider refactoring it
var authed = isAuthed ?? 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;
_accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email));
}
else if (await _vaultTimeoutService.IsLockedAsync() ||
await _vaultTimeoutService.ShouldLockAsync())
{
_accountsManagerHost.Navigate(NavigationTarget.Lock);
}
else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
{
_accountsManagerHost.Navigate(NavigationTarget.AddEditCipher);
}
else if (Options.Uri != null)
{
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
}
else if (Options.CreateSend != null)
{
_accountsManagerHost.Navigate(NavigationTarget.SendAddEdit);
}
else
{
_accountsManagerHost.Navigate(NavigationTarget.Home);
}
}
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();
_accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email));
}
else
{
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
}
}
}
private async void OnMessage(Message message)
{
switch (message.Command)
{
case AccountsManagerMessageCommands.LOCKED:
Locked(message.Data as Tuple<string, bool>);
break;
case AccountsManagerMessageCommands.LOCK_VAULT:
await _vaultTimeoutService.LockAsync(true);
break;
case AccountsManagerMessageCommands.LOGOUT:
LogOut(message.Data as Tuple<string, bool, bool>);
break;
case AccountsManagerMessageCommands.LOGGED_OUT:
// Clean up old migrated key if they ever log out.
await _secureStorageService.RemoveAsync("oldKey");
break;
case AccountsManagerMessageCommands.ADD_ACCOUNT:
AddAccount();
break;
case AccountsManagerMessageCommands.ACCOUNT_ADDED:
await _accountsManagerHost.UpdateThemeAsync();
break;
case AccountsManagerMessageCommands.SWITCHED_ACCOUNT:
await SwitchedAccountAsync();
break;
}
}
private void Locked(Tuple<string, bool> extras)
{
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? false;
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
}
private async Task LockedAsync(string userId, bool userInitiated)
{
if (!await _stateService.IsActiveAccountAsync(userId))
{
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
return;
}
var autoPromptBiometric = !userInitiated;
if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
{
var vaultTimeout = await _stateService.GetVaultTimeoutAsync();
if (vaultTimeout == 0)
{
autoPromptBiometric = false;
}
}
await _accountsManagerHost.SetPreviousPageInfoAsync();
Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric)));
}
private void AddAccount()
{
Device.BeginInvokeOnMainThread(() =>
{
Options.HideAccountSwitcher = false;
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
});
}
private void LogOut(Tuple<string, bool, bool> extras)
{
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false;
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
}
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{
await AppHelpers.LogOutAsync(userId, userInitiated);
await NavigateOnAccountChangeAsync();
_authService.LogOut(() =>
{
if (expired)
{
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
}
});
}
private async Task SwitchedAccountAsync()
{
await AppHelpers.OnAccountSwitchAsync();
await Device.InvokeOnMainThreadAsync(async () =>
{
if (await _vaultTimeoutService.ShouldTimeoutAsync())
{
await _vaultTimeoutService.ExecuteTimeoutActionAsync();
}
else
{
await NavigateOnAccountChangeAsync();
}
await Task.Delay(50);
await _accountsManagerHost.UpdateThemeAsync();
});
}
}
}

View File

@@ -0,0 +1,14 @@
using Bit.App.Abstractions;
namespace Bit.App.Utilities.AccountManagement
{
public class LockNavigationParams : INavigationParams
{
public LockNavigationParams(bool autoPromptBiometric = true)
{
AutoPromptBiometric = autoPromptBiometric;
}
public bool AutoPromptBiometric { get; }
}
}

View File

@@ -0,0 +1,14 @@
using Bit.App.Abstractions;
namespace Bit.App.Utilities.AccountManagement
{
public class LoginNavigationParams : INavigationParams
{
public LoginNavigationParams(string email)
{
Email = email;
}
public string Email { get; }
}
}

View File

@@ -97,16 +97,14 @@ namespace Bit.App.Utilities
else if (selection == AppResources.CopyUsername)
{
await clipboardService.CopyTextAsync(cipher.Login.Username);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.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.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
}
}
@@ -119,8 +117,7 @@ namespace Bit.App.Utilities
if (!string.IsNullOrWhiteSpace(totp))
{
await clipboardService.CopyTextAsync(totp);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
}
@@ -133,8 +130,7 @@ namespace Bit.App.Utilities
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Number);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Number));
platformUtilsService.ShowToastForCopiedValue(AppResources.Number);
}
}
else if (selection == AppResources.CopySecurityCode)
@@ -142,16 +138,14 @@ namespace Bit.App.Utilities
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Code);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode));
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.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Notes));
platformUtilsService.ShowToastForCopiedValue(AppResources.Notes);
}
return selection;
}
@@ -262,8 +256,7 @@ namespace Bit.App.Utilities
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
await clipboardService.CopyTextAsync(GetSendUrl(send));
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SendLink));
platformUtilsService.ShowToastForCopiedValue(AppResources.SendLink);
}
public static async Task ShareSendUrlAsync(SendView send)

View File

@@ -44,7 +44,7 @@ namespace Bit.Core.Abstractions
Task PutDeleteCipherAsync(string id);
Task<CipherResponse> PutRestoreCipherAsync(string id);
Task RefreshIdentityTokenAsync();
Task<object> PreValidateSso(string identifier);
Task<SsoPrevalidateResponse> PreValidateSso(string identifier);
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
TRequest body, bool authed, bool hasResponse, bool logoutOnUnauthorized = true);
void SetUrls(EnvironmentUrls urls);

View File

@@ -8,9 +8,17 @@ namespace Bit.Core.Abstractions
/// Copies the <paramref name="text"/> to the Clipboard.
/// If <paramref name="expiresInMs"/> is set > 0 then the Clipboard will be cleared after this time in milliseconds.
/// if less than 0 then it takes the configuration that the user set in Options.
/// If <paramref name="isSensitive"/> is true the sensitive flag is passed to the clipdata to obfuscate the
/// clipboard text in the popup (Android 13+ only)
/// </summary>
/// <param name="text">Text to be copied to the Clipboard</param>
/// <param name="expiresInMs">Expiration time in milliseconds of the copied text</param>
Task CopyTextAsync(string text, int expiresInMs = -1);
/// <param name="isSensitive">Flag to mark copied text as sensitive</param>
Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true);
/// <summary>
/// Returns true if the platform provides its own notification when text is copied to the clipboard
/// </summary>
bool IsCopyNotificationHandledByPlatform();
}
}

View File

@@ -15,7 +15,7 @@ namespace Bit.Core.Abstractions
Task<Folder> EncryptAsync(FolderView model, SymmetricCryptoKey key = null);
Task<List<Folder>> GetAllAsync();
Task<List<FolderView>> GetAllDecryptedAsync();
Task<List<TreeNode<FolderView>>> GetAllNestedAsync();
Task<List<TreeNode<FolderView>>> GetAllNestedAsync(List<FolderView> folders = null);
Task<Folder> GetAsync(string id);
Task<TreeNode<FolderView>> GetNestedAsync(string id);
Task ReplaceAsync(Dictionary<string, FolderData> folders);

View File

@@ -1,11 +1,29 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Bit.Core.Abstractions
{
public interface ILogger
{
/// <summary>
/// Place necessary code to initiaze logger
/// </summary>
/// <returns></returns>
Task InitAsync();
/// <summary>
/// Returns if the current logger is enable or disable.
/// </summary>
/// <returns></returns>
Task<bool> IsEnabled();
/// <summary>
/// Changes the state of the current logger. Setting state enabled to false will block logging.
/// </summary>
Task SetEnabled(bool value);
/// <summary>
/// Logs something that is not in itself an exception, e.g. a wrong flow or value that needs to be reported
/// and looked into.

View File

@@ -23,6 +23,7 @@ namespace Bit.Core.Abstractions
Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> validator);
void ShowToast(string type, string title, string text, Dictionary<string, object> options = null);
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
void ShowToastForCopiedValue(string valueNameCopied);
bool SupportsFido2();
bool SupportsDuo();
Task<bool> SupportsBiometricAsync();

View File

@@ -20,5 +20,6 @@ namespace Bit.Core.Abstractions
string orgId);
Task<bool> PolicyAppliesToUser(PolicyType policyType, Func<Policy, bool> policyFilter = null, string userId = null);
int? GetPolicyInt(Policy policy, string key);
Task<bool> ShouldShowVaultFilterAsync();
}
}

View File

@@ -14,6 +14,7 @@ namespace Bit.Core.Abstractions
Task<string> GetActiveUserIdAsync();
Task<bool> IsActiveAccountAsync(string userId = null);
Task SetActiveUserAsync(string userId);
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();
Task<bool> IsAuthenticatedAsync(string userId = null);
Task<string> GetUserIdAsync(string email);
Task RefreshAccountViewsAsync(bool allowAddAccountRow);
@@ -145,5 +146,6 @@ namespace Bit.Core.Abstractions
Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null);
Task<string> GetTwoFactorTokenAsync(string email = null);
Task SetTwoFactorTokenAsync(string value, string email = null);
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
}
}

View File

@@ -111,5 +111,6 @@
public const string EyeSlash = "\xe96d";
public const string File = "\xe96e";
public const string Paste = "\xe96f";
public const string ViewCellMenu = "\xe5d3";
}
}

View File

@@ -25,6 +25,7 @@
public static string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState";
public static string iOSShareExtensionClearCiphersCacheKey = "iOSShareExtensionClearCiphersCache";
public static string iOSShareExtensionBiometricIntegrityKey = "iOSShareExtensionBiometricIntegrityState";
public static string iOSExtensionActiveUserIdKey = "iOSExtensionActiveUserId";
public static string EventCollectionKey = "eventCollection";
public static string RememberedEmailKey = "rememberedEmail";
public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier";

View File

@@ -0,0 +1,13 @@
namespace Bit.Core.Enums
{
public enum NavigationTarget
{
HomeLogin,
Login,
Lock,
Home,
AddEditCipher,
AutofillCiphers,
SendAddEdit
}
}

View File

@@ -9,7 +9,7 @@
if (defaultOptions)
{
Length = 14;
Ambiguous = false;
AllowAmbiguousChar = true;
Number = true;
MinNumber = 1;
Uppercase = true;
@@ -27,7 +27,7 @@
}
public int? Length { get; set; }
public bool? Ambiguous { get; set; }
public bool? AllowAmbiguousChar { get; set; }
public bool? Number { get; set; }
public int? MinNumber { get; set; }
public bool? Uppercase { get; set; }
@@ -45,7 +45,7 @@
public void Merge(PasswordGenerationOptions defaults)
{
Length = Length ?? defaults.Length;
Ambiguous = Ambiguous ?? defaults.Ambiguous;
AllowAmbiguousChar = AllowAmbiguousChar ?? defaults.AllowAmbiguousChar;
Number = Number ?? defaults.Number;
MinNumber = MinNumber ?? defaults.MinNumber;
Uppercase = Uppercase ?? defaults.Uppercase;

View File

@@ -2,6 +2,7 @@
{
public class OrganizationUserResetPasswordEnrollmentRequest
{
public string MasterPasswordHash { get; set; }
public string ResetPasswordKey { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Models.Response
{
public class SsoPrevalidateResponse
{
public string Token { get; set; }
}
}

View File

@@ -547,7 +547,7 @@ namespace Bit.Core.Services
return accessToken;
}
public async Task<object> PreValidateSso(string identifier)
public async Task<SsoPrevalidateResponse> PreValidateSso(string identifier)
{
var path = "/account/prevalidate?domainHint=" + WebUtility.UrlEncode(identifier);
using (var requestMessage = new HttpRequestMessage())
@@ -571,7 +571,8 @@ namespace Bit.Core.Services
var error = await HandleErrorAsync(response, false, true);
throw new ApiException(error);
}
return null;
var responseJsonString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<SsoPrevalidateResponse>(responseJsonString);
}
}

View File

@@ -346,6 +346,7 @@ namespace Bit.Core.Services
TwoFactorProvidersData = response.TwoFactorResponse.TwoFactorProviders2;
result.TwoFactorProviders = response.TwoFactorResponse.TwoFactorProviders2;
CaptchaToken = response.TwoFactorResponse.CaptchaToken;
await _tokenService.ClearTwoFactorTokenAsync(email);
return result;
}

View File

@@ -107,9 +107,12 @@ namespace Bit.Core.Services
return _decryptedFolderCache;
}
public async Task<List<TreeNode<FolderView>>> GetAllNestedAsync()
public async Task<List<TreeNode<FolderView>>> GetAllNestedAsync(List<FolderView> folders = null)
{
var folders = await GetAllDecryptedAsync();
if (folders == null)
{
folders = await GetAllDecryptedAsync();
}
var nodes = new List<TreeNode<FolderView>>();
foreach (var f in folders)
{

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
namespace Bit.Core.Services
@@ -45,6 +46,12 @@ namespace Bit.Core.Services
}
public void Exception(Exception ex) => Debug.WriteLine(ex);
public Task InitAsync() => Task.CompletedTask;
public Task<bool> IsEnabled() => Task.FromResult(true);
public Task SetEnabled(bool value) => Task.CompletedTask;
}
}
#endif

View File

@@ -5,13 +5,24 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Microsoft.AppCenter;
using Microsoft.AppCenter.Crashes;
using Newtonsoft.Json;
namespace Bit.Core.Services
{
public class Logger : ILogger
{
private const string iOSAppSecret = "51f96ae5-68ba-45f6-99a1-8ad9f63046c3";
private const string DroidAppSecret = "d3834185-b4a6-4347-9047-b86c65293d42";
private string _userId;
private string _appId;
private bool _isInitialised = false;
static ILogger _instance;
public static ILogger Instance
{
@@ -29,6 +40,60 @@ namespace Bit.Core.Services
{
}
public string Description
{
get
{
return JsonConvert.SerializeObject(new
{
AppId = _appId,
UserId = _userId
}, Formatting.Indented);
}
}
public async Task InitAsync()
{
if (_isInitialised)
{
return;
}
var device = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService").GetDevice();
_userId = await ServiceContainer.Resolve<IStateService>("stateService").GetActiveUserIdAsync();
_appId = await ServiceContainer.Resolve<IAppIdService>("appIdService").GetAppIdAsync();
switch (device)
{
case Enums.DeviceType.Android:
AppCenter.Start(DroidAppSecret, typeof(Crashes));
break;
case Enums.DeviceType.iOS:
AppCenter.Start(iOSAppSecret, typeof(Crashes));
break;
default:
throw new AppCenterException("Cannot start AppCenter. Device type is not configured.");
}
AppCenter.SetUserId(_userId);
Crashes.GetErrorAttachments = (ErrorReport report) =>
{
return new ErrorAttachmentLog[]
{
ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"),
};
};
_isInitialised = true;
}
public async Task<bool> IsEnabled() => await AppCenter.IsEnabledAsync();
public async Task SetEnabled(bool value) => await AppCenter.SetEnabledAsync(value);
public void Error(string message,
IDictionary<string, string> extraData = null,
[CallerMemberName] string memberName = "",

View File

@@ -1,9 +1,6 @@
using System;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
#if !FDROID
using Microsoft.AppCenter.Crashes;
#endif
namespace Bit.Core.Services
{
@@ -25,8 +22,9 @@ namespace Bit.Core.Services
#if !FDROID
// just in case the caller throws the exception in a moment where the logger can't be resolved
// we need to track the error as well
Crashes.TrackError(ex);
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
#endif
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
namespace Bit.Core.Services
@@ -17,5 +18,11 @@ namespace Bit.Core.Services
public void Exception(Exception ex)
{
}
public Task InitAsync() => Task.CompletedTask;
public Task<bool> IsEnabled() => Task.FromResult(false);
public Task SetEnabled(bool value) => Task.CompletedTask;
}
}

View File

@@ -93,7 +93,7 @@ namespace Bit.Core.Services
// Build out other character sets
var allCharSet = string.Empty;
var lowercaseCharSet = LowercaseCharSet;
if (options.Ambiguous.GetValueOrDefault())
if (options.AllowAmbiguousChar.GetValueOrDefault())
{
lowercaseCharSet = string.Concat(lowercaseCharSet, "l");
}
@@ -103,7 +103,7 @@ namespace Bit.Core.Services
}
var uppercaseCharSet = UppercaseCharSet;
if (options.Ambiguous.GetValueOrDefault())
if (options.AllowAmbiguousChar.GetValueOrDefault())
{
uppercaseCharSet = string.Concat(uppercaseCharSet, "IO");
}
@@ -113,7 +113,7 @@ namespace Bit.Core.Services
}
var numberCharSet = NumberCharSet;
if (options.Ambiguous.GetValueOrDefault())
if (options.AllowAmbiguousChar.GetValueOrDefault())
{
numberCharSet = string.Concat(numberCharSet, "01");
}

View File

@@ -193,7 +193,8 @@ namespace Bit.Core.Services
return new Tuple<ResetPasswordPolicyOptions, bool>(resetPasswordPolicyOptions, policy != null);
}
public async Task<bool> PolicyAppliesToUser(PolicyType policyType, Func<Policy, bool> policyFilter, string userId = null)
public async Task<bool> PolicyAppliesToUser(PolicyType policyType, Func<Policy, bool> policyFilter = null,
string userId = null)
{
var policies = await GetAll(policyType, userId);
if (policies == null)
@@ -246,6 +247,18 @@ namespace Bit.Core.Services
return null;
}
public async Task<bool> ShouldShowVaultFilterAsync()
{
var personalOwnershipPolicyApplies = await PolicyAppliesToUser(PolicyType.PersonalOwnership);
var singleOrgPolicyApplies = await PolicyAppliesToUser(PolicyType.OnlyOrg);
if (personalOwnershipPolicyApplies && singleOrgPolicyApplies)
{
return false;
}
var organizations = await _organizationService.GetAllAsync();
return organizations?.Any() ?? false;
}
private bool? GetPolicyBool(Policy policy, string key)
{
if (policy.Data.ContainsKey(key))

View File

@@ -74,28 +74,34 @@ namespace Bit.Core.Services
CancellationToken ct = default, bool deleted = false)
{
ct.ThrowIfCancellationRequested();
var matchedCiphers = new List<CipherView>();
var lowPriorityMatchedCiphers = new List<CipherView>();
query = query.Trim().ToLower();
return ciphers.Where(c =>
foreach (var c in ciphers)
{
ct.ThrowIfCancellationRequested();
if (c.Name?.ToLower().Contains(query) ?? false)
{
return true;
matchedCiphers.Add(c);
}
if (query.Length >= 8 && c.Id.StartsWith(query))
else if (query.Length >= 8 && c.Id.StartsWith(query))
{
return true;
lowPriorityMatchedCiphers.Add(c);
}
if (c.SubTitle?.ToLower().Contains(query) ?? false)
else if (c.SubTitle?.ToLower().Contains(query) ?? false)
{
return true;
lowPriorityMatchedCiphers.Add(c);
}
if (c.Login?.Uri?.ToLower()?.Contains(query) ?? false)
else if (c.Login?.Uri?.ToLower()?.Contains(query) ?? false)
{
return true;
lowPriorityMatchedCiphers.Add(c);
}
return false;
}).ToList();
}
ct.ThrowIfCancellationRequested();
matchedCiphers.AddRange(lowPriorityMatchedCiphers);
return matchedCiphers;
}
public async Task<List<SendView>> SearchSendsAsync(string query, Func<SendView, bool> filter = null,
@@ -133,25 +139,31 @@ namespace Bit.Core.Services
public List<SendView> SearchSendsBasic(List<SendView> sends, string query, CancellationToken ct = default,
bool deleted = false)
{
var matchedSends = new List<SendView>();
var lowPriorityMatchSends = new List<SendView>();
ct.ThrowIfCancellationRequested();
query = query.Trim().ToLower();
return sends.Where(s =>
foreach (var s in sends)
{
ct.ThrowIfCancellationRequested();
if (s.Name?.ToLower().Contains(query) ?? false)
{
return true;
matchedSends.Add(s);
}
if (s.Text?.Text?.ToLower().Contains(query) ?? false)
else if (s.Text?.Text?.ToLower().Contains(query) ?? false)
{
return true;
lowPriorityMatchSends.Add(s);
}
if (s.File?.FileName?.ToLower()?.Contains(query) ?? false)
else if (s.File?.FileName?.ToLower()?.Contains(query) ?? false)
{
return true;
lowPriorityMatchSends.Add(s);
}
return false;
}).ToList();
}
ct.ThrowIfCancellationRequested();
matchedSends.AddRange(lowPriorityMatchSends);
return matchedSends;
}
}
}

View File

@@ -15,16 +15,20 @@ namespace Bit.Core.Services
{
private readonly IStorageService _storageService;
private readonly IStorageService _secureStorageService;
private readonly IMessagingService _messagingService;
private State _state;
private bool _migrationChecked;
public List<AccountView> AccountViews { get; set; }
public StateService(IStorageService storageService, IStorageService secureStorageService)
public StateService(IStorageService storageService,
IStorageService secureStorageService,
IMessagingService messagingService)
{
_storageService = storageService;
_secureStorageService = secureStorageService;
_messagingService = messagingService;
}
public async Task<string> GetActiveUserIdAsync()
@@ -67,6 +71,28 @@ namespace Bit.Core.Services
await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync());
}
public async Task CheckExtensionActiveUserAndSwitchIfNeededAsync()
{
var extensionUserId = await GetExtensionActiveUserIdFromStorageAsync();
if (string.IsNullOrEmpty(extensionUserId))
{
return;
}
if (_state?.ActiveUserId == extensionUserId)
{
// Clear the value in case the user changes the active user from the app
// so if that happens and the user sends the app to background and comes back
// the user is not changed again.
await SaveExtensionActiveUserIdToStorageAsync(null);
return;
}
await SetActiveUserAsync(extensionUserId);
await SaveExtensionActiveUserIdToStorageAsync(null);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
public async Task<bool> IsAuthenticatedAsync(string userId = null)
{
return await GetAccessTokenAsync(userId) != null;
@@ -1372,10 +1398,6 @@ namespace Bit.Core.Services
await SetEncryptedPasswordGenerationHistoryAsync(null, userId);
await SetEncryptedSendsAsync(null, userId);
await SetSettingsAsync(null, userId);
if (!string.IsNullOrWhiteSpace(email))
{
await SetTwoFactorTokenAsync(null, email);
}
if (userInitiated)
{
@@ -1514,6 +1536,16 @@ namespace Bit.Core.Services
await _storageService.SaveAsync(Constants.StateKey, state);
}
private async Task<string> GetExtensionActiveUserIdFromStorageAsync()
{
return await _storageService.GetAsync<string>(Constants.iOSExtensionActiveUserIdKey);
}
public async Task SaveExtensionActiveUserIdToStorageAsync(string userId)
{
await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId);
}
private async Task CheckStateAsync()
{
if (!_migrationChecked)

View File

@@ -0,0 +1,13 @@
namespace Bit.Core.Utilities
{
public static class AccountsManagerMessageCommands
{
public const string LOCKED = "locked";
public const string LOCK_VAULT = "lockVault";
public const string LOGOUT = "logout";
public const string LOGGED_OUT = "loggedOut";
public const string ADD_ACCOUNT = "addAccount";
public const string ACCOUNT_ADDED = "accountAdded";
public const string SWITCHED_ACCOUNT = "switchedAccount";
}
}

Some files were not shown because too many files have changed in this diff Show More