mirror of
https://github.com/bitwarden/mobile
synced 2026-01-22 04:13:19 +00:00
Compare commits
75 Commits
aesgcm
...
fido2-goog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1461ab16b | ||
|
|
307a5a5843 | ||
|
|
d050215ebc | ||
|
|
67e26c778b | ||
|
|
efb10d155d | ||
|
|
913c673773 | ||
|
|
339decafe4 | ||
|
|
380c3583fb | ||
|
|
244a6a7f41 | ||
|
|
745b54bf2a | ||
|
|
4f86bb2043 | ||
|
|
ada8a73849 | ||
|
|
1e3204f91d | ||
|
|
b5c6a57fa0 | ||
|
|
6ca5b66aa7 | ||
|
|
24a0396d0f | ||
|
|
7f7b673b0a | ||
|
|
f79ff3fd63 | ||
|
|
2f2fa8a25b | ||
|
|
9042b1009e | ||
|
|
dbc0f490c5 | ||
|
|
6d8f627772 | ||
|
|
b4c016c9d4 | ||
|
|
ae763ebca8 | ||
|
|
880483ac79 | ||
|
|
f44e6ab75f | ||
|
|
10a718b0c7 | ||
|
|
9ec4050e4d | ||
|
|
93e2c0df7c | ||
|
|
8d07397a59 | ||
|
|
15d44b873b | ||
|
|
6a979d0ff5 | ||
|
|
a4db088eda | ||
|
|
96454b7cbf | ||
|
|
9c1df2179c | ||
|
|
52c9125404 | ||
|
|
172a857604 | ||
|
|
d8e68a266c | ||
|
|
1f57ba6c50 | ||
|
|
ff19578807 | ||
|
|
9298d57f22 | ||
|
|
8cf5d5728e | ||
|
|
bdf6d764ca | ||
|
|
05e8da4bcc | ||
|
|
382e547f74 | ||
|
|
4f9985d2b0 | ||
|
|
ef97417cd7 | ||
|
|
7bdf4d8b18 | ||
|
|
a6c95d06b5 | ||
|
|
bd4a275558 | ||
|
|
a2b46ee7cb | ||
|
|
2003ac9d2c | ||
|
|
2a5667251e | ||
|
|
79589b07fc | ||
|
|
0aed13a2cf | ||
|
|
df412e75d1 | ||
|
|
2b8dbde923 | ||
|
|
33791a03ac | ||
|
|
80a33e98a2 | ||
|
|
afed18908b | ||
|
|
fe58dea3e0 | ||
|
|
569045fcd5 | ||
|
|
fdda670311 | ||
|
|
fbb7b05b9c | ||
|
|
976eeab6d7 | ||
|
|
e61bcd2785 | ||
|
|
570edb4319 | ||
|
|
8fe8c42765 | ||
|
|
0eebe6b156 | ||
|
|
946831b37e | ||
|
|
1d4e742d66 | ||
|
|
29979f6b04 | ||
|
|
2f6e1ff477 | ||
|
|
c1030c48fa | ||
|
|
ef5d08cb75 |
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Set up cloc
|
||||
run: |
|
||||
@@ -31,22 +31,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@v1
|
||||
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help
|
||||
nuget help | grep Version
|
||||
msbuild -version
|
||||
dotnet --info
|
||||
Write-Output "GitHub ref: $env:GITHUB_REF"
|
||||
Write-Output "GitHub event: $env:GITHUB_EVENT"
|
||||
shell: pwsh
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Decrypt secrets
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
@@ -83,14 +82,14 @@ jobs:
|
||||
|
||||
- name: Upload Play Store .aab artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: ./com.x8bit.bitwarden.aab
|
||||
|
||||
- name: Upload Play Store .apk artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: ./com.x8bit.bitwarden.apk
|
||||
@@ -115,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -146,7 +145,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set up Node
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
|
||||
with:
|
||||
node-version: '10.x'
|
||||
|
||||
@@ -181,7 +180,7 @@ jobs:
|
||||
|
||||
- name: Checkout repo
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Install Node dependencies
|
||||
if: github.event_name == 'release'
|
||||
@@ -215,18 +214,17 @@ jobs:
|
||||
steps:
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help
|
||||
nuget help | grep Version
|
||||
msbuild -version
|
||||
dotnet --info
|
||||
Write-Output "GitHub ref: $env:GITHUB_REF"
|
||||
Write-Output "GitHub event: $env:GITHUB_EVENT"
|
||||
shell: pwsh
|
||||
echo "GitHub ref: $GITHUB_REF"
|
||||
echo "GitHub event: $GITHUB_EVENT"
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: ./.github/scripts/ios/decrypt-secrets.ps1
|
||||
@@ -271,7 +269,7 @@ jobs:
|
||||
|
||||
- name: Upload App Store .ipa artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700
|
||||
with:
|
||||
name: Bitwarden.ipa
|
||||
path: ./bitwarden-export/Bitwarden.ipa
|
||||
|
||||
48
.github/workflows/crowdin-sync.yml
vendored
Normal file
48
.github/workflows/crowdin-sync.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Crowdin Sync
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
#schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
crowdin_branch_name: master
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
github_user_name: "github-actions"
|
||||
github_user_email: "<>"
|
||||
commit_message: "Autosync the updated translations"
|
||||
localization_branch_name: crowdin-auto-sync
|
||||
create_pull_request: true
|
||||
pull_request_title: "Autosync Crowdin Translations"
|
||||
pull_request_body: "Autosync the updated translations"
|
||||
21
.github/workflows/release.yml
vendored
Normal file
21
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
cloc:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- name: Set up cloc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install cloc
|
||||
|
||||
- name: Print lines of code
|
||||
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
||||
@@ -1,3 +1,5 @@
|
||||
project_id_env: _CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
files:
|
||||
- source: /src/App/Resources/AppResources.resx
|
||||
translation: /src/App/Resources/AppResources.%two_letters_code%.resx
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.chrome.beta", "url_bar"),
|
||||
new Browser("com.chrome.canary", "url_bar"),
|
||||
new Browser("com.chrome.dev", "url_bar"),
|
||||
new Browser("com.cookiegames.smartcookie", "search"),
|
||||
new Browser("com.cookiejarapps.android.smartcookieweb", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("com.duckduckgo.mobile.android", "omnibarTextInput"),
|
||||
new Browser("com.ecosia.android", "url_bar"),
|
||||
new Browser("com.google.android.apps.chrome", "url_bar"),
|
||||
@@ -107,6 +109,7 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("org.torproject.torbrowser_alpha", "mozac_browser_toolbar_url_view,url_bar_title"), // 2nd = Legacy (before v10.0a8)
|
||||
new Browser("org.ungoogled.chromium.extensions.stable", "url_bar"),
|
||||
new Browser("org.ungoogled.chromium.stable", "url_bar"),
|
||||
new Browser("us.spotco.fennec_dos", "mozac_browser_toolbar_url_view,url_bar_title"), // 2nd = Legacy
|
||||
|
||||
// [Section B] Entries only present here
|
||||
//
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<DefineConstants>DEBUG;</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>3</WarningLevel>
|
||||
<AndroidLinkMode>None</AndroidLinkMode>
|
||||
<AndroidSupportedAbis />
|
||||
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
|
||||
</PropertyGroup>
|
||||
@@ -75,24 +74,27 @@
|
||||
<Version>2.1.0.4</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Portable.BouncyCastle">
|
||||
<Version>1.8.6.7</Version>
|
||||
<Version>1.8.10</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.3-beta01" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.5.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AppCompat.AppCompatResources" Version="1.3.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.6" />
|
||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.8" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.7" />
|
||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.4" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.5.3.2</Version>
|
||||
<Version>1.7.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>121.0.1</Version>
|
||||
<Version>122.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.0.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.6" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.27.0" />
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.3.0.1" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.Fido" Version="118.1.0" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>71.1600.0</Version>
|
||||
<Version>117.0.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -114,24 +116,24 @@
|
||||
<Compile Include="Effects\FixedSizeEffect.cs" />
|
||||
<Compile Include="Effects\SelectableLabelEffect.cs" />
|
||||
<Compile Include="Effects\TabBarEffect.cs" />
|
||||
<Compile Include="Fido2System\Fido2BuilderObject.cs" />
|
||||
<Compile Include="Fido2System\Fido2Service.cs" />
|
||||
<Compile Include="Push\FirebaseMessagingService.cs" />
|
||||
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\RestrictionsChangedReceiver.cs" />
|
||||
<Compile Include="Receivers\EventUploadReceiver.cs" />
|
||||
<Compile Include="Receivers\LockAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\PackageReplacedReceiver.cs" />
|
||||
<Compile Include="Renderers\CipherViewCellRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedGridRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedDatePickerRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedStackLayoutRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedTimePickerRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedSliderRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomEditorRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomPickerRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomEntryRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomSearchBarRenderer.cs" />
|
||||
<Compile Include="Renderers\ExtendedListViewRenderer.cs" />
|
||||
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
||||
<Compile Include="Renderers\SendViewCellRenderer.cs" />
|
||||
<Compile Include="Services\AndroidPushNotificationService.cs" />
|
||||
<Compile Include="Services\AndroidLogService.cs" />
|
||||
<Compile Include="MainApplication.cs" />
|
||||
@@ -173,6 +175,9 @@
|
||||
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" />
|
||||
<AndroidResource Include="Resources\drawable\id.xml" />
|
||||
<AndroidResource Include="Resources\drawable\info.xml" />
|
||||
<AndroidResource Include="Resources\drawable\list_item_bg.xml" />
|
||||
<AndroidResource Include="Resources\drawable\list_item_bg_dark.xml" />
|
||||
<AndroidResource Include="Resources\drawable\list_item_bg_nord.xml" />
|
||||
<AndroidResource Include="Resources\drawable\lock.xml" />
|
||||
<AndroidResource Include="Resources\drawable\login.xml" />
|
||||
<AndroidResource Include="Resources\drawable\logo.xml" />
|
||||
@@ -185,7 +190,6 @@
|
||||
<AndroidResource Include="Resources\drawable\shield.xml" />
|
||||
<AndroidResource Include="Resources\drawable-v23\splash_screen.xml" />
|
||||
<AndroidResource Include="Resources\drawable-v23\splash_screen_dark.xml" />
|
||||
<AndroidResource Include="Resources\layout\SendViewCell.axml" />
|
||||
<AndroidResource Include="Resources\layout\Tabbar.axml" />
|
||||
<AndroidResource Include="Resources\layout\Toolbar.axml" />
|
||||
<AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" />
|
||||
@@ -262,12 +266,6 @@
|
||||
<SubType>Designer</SubType>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\layout\CipherViewCell.axml">
|
||||
<Generator>MSBuild:UpdateGeneratedFiles</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\xml\app_restrictions.xml">
|
||||
<Generator>MSBuild:UpdateGeneratedFiles</Generator>
|
||||
|
||||
@@ -63,6 +63,8 @@ namespace Bit.Droid.Autofill
|
||||
"com.chrome.beta",
|
||||
"com.chrome.canary",
|
||||
"com.chrome.dev",
|
||||
"com.cookiegames.smartcookie",
|
||||
"com.cookiejarapps.android.smartcookieweb",
|
||||
"com.ecosia.android",
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
@@ -120,6 +122,7 @@ namespace Bit.Droid.Autofill
|
||||
"org.torproject.torbrowser_alpha",
|
||||
"org.ungoogled.chromium.extensions.stable",
|
||||
"org.ungoogled.chromium.stable",
|
||||
"us.spotco.fennec_dos",
|
||||
};
|
||||
|
||||
// The URLs are blacklisted from autofilling
|
||||
@@ -140,13 +143,14 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
var allCiphers = ciphers.Item1.ToList();
|
||||
allCiphers.AddRange(ciphers.Item2.ToList());
|
||||
return allCiphers.Select(c => new FilledItem(c)).ToList();
|
||||
var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None);
|
||||
return nonPromptCiphers.Select(c => new FilledItem(c)).ToList();
|
||||
}
|
||||
}
|
||||
else if (parser.FieldCollection.FillableForCard)
|
||||
{
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
return ciphers.Where(c => c.Type == CipherType.Card).Select(c => new FilledItem(c)).ToList();
|
||||
return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList();
|
||||
}
|
||||
return new List<FilledItem>();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace Bit.Droid.Autofill
|
||||
}
|
||||
|
||||
List<FilledItem> items = null;
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
var locked = await _vaultTimeoutService.IsLockedAsync();
|
||||
if (!locked)
|
||||
{
|
||||
|
||||
106
src/Android/Fido2System/Fido2BuilderObject.cs
Normal file
106
src/Android/Fido2System/Fido2BuilderObject.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
#if !FDROID
|
||||
using System.Collections.Generic;
|
||||
using Android.Gms.Fido.Common;
|
||||
using Android.Gms.Fido.Fido2.Api.Common;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Java.Lang;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Bit.Droid.Fido2System
|
||||
{
|
||||
class Fido2BuilderObject
|
||||
{
|
||||
public static PublicKeyCredentialRequestOptions ParsePublicKeyCredentialRequestOptions(
|
||||
Fido2AuthenticationChallengeResponse data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var builder = new PublicKeyCredentialRequestOptions.Builder();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.Challenge))
|
||||
{
|
||||
builder.SetChallenge(CoreHelpers.Base64UrlDecode(data.Challenge));
|
||||
}
|
||||
if (data.AllowCredentials != null && data.AllowCredentials.Count > 0)
|
||||
{
|
||||
builder.SetAllowList(ParseCredentialDescriptors(data.AllowCredentials));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(data.RpId))
|
||||
{
|
||||
builder.SetRpId(data.RpId);
|
||||
}
|
||||
if (data.Timeout > 0)
|
||||
{
|
||||
builder.SetTimeoutSeconds((Double)(data.Timeout / 1000));
|
||||
}
|
||||
if (data.Extensions != null)
|
||||
{
|
||||
builder.SetAuthenticationExtensions(ParseExtensions((JObject)data.Extensions));
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private static List<PublicKeyCredentialDescriptor> ParseCredentialDescriptors(
|
||||
List<Fido2CredentialDescriptor> listData)
|
||||
{
|
||||
if (listData == null || listData.Count == 0)
|
||||
{
|
||||
return new List<PublicKeyCredentialDescriptor>();
|
||||
}
|
||||
|
||||
var credentials = new List<PublicKeyCredentialDescriptor>();
|
||||
|
||||
foreach (var data in listData)
|
||||
{
|
||||
string id = null;
|
||||
string type = null;
|
||||
var transports = new List<Transport>();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.Id))
|
||||
{
|
||||
id = data.Id;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(data.Type))
|
||||
{
|
||||
type = data.Type;
|
||||
}
|
||||
if (data.Transports != null && data.Transports.Count > 0)
|
||||
{
|
||||
foreach (var transport in data.Transports)
|
||||
{
|
||||
transports.Add(Transport.FromString(transport));
|
||||
}
|
||||
}
|
||||
|
||||
credentials.Add(new PublicKeyCredentialDescriptor(type, CoreHelpers.Base64UrlDecode(id), transports));
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private static AuthenticationExtensions ParseExtensions(JObject extensions)
|
||||
{
|
||||
var builder = new AuthenticationExtensions.Builder();
|
||||
|
||||
if (extensions.ContainsKey("appid"))
|
||||
{
|
||||
var appId = new FidoAppIdExtension((string)extensions.GetValue("appid"));
|
||||
builder.SetFido2Extension(appId);
|
||||
}
|
||||
|
||||
if (extensions.ContainsKey("uvm"))
|
||||
{
|
||||
var uvm = new UserVerificationMethodExtension((bool)extensions.GetValue("uvm"));
|
||||
builder.SetUserVerificationMethodExtension(uvm);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
249
src/Android/Fido2System/Fido2Service.cs
Normal file
249
src/Android/Fido2System/Fido2Service.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
#if !FDROID
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Gms.Fido;
|
||||
using Android.Gms.Fido.Fido2;
|
||||
using Android.Gms.Fido.Fido2.Api.Common;
|
||||
using Android.Gms.Tasks;
|
||||
using Android.Util;
|
||||
using AndroidX.AppCompat.App;
|
||||
using Bit.App.Services;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Java.Lang;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Forms;
|
||||
using Enum = System.Enum;
|
||||
|
||||
namespace Bit.Droid.Fido2System
|
||||
{
|
||||
public class Fido2Service
|
||||
{
|
||||
public static readonly string _tag_log = "Fido2Service";
|
||||
|
||||
public static Fido2Service INSTANCE = new Fido2Service();
|
||||
|
||||
private readonly MobileI18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private AppCompatActivity _activity;
|
||||
private Fido2ApiClient _fido2ApiClient;
|
||||
private Fido2CodesTypes _fido2CodesType;
|
||||
|
||||
public Fido2Service()
|
||||
{
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
}
|
||||
|
||||
public void Start(AppCompatActivity activity)
|
||||
{
|
||||
_activity = activity;
|
||||
_fido2ApiClient = Fido.GetFido2ApiClient(_activity);
|
||||
}
|
||||
|
||||
public void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||
{
|
||||
if (resultCode == Result.Ok && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
|
||||
{
|
||||
switch ((Fido2CodesTypes)requestCode)
|
||||
{
|
||||
case Fido2CodesTypes.RequestSignInUser:
|
||||
var errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
|
||||
if (errorExtra != null)
|
||||
{
|
||||
HandleErrorCode(errorExtra);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
SignInUserResponse(data);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// TODO: Key registration, should we ever choose to implement client-side
|
||||
/*case Fido2CodesTypes.RequestRegisterNewKey:
|
||||
errorExtra = data?.GetByteArrayExtra(Fido.Fido2KeyErrorExtra);
|
||||
if (errorExtra != null)
|
||||
{
|
||||
HandleErrorCode(errorExtra);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
// begin registration flow
|
||||
}
|
||||
}
|
||||
break;*/
|
||||
}
|
||||
}
|
||||
else if (resultCode == Result.Canceled && Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
|
||||
{
|
||||
Log.Info(_tag_log, "cancelled");
|
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2AbortError"),
|
||||
_i18nService.T("Fido2Title"));
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSuccess(Object result)
|
||||
{
|
||||
if (result != null && Enum.IsDefined(typeof(Fido2CodesTypes), _fido2CodesType))
|
||||
{
|
||||
try
|
||||
{
|
||||
_activity.StartIntentSenderForResult(((PendingIntent)result).IntentSender, (int)_fido2CodesType,
|
||||
null, 0, 0, 0);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Error(_tag_log, e.Message);
|
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
|
||||
_i18nService.T("Fido2Title"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnFailure(Exception e)
|
||||
{
|
||||
Log.Error(_tag_log, e.Message ?? "OnFailure: No error message returned");
|
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
|
||||
_i18nService.T("Fido2Title"));
|
||||
}
|
||||
|
||||
public void OnComplete(Task task)
|
||||
{
|
||||
Log.Debug(_tag_log, "OnComplete");
|
||||
}
|
||||
|
||||
public async System.Threading.Tasks.Task SignInUserRequestAsync(string dataJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataObject = JsonConvert.DeserializeObject<Fido2AuthenticationChallengeResponse>(dataJson);
|
||||
_fido2CodesType = Fido2CodesTypes.RequestSignInUser;
|
||||
var options = Fido2BuilderObject.ParsePublicKeyCredentialRequestOptions(dataObject);
|
||||
var task = _fido2ApiClient.GetSignPendingIntent(options);
|
||||
task.AddOnSuccessListener((IOnSuccessListener)_activity)
|
||||
.AddOnFailureListener((IOnFailureListener)_activity)
|
||||
.AddOnCompleteListener((IOnCompleteListener)_activity);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Error(_tag_log, e.StackTrace);
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
|
||||
_i18nService.T("Fido2Title"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.Info(_tag_log, "SignInUserRequest() -> finally()");
|
||||
}
|
||||
}
|
||||
|
||||
private void SignInUserResponse(Intent data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response =
|
||||
AuthenticatorAssertionResponse.DeserializeFromBytes(
|
||||
data.GetByteArrayExtra(Fido.Fido2KeyResponseExtra));
|
||||
var responseJson = JsonConvert.SerializeObject(new Fido2AuthenticationChallengeRequest
|
||||
{
|
||||
Id = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
|
||||
RawId = CoreHelpers.Base64UrlEncode(response.GetKeyHandle()),
|
||||
Type = "public-key",
|
||||
Response = new Fido2AssertionResponse
|
||||
{
|
||||
AuthenticatorData = CoreHelpers.Base64UrlEncode(response.GetAuthenticatorData()),
|
||||
ClientDataJson = CoreHelpers.Base64UrlEncode(response.GetClientDataJSON()),
|
||||
Signature = CoreHelpers.Base64UrlEncode(response.GetSignature()),
|
||||
UserHandle = (response.GetUserHandle() != null
|
||||
? CoreHelpers.Base64UrlEncode(response.GetUserHandle()) : null),
|
||||
},
|
||||
Extensions = null
|
||||
}
|
||||
);
|
||||
Device.BeginInvokeOnMainThread(() => ((MainActivity)_activity).Fido2Submission(responseJson));
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Log.Error(_tag_log, e.Message);
|
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T("Fido2SomethingWentWrong"),
|
||||
_i18nService.T("Fido2Title"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.Info(_tag_log, "SignInUserResponse() -> finally()");
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleErrorCode(byte[] errorExtra)
|
||||
{
|
||||
var error = AuthenticatorErrorResponse.DeserializeFromBytes(errorExtra);
|
||||
if (error.ErrorMessage.Length > 0)
|
||||
{
|
||||
Log.Info(_tag_log, error.ErrorMessage);
|
||||
}
|
||||
string message = "";
|
||||
if (error.ErrorCode == ErrorCode.AbortErr)
|
||||
{
|
||||
message = "Fido2AbortError";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.TimeoutErr)
|
||||
{
|
||||
message = "Fido2TimeoutError";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.AttestationNotPrivateErr)
|
||||
{
|
||||
message = "Fido2PrivacyError";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.ConstraintErr)
|
||||
{
|
||||
message = "Fido2SomethingWentWrong";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.DataErr)
|
||||
{
|
||||
message = "Fido2ServerDataFail";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.EncodingErr)
|
||||
{
|
||||
message = "Fido2SomethingWentWrong";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.InvalidStateErr)
|
||||
{
|
||||
message = "Fido2SomethingWentWrong";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.NetworkErr)
|
||||
{
|
||||
message = "Fido2NetworkFail";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.NotAllowedErr)
|
||||
{
|
||||
message = "Fido2NoPermission";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.NotSupportedErr)
|
||||
{
|
||||
message = "Fido2NotSupportedError";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.SecurityErr)
|
||||
{
|
||||
message = "Fido2SecurityError";
|
||||
}
|
||||
else if (error.ErrorCode == ErrorCode.UnknownErr)
|
||||
{
|
||||
message = "Fido2SomethingWentWrong";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "Fido2SomethingWentWrong";
|
||||
}
|
||||
_platformUtilsService.ShowDialogAsync(_i18nService.T(message), _i18nService.T("Fido2Title"));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Android.Content;
|
||||
using Bit.Droid.Utilities;
|
||||
using Bit.Droid.Receivers;
|
||||
@@ -17,7 +18,11 @@ using Bit.Core.Enums;
|
||||
using Android.Nfc;
|
||||
using Bit.App.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Util;
|
||||
using AndroidX.Core.Content;
|
||||
#if !FDROID
|
||||
using Bit.Droid.Fido2System;
|
||||
#endif
|
||||
using ZXing.Net.Mobile.Android;
|
||||
|
||||
namespace Bit.Droid
|
||||
@@ -27,6 +32,7 @@ namespace Bit.Droid
|
||||
Icon = "@mipmap/ic_launcher",
|
||||
Theme = "@style/LaunchTheme",
|
||||
MainLauncher = true,
|
||||
LaunchMode = LaunchMode.SingleTask,
|
||||
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
|
||||
ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
|
||||
ConfigChanges.Navigation)]
|
||||
@@ -41,7 +47,10 @@ namespace Bit.Droid
|
||||
@"text/*"
|
||||
})]
|
||||
[Register("com.x8bit.bitwarden.MainActivity")]
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity,
|
||||
Android.Gms.Tasks.IOnSuccessListener,
|
||||
Android.Gms.Tasks.IOnCompleteListener,
|
||||
Android.Gms.Tasks.IOnFailureListener
|
||||
{
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IMessagingService _messagingService;
|
||||
@@ -50,22 +59,19 @@ namespace Bit.Droid
|
||||
private IAppIdService _appIdService;
|
||||
private IStorageService _storageService;
|
||||
private IEventService _eventService;
|
||||
private PendingIntent _vaultTimeoutAlarmPendingIntent;
|
||||
private PendingIntent _clearClipboardPendingIntent;
|
||||
private PendingIntent _eventUploadPendingIntent;
|
||||
private AppOptions _appOptions;
|
||||
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
||||
private Java.Util.Regex.Pattern _otpPattern =
|
||||
Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
|
||||
private string _fidoDataJson;
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
||||
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
var alarmIntent = new Intent(this, typeof(LockAlarmReceiver));
|
||||
_vaultTimeoutAlarmPendingIntent = PendingIntent.GetBroadcast(this, 0, alarmIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
var clearClipboardIntent = new Intent(this, typeof(ClearClipboardAlarmReceiver));
|
||||
_clearClipboardPendingIntent = PendingIntent.GetBroadcast(this, 0, clearClipboardIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
@@ -94,6 +100,7 @@ namespace Bit.Droid
|
||||
#if !FDROID
|
||||
var appCenterHelper = new AppCenterHelper(_appIdService, _userService);
|
||||
var appCenterTask = appCenterHelper.InitAsync();
|
||||
Fido2Service.INSTANCE.Start(this);
|
||||
#endif
|
||||
|
||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||
@@ -103,20 +110,7 @@ namespace Bit.Droid
|
||||
|
||||
_broadcasterService.Subscribe(_activityKey, (message) =>
|
||||
{
|
||||
if (message.Command == "scheduleVaultTimeoutTimer")
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
var vaultTimeoutMinutes = (int)message.Data;
|
||||
var vaultTimeoutMs = vaultTimeoutMinutes * 60000;
|
||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + vaultTimeoutMs + 10;
|
||||
alarmManager.Set(AlarmType.RtcWakeup, triggerMs, _vaultTimeoutAlarmPendingIntent);
|
||||
}
|
||||
else if (message.Command == "cancelVaultTimeoutTimer")
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Cancel(_vaultTimeoutAlarmPendingIntent);
|
||||
}
|
||||
else if (message.Command == "startEventTimer")
|
||||
if (message.Command == "startEventTimer")
|
||||
{
|
||||
StartEventAlarm();
|
||||
}
|
||||
@@ -128,6 +122,14 @@ namespace Bit.Droid
|
||||
{
|
||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
|
||||
}
|
||||
else if (message.Command == "listenFido2")
|
||||
{
|
||||
ListenFido2((Dictionary<string, object>)message.Data);
|
||||
}
|
||||
else if (message.Command == "listenFido2TryAgain")
|
||||
{
|
||||
ListenFido2();
|
||||
}
|
||||
else if (message.Command == "listenYubiKeyOTP")
|
||||
{
|
||||
ListenYubiKey((bool)message.Data);
|
||||
@@ -165,39 +167,48 @@ namespace Bit.Droid
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
var setRestrictions = AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this);
|
||||
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
if (intent.GetBooleanExtra("generatorTile", false))
|
||||
try
|
||||
{
|
||||
_messagingService.Send("popAllAndGoToTabGenerator");
|
||||
if (_appOptions != null)
|
||||
if (intent.GetBooleanExtra("generatorTile", false))
|
||||
{
|
||||
_appOptions.GeneratorTile = true;
|
||||
_messagingService.Send("popAllAndGoToTabGenerator");
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.GeneratorTile = true;
|
||||
}
|
||||
}
|
||||
else if (intent.GetBooleanExtra("myVaultTile", false))
|
||||
{
|
||||
_messagingService.Send("popAllAndGoToTabMyVault");
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.MyVaultTile = true;
|
||||
}
|
||||
}
|
||||
else if (intent.Action == Intent.ActionSend && intent.Type != null)
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.CreateSend = GetCreateSendRequest(intent);
|
||||
}
|
||||
_messagingService.Send("popAllAndGoToTabSend");
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseYubiKey(intent.DataString);
|
||||
}
|
||||
}
|
||||
else if (intent.GetBooleanExtra("myVaultTile", false))
|
||||
catch (Exception e)
|
||||
{
|
||||
_messagingService.Send("popAllAndGoToTabMyVault");
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.MyVaultTile = true;
|
||||
}
|
||||
}
|
||||
else if (intent.Action == Intent.ActionSend && intent.Type != null)
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
_appOptions.CreateSend = GetCreateSendRequest(intent);
|
||||
}
|
||||
_messagingService.Send("popAllAndGoToTabSend");
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseYubiKey(intent.DataString);
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +278,34 @@ namespace Bit.Droid
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (resultCode == Result.Ok &&
|
||||
Enum.IsDefined(typeof(Fido2CodesTypes), requestCode))
|
||||
{
|
||||
#if !FDROID
|
||||
Fido2Service.INSTANCE.OnActivityResult(requestCode, resultCode, data);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSuccess(Java.Lang.Object result)
|
||||
{
|
||||
#if !FDROID
|
||||
Fido2Service.INSTANCE.OnSuccess(result);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnComplete(Android.Gms.Tasks.Task task)
|
||||
{
|
||||
#if !FDROID
|
||||
Fido2Service.INSTANCE.OnComplete(task);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnFailure(Java.Lang.Exception e)
|
||||
{
|
||||
#if !FDROID
|
||||
Fido2Service.INSTANCE.OnFailure(e);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
@@ -275,6 +314,41 @@ namespace Bit.Droid
|
||||
_broadcasterService.Unsubscribe(_activityKey);
|
||||
}
|
||||
|
||||
private void ListenFido2(Dictionary<string, object> data = null)
|
||||
{
|
||||
if (!_deviceActionService.SupportsFido2())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if !FDROID
|
||||
RunOnUiThread(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
_fidoDataJson = Newtonsoft.Json.JsonConvert.SerializeObject(data);
|
||||
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Fido2Service.INSTANCE.SignInUserRequestAsync(_fidoDataJson);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(Fido2Service._tag_log, e.Message);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Fido2Submission(string token)
|
||||
{
|
||||
_messagingService.Send("gotFido2Token", token);
|
||||
}
|
||||
|
||||
private void ListenYubiKey(bool listen)
|
||||
{
|
||||
if (!_deviceActionService.SupportsNfc())
|
||||
@@ -343,6 +417,11 @@ namespace Bit.Droid
|
||||
{
|
||||
if (intent.Action == Intent.ActionSend && intent.Type != null)
|
||||
{
|
||||
if ((intent.Flags & ActivityFlags.LaunchedFromHistory) == ActivityFlags.LaunchedFromHistory)
|
||||
{
|
||||
// don't re-deliver intent if resuming from app switcher
|
||||
return null;
|
||||
}
|
||||
var type = intent.Type;
|
||||
if (type.Contains("text/"))
|
||||
{
|
||||
|
||||
@@ -99,6 +99,9 @@ namespace Bit.Droid
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
var biometricService = new BiometricService();
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(mobileStorageService, secureStorageService, cryptoFunctionService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);
|
||||
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
|
||||
@@ -110,6 +113,9 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.10.0"
|
||||
android:versionName="2.12.0"
|
||||
android:installLocation="internalOnly"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
@@ -54,12 +54,7 @@
|
||||
<!-- Package visibility (for Android 11+) -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
<action android:name="*"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
using Android.Content;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.LockAlarmReceiver", Exported = false)]
|
||||
public class LockAlarmReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
await vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Graphics;
|
||||
using Android.Runtime;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Views.InputMethods;
|
||||
using Android.Widget;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Droid.Renderers;
|
||||
using FFImageLoading;
|
||||
using FFImageLoading.Views;
|
||||
using FFImageLoading.Work;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(CipherViewCell), typeof(CipherViewCellRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class CipherViewCellRenderer : ViewCellRenderer
|
||||
{
|
||||
private static Typeface _faTypeface;
|
||||
private static Typeface _miTypeface;
|
||||
private static Android.Graphics.Color _textColor;
|
||||
private static Android.Graphics.Color _mutedColor;
|
||||
private static Android.Graphics.Color _disabledIconColor;
|
||||
private static bool _usingLightTheme;
|
||||
|
||||
private AndroidCipherCell _cell;
|
||||
|
||||
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView,
|
||||
ViewGroup parent, Context context)
|
||||
{
|
||||
// TODO expand beyond light/dark detection once we support custom theme switching without app restart
|
||||
var themeChanged = _usingLightTheme != ThemeManager.UsingLightTheme;
|
||||
if (_faTypeface == null)
|
||||
{
|
||||
_faTypeface = Typeface.CreateFromAsset(context.Assets, "FontAwesome.ttf");
|
||||
}
|
||||
if (_miTypeface == null)
|
||||
{
|
||||
_miTypeface = Typeface.CreateFromAsset(context.Assets, "MaterialIcons_Regular.ttf");
|
||||
}
|
||||
if (_textColor == default(Android.Graphics.Color) || themeChanged)
|
||||
{
|
||||
_textColor = ThemeManager.GetResourceColor("TextColor").ToAndroid();
|
||||
}
|
||||
if (_mutedColor == default(Android.Graphics.Color) || themeChanged)
|
||||
{
|
||||
_mutedColor = ThemeManager.GetResourceColor("MutedColor").ToAndroid();
|
||||
}
|
||||
if (_disabledIconColor == default(Android.Graphics.Color) || themeChanged)
|
||||
{
|
||||
_disabledIconColor = ThemeManager.GetResourceColor("DisabledIconColor").ToAndroid();
|
||||
}
|
||||
_usingLightTheme = ThemeManager.UsingLightTheme;
|
||||
|
||||
var cipherCell = item as CipherViewCell;
|
||||
_cell = convertView as AndroidCipherCell;
|
||||
if (_cell == null)
|
||||
{
|
||||
_cell = new AndroidCipherCell(context, cipherCell, _faTypeface, _miTypeface);
|
||||
}
|
||||
else
|
||||
{
|
||||
_cell.CipherViewCell.PropertyChanged -= CellPropertyChanged;
|
||||
}
|
||||
cipherCell.PropertyChanged += CellPropertyChanged;
|
||||
_cell.UpdateCell(cipherCell);
|
||||
_cell.UpdateColors(_textColor, _mutedColor, _disabledIconColor);
|
||||
return _cell;
|
||||
}
|
||||
|
||||
public void CellPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
var cipherCell = sender as CipherViewCell;
|
||||
_cell.CipherViewCell = cipherCell;
|
||||
if (e.PropertyName == CipherViewCell.CipherProperty.PropertyName)
|
||||
{
|
||||
_cell.UpdateCell(cipherCell);
|
||||
}
|
||||
else if (e.PropertyName == CipherViewCell.WebsiteIconsEnabledProperty.PropertyName)
|
||||
{
|
||||
_cell.UpdateIconImage(cipherCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AndroidCipherCell : LinearLayout, INativeElementView
|
||||
{
|
||||
private readonly Typeface _faTypeface;
|
||||
private readonly Typeface _miTypeface;
|
||||
|
||||
private IScheduledWork _currentTask;
|
||||
|
||||
public AndroidCipherCell(Context context, CipherViewCell cipherView, Typeface faTypeface, Typeface miTypeface)
|
||||
: base(context)
|
||||
{
|
||||
CipherViewCell = cipherView;
|
||||
_faTypeface = faTypeface;
|
||||
_miTypeface = miTypeface;
|
||||
|
||||
var view = (context as Activity).LayoutInflater.Inflate(Resource.Layout.CipherViewCell, null);
|
||||
IconImage = view.FindViewById<IconImageView>(Resource.Id.CipherCellIconImage);
|
||||
Icon = view.FindViewById<TextView>(Resource.Id.CipherCellIcon);
|
||||
Name = view.FindViewById<TextView>(Resource.Id.CipherCellName);
|
||||
SubTitle = view.FindViewById<TextView>(Resource.Id.CipherCellSubTitle);
|
||||
SharedIcon = view.FindViewById<TextView>(Resource.Id.CipherCellSharedIcon);
|
||||
AttachmentsIcon = view.FindViewById<TextView>(Resource.Id.CipherCellAttachmentsIcon);
|
||||
MoreButton = view.FindViewById<Android.Widget.Button>(Resource.Id.CipherCellButton);
|
||||
MoreButton.Click += MoreButton_Click;
|
||||
|
||||
Icon.Typeface = _faTypeface;
|
||||
SharedIcon.Typeface = _faTypeface;
|
||||
AttachmentsIcon.Typeface = _faTypeface;
|
||||
MoreButton.Typeface = _miTypeface;
|
||||
|
||||
var small = (float)Device.GetNamedSize(NamedSize.Small, typeof(Label));
|
||||
Icon.SetTextSize(ComplexUnitType.Pt, 10);
|
||||
Name.SetTextSize(ComplexUnitType.Sp, (float)Device.GetNamedSize(NamedSize.Medium, typeof(Label)));
|
||||
SubTitle.SetTextSize(ComplexUnitType.Sp, small);
|
||||
SharedIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
AttachmentsIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
MoreButton.SetTextSize(ComplexUnitType.Sp, 25);
|
||||
|
||||
AddView(view);
|
||||
}
|
||||
|
||||
public CipherViewCell CipherViewCell { get; set; }
|
||||
public Element Element => CipherViewCell;
|
||||
|
||||
public IconImageView IconImage { get; set; }
|
||||
public TextView Icon { get; set; }
|
||||
public TextView Name { get; set; }
|
||||
public TextView SubTitle { get; set; }
|
||||
public TextView SharedIcon { get; set; }
|
||||
public TextView AttachmentsIcon { get; set; }
|
||||
public Android.Widget.Button MoreButton { get; set; }
|
||||
|
||||
public void UpdateCell(CipherViewCell cipherCell)
|
||||
{
|
||||
UpdateIconImage(cipherCell);
|
||||
|
||||
var cipher = cipherCell.Cipher;
|
||||
Name.Text = cipher.Name;
|
||||
if (!string.IsNullOrWhiteSpace(cipher.SubTitle))
|
||||
{
|
||||
SubTitle.Text = cipher.SubTitle;
|
||||
SubTitle.Visibility = ViewStates.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
SubTitle.Visibility = ViewStates.Invisible;
|
||||
}
|
||||
SharedIcon.Visibility = cipher.Shared ? ViewStates.Visible : ViewStates.Gone;
|
||||
AttachmentsIcon.Visibility = cipher.HasAttachments ? ViewStates.Visible : ViewStates.Gone;
|
||||
}
|
||||
|
||||
public void UpdateIconImage(CipherViewCell cipherCell)
|
||||
{
|
||||
if (_currentTask != null && !_currentTask.IsCancelled && !_currentTask.IsCompleted)
|
||||
{
|
||||
_currentTask.Cancel();
|
||||
}
|
||||
|
||||
var cipher = cipherCell.Cipher;
|
||||
|
||||
var iconImage = cipherCell.GetIconImage(cipher);
|
||||
if (iconImage.Item2 != null)
|
||||
{
|
||||
IconImage.SetImageResource(Resource.Drawable.login);
|
||||
IconImage.Visibility = ViewStates.Visible;
|
||||
Icon.Visibility = ViewStates.Gone;
|
||||
_currentTask = ImageService.Instance.LoadUrl(iconImage.Item2).DownSample(64).Into(IconImage);
|
||||
IconImage.Key = iconImage.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
IconImage.Visibility = ViewStates.Gone;
|
||||
Icon.Visibility = ViewStates.Visible;
|
||||
Icon.Text = iconImage.Item1;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateColors(Android.Graphics.Color textColor, Android.Graphics.Color mutedColor,
|
||||
Android.Graphics.Color iconDisabledColor)
|
||||
{
|
||||
Name.SetTextColor(textColor);
|
||||
SubTitle.SetTextColor(mutedColor);
|
||||
Icon.SetTextColor(mutedColor);
|
||||
SharedIcon.SetTextColor(mutedColor);
|
||||
AttachmentsIcon.SetTextColor(mutedColor);
|
||||
MoreButton.SetTextColor(iconDisabledColor);
|
||||
}
|
||||
|
||||
private void MoreButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (CipherViewCell.ButtonCommand?.CanExecute(CipherViewCell.Cipher) ?? false)
|
||||
{
|
||||
CipherViewCell.ButtonCommand.Execute(CipherViewCell.Cipher);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
MoreButton.Click -= MoreButton_Click;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
[Android.Runtime.Preserve(AllMembers = true)]
|
||||
[Register("bit.droid.renderers.IconImageView")]
|
||||
public class IconImageView : ImageViewAsync
|
||||
{
|
||||
public IconImageView(Context context) : base(context)
|
||||
{ }
|
||||
|
||||
public IconImageView(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{ }
|
||||
|
||||
public IconImageView(Context context, IAttributeSet attrs)
|
||||
: base(context, attrs)
|
||||
{ }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
protected override void JavaFinalize()
|
||||
{
|
||||
SetImageDrawable(null);
|
||||
SetImageBitmap(null);
|
||||
ImageService.Instance.InvalidateCacheEntryAsync(Key, FFImageLoading.Cache.CacheType.Memory);
|
||||
base.JavaFinalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/Android/Renderers/ExtendedGridRenderer.cs
Normal file
43
src/Android/Renderers/ExtendedGridRenderer.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Android.Content;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(ExtendedGrid), typeof(ExtendedGridRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class ExtendedGridRenderer : ViewRenderer
|
||||
{
|
||||
private static int? _bgResId;
|
||||
|
||||
public ExtendedGridRenderer(Context context) : base(context) { }
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<View> elementChangedEvent)
|
||||
{
|
||||
base.OnElementChanged(elementChangedEvent);
|
||||
if (elementChangedEvent.NewElement != null)
|
||||
{
|
||||
SetBackgroundResource(GetBgResId());
|
||||
}
|
||||
}
|
||||
|
||||
private int GetBgResId()
|
||||
{
|
||||
if (_bgResId == null)
|
||||
{
|
||||
if (ThemeManager.GetTheme(true) == "nord")
|
||||
{
|
||||
_bgResId = Resource.Drawable.list_item_bg_nord;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bgResId ??= ThemeManager.UsingLightTheme ? Resource.Drawable.list_item_bg :
|
||||
Resource.Drawable.list_item_bg_dark;
|
||||
}
|
||||
}
|
||||
return _bgResId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Android.Content;
|
||||
using Android.Views;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(ExtendedListView), typeof(ExtendedListViewRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class ExtendedListViewRenderer : ListViewRenderer
|
||||
{
|
||||
public ExtendedListViewRenderer(Context context)
|
||||
: base(context)
|
||||
{ }
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
|
||||
{
|
||||
base.OnElementChanged(e);
|
||||
if (Control != null && e.NewElement != null && e.NewElement is ExtendedListView listView)
|
||||
{
|
||||
// Pad for FAB
|
||||
Control.SetPadding(0, 0, 0, 170);
|
||||
Control.SetClipToPadding(false);
|
||||
Control.ScrollBarStyle = ScrollbarStyles.OutsideOverlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/Android/Renderers/ExtendedStackLayoutRenderer.cs
Normal file
43
src/Android/Renderers/ExtendedStackLayoutRenderer.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Android.Content;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
|
||||
[assembly: ExportRenderer(typeof(ExtendedStackLayout), typeof(ExtendedStackLayoutRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class ExtendedStackLayoutRenderer : ViewRenderer
|
||||
{
|
||||
private static int? _bgResId;
|
||||
|
||||
public ExtendedStackLayoutRenderer(Context context) : base(context) { }
|
||||
|
||||
protected override void OnElementChanged(ElementChangedEventArgs<View> elementChangedEvent)
|
||||
{
|
||||
base.OnElementChanged(elementChangedEvent);
|
||||
if (elementChangedEvent.NewElement != null)
|
||||
{
|
||||
SetBackgroundResource(GetBgResId());
|
||||
}
|
||||
}
|
||||
|
||||
private int GetBgResId()
|
||||
{
|
||||
if (_bgResId == null)
|
||||
{
|
||||
if (ThemeManager.GetTheme(true) == "nord")
|
||||
{
|
||||
_bgResId = Resource.Drawable.list_item_bg_nord;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bgResId ??= ThemeManager.UsingLightTheme ? Resource.Drawable.list_item_bg :
|
||||
Resource.Drawable.list_item_bg_dark;
|
||||
}
|
||||
}
|
||||
return _bgResId.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Graphics;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Droid.Renderers;
|
||||
using FFImageLoading.Work;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
using Button = Android.Widget.Button;
|
||||
using Color = Android.Graphics.Color;
|
||||
using View = Android.Views.View;
|
||||
|
||||
[assembly: ExportRenderer(typeof(SendViewCell), typeof(SendViewCellRenderer))]
|
||||
namespace Bit.Droid.Renderers
|
||||
{
|
||||
public class SendViewCellRenderer : ViewCellRenderer
|
||||
{
|
||||
private static Typeface _faTypeface;
|
||||
private static Typeface _miTypeface;
|
||||
private static Color _textColor;
|
||||
private static Color _mutedColor;
|
||||
private static Color _disabledIconColor;
|
||||
private static bool _usingLightTheme;
|
||||
|
||||
private AndroidSendCell _cell;
|
||||
|
||||
protected override View GetCellCore(Cell item, View convertView,
|
||||
ViewGroup parent, Context context)
|
||||
{
|
||||
// TODO expand beyond light/dark detection once we support custom theme switching without app restart
|
||||
var themeChanged = _usingLightTheme != ThemeManager.UsingLightTheme;
|
||||
if (_faTypeface == null)
|
||||
{
|
||||
_faTypeface = Typeface.CreateFromAsset(context.Assets, "FontAwesome.ttf");
|
||||
}
|
||||
if (_miTypeface == null)
|
||||
{
|
||||
_miTypeface = Typeface.CreateFromAsset(context.Assets, "MaterialIcons_Regular.ttf");
|
||||
}
|
||||
if (_textColor == default(Color) || themeChanged)
|
||||
{
|
||||
_textColor = ThemeManager.GetResourceColor("TextColor").ToAndroid();
|
||||
}
|
||||
if (_mutedColor == default(Color) || themeChanged)
|
||||
{
|
||||
_mutedColor = ThemeManager.GetResourceColor("MutedColor").ToAndroid();
|
||||
}
|
||||
if (_disabledIconColor == default(Color) || themeChanged)
|
||||
{
|
||||
_disabledIconColor = ThemeManager.GetResourceColor("DisabledIconColor").ToAndroid();
|
||||
}
|
||||
_usingLightTheme = ThemeManager.UsingLightTheme;
|
||||
|
||||
var sendCell = item as SendViewCell;
|
||||
_cell = convertView as AndroidSendCell;
|
||||
if (_cell == null)
|
||||
{
|
||||
_cell = new AndroidSendCell(context, sendCell, _faTypeface, _miTypeface);
|
||||
}
|
||||
else
|
||||
{
|
||||
_cell.SendViewCell.PropertyChanged -= CellPropertyChanged;
|
||||
}
|
||||
sendCell.PropertyChanged += CellPropertyChanged;
|
||||
_cell.UpdateCell(sendCell);
|
||||
_cell.UpdateColors(_textColor, _mutedColor, _disabledIconColor);
|
||||
return _cell;
|
||||
}
|
||||
|
||||
public void CellPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
var sendCell = sender as SendViewCell;
|
||||
_cell.SendViewCell = sendCell;
|
||||
if (e.PropertyName == SendViewCell.SendProperty.PropertyName)
|
||||
{
|
||||
_cell.UpdateCell(sendCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AndroidSendCell : LinearLayout, INativeElementView
|
||||
{
|
||||
private readonly Typeface _faTypeface;
|
||||
private readonly Typeface _miTypeface;
|
||||
|
||||
private IScheduledWork _currentTask;
|
||||
|
||||
public AndroidSendCell(Context context, SendViewCell sendView, Typeface faTypeface, Typeface miTypeface)
|
||||
: base(context)
|
||||
{
|
||||
SendViewCell = sendView;
|
||||
_faTypeface = faTypeface;
|
||||
_miTypeface = miTypeface;
|
||||
|
||||
var view = (context as Activity).LayoutInflater.Inflate(Resource.Layout.SendViewCell, null);
|
||||
Icon = view.FindViewById<TextView>(Resource.Id.SendCellIcon);
|
||||
Name = view.FindViewById<TextView>(Resource.Id.SendCellName);
|
||||
SubTitle = view.FindViewById<TextView>(Resource.Id.SendCellSubTitle);
|
||||
DisabledIcon = view.FindViewById<TextView>(Resource.Id.SendCellDisabledIcon);
|
||||
HasPasswordIcon = view.FindViewById<TextView>(Resource.Id.SendCellHasPasswordIcon);
|
||||
MaxAccessCountReachedIcon = view.FindViewById<TextView>(Resource.Id.SendCellMaxAccessCountReachedIcon);
|
||||
ExpiredIcon = view.FindViewById<TextView>(Resource.Id.SendCellExpiredIcon);
|
||||
PendingDeleteIcon = view.FindViewById<TextView>(Resource.Id.SendCellPendingDeleteIcon);
|
||||
MoreButton = view.FindViewById<Button>(Resource.Id.SendCellButton);
|
||||
MoreButton.Click += MoreButton_Click;
|
||||
|
||||
Icon.Typeface = _faTypeface;
|
||||
DisabledIcon.Typeface = _faTypeface;
|
||||
HasPasswordIcon.Typeface = _faTypeface;
|
||||
MaxAccessCountReachedIcon.Typeface = _faTypeface;
|
||||
ExpiredIcon.Typeface = _faTypeface;
|
||||
PendingDeleteIcon.Typeface = _faTypeface;
|
||||
MoreButton.Typeface = _miTypeface;
|
||||
|
||||
var small = (float)Device.GetNamedSize(NamedSize.Small, typeof(Label));
|
||||
Icon.SetTextSize(ComplexUnitType.Pt, 10);
|
||||
Name.SetTextSize(ComplexUnitType.Sp, (float)Device.GetNamedSize(NamedSize.Medium, typeof(Label)));
|
||||
SubTitle.SetTextSize(ComplexUnitType.Sp, small);
|
||||
DisabledIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
HasPasswordIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
MaxAccessCountReachedIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
ExpiredIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
PendingDeleteIcon.SetTextSize(ComplexUnitType.Sp, small);
|
||||
MoreButton.SetTextSize(ComplexUnitType.Sp, 25);
|
||||
|
||||
if (!SendViewCell.ShowOptions)
|
||||
{
|
||||
MoreButton.Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
AddView(view);
|
||||
}
|
||||
|
||||
public SendViewCell SendViewCell { get; set; }
|
||||
public Element Element => SendViewCell;
|
||||
|
||||
public TextView Icon { get; set; }
|
||||
public TextView Name { get; set; }
|
||||
public TextView SubTitle { get; set; }
|
||||
public TextView DisabledIcon { get; set; }
|
||||
public TextView HasPasswordIcon { get; set; }
|
||||
public TextView MaxAccessCountReachedIcon { get; set; }
|
||||
public TextView ExpiredIcon { get; set; }
|
||||
public TextView PendingDeleteIcon { get; set; }
|
||||
public Button MoreButton { get; set; }
|
||||
|
||||
public void UpdateCell(SendViewCell sendCell)
|
||||
{
|
||||
UpdateIconImage(sendCell);
|
||||
|
||||
var send = sendCell.Send;
|
||||
Name.Text = send.Name;
|
||||
SubTitle.Text = send.DisplayDate;
|
||||
DisabledIcon.Visibility = send.Disabled ? ViewStates.Visible : ViewStates.Gone;
|
||||
HasPasswordIcon.Visibility = send.HasPassword ? ViewStates.Visible : ViewStates.Gone;
|
||||
MaxAccessCountReachedIcon.Visibility = send.MaxAccessCountReached ? ViewStates.Visible : ViewStates.Gone;
|
||||
ExpiredIcon.Visibility = send.Expired ? ViewStates.Visible : ViewStates.Gone;
|
||||
PendingDeleteIcon.Visibility = send.PendingDelete ? ViewStates.Visible : ViewStates.Gone;
|
||||
}
|
||||
|
||||
public void UpdateIconImage(SendViewCell sendCell)
|
||||
{
|
||||
if (_currentTask != null && !_currentTask.IsCancelled && !_currentTask.IsCompleted)
|
||||
{
|
||||
_currentTask.Cancel();
|
||||
}
|
||||
|
||||
var send = sendCell.Send;
|
||||
var iconImage = sendCell.GetIconImage(send);
|
||||
Icon.Text = iconImage;
|
||||
}
|
||||
|
||||
public void UpdateColors(Color textColor, Color mutedColor,
|
||||
Color iconDisabledColor)
|
||||
{
|
||||
Name.SetTextColor(textColor);
|
||||
SubTitle.SetTextColor(mutedColor);
|
||||
Icon.SetTextColor(mutedColor);
|
||||
DisabledIcon.SetTextColor(mutedColor);
|
||||
HasPasswordIcon.SetTextColor(mutedColor);
|
||||
MaxAccessCountReachedIcon.SetTextColor(mutedColor);
|
||||
ExpiredIcon.SetTextColor(mutedColor);
|
||||
PendingDeleteIcon.SetTextColor(mutedColor);
|
||||
MoreButton.SetTextColor(iconDisabledColor);
|
||||
}
|
||||
|
||||
private void MoreButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (SendViewCell.ButtonCommand?.CanExecute(SendViewCell.Send) ?? false)
|
||||
{
|
||||
SendViewCell.ButtonCommand.Execute(SendViewCell.Send);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
MoreButton.Click -= MoreButton_Click;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/Android/Resources/drawable/list_item_bg.xml
Normal file
7
src/Android/Resources/drawable/list_item_bg.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/itemPressed">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:drawable="@android:color/white" />
|
||||
</ripple>
|
||||
7
src/Android/Resources/drawable/list_item_bg_dark.xml
Normal file
7
src/Android/Resources/drawable/list_item_bg_dark.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/dark_primary">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:drawable="@android:color/white" />
|
||||
</ripple>
|
||||
7
src/Android/Resources/drawable/list_item_bg_nord.xml
Normal file
7
src/Android/Resources/drawable/list_item_bg_nord.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/nord_primary">
|
||||
|
||||
<item
|
||||
android:id="@android:id/mask"
|
||||
android:drawable="@android:color/white" />
|
||||
</ripple>
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="44dp"
|
||||
android:gravity="center_vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="2.2dp">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="39.8dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center">
|
||||
<bit.droid.renderers.IconImageView
|
||||
android:id="@+id/CipherCellIconImage"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center" />
|
||||
<TextView
|
||||
android:id="@+id/CipherCellIcon"
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="[X]" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/CipherCellContent"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="7.65dp">
|
||||
<LinearLayout
|
||||
android:id="@+id/CipherCellContentTop"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@+id/CipherCellName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="Name" />
|
||||
<TextView
|
||||
android:id="@+id/CipherCellSharedIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
<TextView
|
||||
android:id="@+id/CipherCellAttachmentsIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/CipherCellSubTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="SubTitle" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/CipherCellButton"
|
||||
android:layout_width="37dp"
|
||||
android:layout_height="match_parent"
|
||||
android:text=""
|
||||
android:gravity="center"
|
||||
android:padding="0dp"
|
||||
android:background="@android:color/transparent" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="44dp"
|
||||
android:gravity="center_vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="2.2dp">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="39.8dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center">
|
||||
<TextView
|
||||
android:id="@+id/SendCellIcon"
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="[X]" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/SendCellContent"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="7.65dp">
|
||||
<LinearLayout
|
||||
android:id="@+id/SendCellContentTop"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@+id/SendCellName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="Name" />
|
||||
<TextView
|
||||
android:id="@+id/SendCellDisabledIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
<TextView
|
||||
android:id="@+id/SendCellHasPasswordIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
<TextView
|
||||
android:id="@+id/SendCellMaxAccessCountReachedIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
<TextView
|
||||
android:id="@+id/SendCellExpiredIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
<TextView
|
||||
android:id="@+id/SendCellPendingDeleteIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingLeft="5dp"
|
||||
android:singleLine="true"
|
||||
android:text="" />
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/SendCellSubTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="SubTitle" />
|
||||
</LinearLayout>
|
||||
<Button
|
||||
android:id="@+id/SendCellButton"
|
||||
android:layout_width="37dp"
|
||||
android:layout_height="match_parent"
|
||||
android:text=""
|
||||
android:gravity="center"
|
||||
android:padding="0dp"
|
||||
android:background="@android:color/transparent" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
@@ -6,6 +6,7 @@
|
||||
<color name="primary">#175DDC</color>
|
||||
<color name="notificationBar">#1452BC</color>
|
||||
<color name="border">#dddddd</color>
|
||||
<color name="itemPressed">#bbbbbb</color>
|
||||
|
||||
<!-- Dark theme -->
|
||||
<color name="dark_primary">#52bdfb</color>
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
<compatibility-package
|
||||
android:name="com.chrome.dev"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.cookiegames.smartcookie"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.cookiejarapps.android.smartcookieweb"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.ecosia.android"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -227,4 +233,7 @@
|
||||
<compatibility-package
|
||||
android:name="org.ungoogled.chromium.stable"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="us.spotco.fennec_dos"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
</autofill-service>
|
||||
|
||||
@@ -307,7 +307,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public Task<string> DisplayPromptAync(string title = null, string description = null,
|
||||
string text = null, string okButtonText = null, string cancelButtonText = null,
|
||||
bool numericKeyboard = false, bool autofocus = true)
|
||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
if (activity == null)
|
||||
@@ -333,6 +333,10 @@ namespace Bit.Droid.Services
|
||||
input.KeyListener = DigitsKeyListener.GetInstance(false, false);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
if (password)
|
||||
{
|
||||
input.InputType = InputTypes.TextVariationPassword | InputTypes.ClassText;
|
||||
}
|
||||
|
||||
input.ImeOptions = input.ImeOptions | (ImeAction)ImeFlags.NoPersonalizedLearning |
|
||||
(ImeAction)ImeFlags.NoExtractUi;
|
||||
@@ -772,6 +776,17 @@ namespace Bit.Droid.Services
|
||||
_messagingService.Send("finishMainActivity");
|
||||
}
|
||||
|
||||
public bool SupportsFido2()
|
||||
{
|
||||
#if !FDROID
|
||||
if ((int)Build.VERSION.SdkInt >= 21)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if (dir != null && dir.IsDirectory)
|
||||
|
||||
@@ -3,7 +3,9 @@ using Android.Content.PM;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
|
||||
[Activity(
|
||||
NoHistory = true,
|
||||
LaunchMode = LaunchMode.SingleTop)]
|
||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||
DataScheme = "bitwarden")]
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Bit.App.Abstractions
|
||||
Task SelectFileAsync();
|
||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||
bool autofocus = true);
|
||||
bool autofocus = true, bool password = false);
|
||||
void RateApp();
|
||||
bool SupportsFaceBiometric();
|
||||
Task<bool> SupportsFaceBiometricAsync();
|
||||
@@ -45,5 +45,6 @@ namespace Bit.App.Abstractions
|
||||
bool UsingDarkTheme();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
bool SupportsFido2();
|
||||
}
|
||||
}
|
||||
|
||||
11
src/App/Abstractions/IPasswordRepromptService.cs
Normal file
11
src/App/Abstractions/IPasswordRepromptService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IPasswordRepromptService
|
||||
{
|
||||
string[] ProtectedFields { get; }
|
||||
|
||||
Task<bool> ShowPasswordPromptAsync();
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="3.2.1" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.2" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.3.0" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="4.5.0.725" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2083" />
|
||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||
</ItemGroup>
|
||||
@@ -411,4 +411,4 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -178,6 +178,10 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
}
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
_messagingService.Send("startEventTimer");
|
||||
}
|
||||
|
||||
@@ -216,7 +220,6 @@ namespace Bit.App
|
||||
private async void ResumedAsync()
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
_messagingService.Send("cancelVaultTimeoutTimer");
|
||||
_messagingService.Send("startEventTimer");
|
||||
await ClearCacheIfNeededAsync();
|
||||
Prime();
|
||||
@@ -313,11 +316,7 @@ namespace Bit.App
|
||||
vaultTimeout = await _storageService.GetAsync<int?>(Constants.VaultTimeoutKey);
|
||||
}
|
||||
vaultTimeout = vaultTimeout.GetValueOrDefault(-1);
|
||||
if (vaultTimeout > 0)
|
||||
{
|
||||
_messagingService.Send("scheduleVaultTimeoutTimer", vaultTimeout.Value);
|
||||
}
|
||||
else if (vaultTimeout == 0)
|
||||
if (vaultTimeout == 0)
|
||||
{
|
||||
var action = await _storageService.GetAsync<string>(Constants.VaultTimeoutActionKey);
|
||||
if (action == "logOut")
|
||||
|
||||
@@ -1,114 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.CipherViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms">
|
||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.CipherViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
|
||||
<Grid
|
||||
x:Name="_grid"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:CipherViewCellViewModel">
|
||||
<Grid.Resources>
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
||||
<u:IconImageConverter x:Key="iconImageConverter"/>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid.BindingContext>
|
||||
<controls:CipherViewCellViewModel />
|
||||
</Grid.BindingContext>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:FaLabel
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<ff:CachedImage
|
||||
Grid.Column="0"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
LoadingPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
IsVisible="{Binding ShowIconImage}"
|
||||
Source="{Binding Cipher, Converter={StaticResource iconImageConverter}}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name}" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle}" />
|
||||
<controls:FaLabel
|
||||
x:Name="_icon"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<ff:CachedImage
|
||||
x:Name="_image"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
IsVisible="False"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name, Mode=OneWay}" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="3"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle, Mode=OneWay}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Shared}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
|
||||
AutomationProperties.Name="{u:I18n Shared}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.HasAttachments, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Attachments}" />
|
||||
</Grid>
|
||||
|
||||
</ViewCell>
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
|
||||
@@ -1,45 +1,26 @@
|
||||
using Bit.App.Pages;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class CipherViewCell : ViewCell
|
||||
public partial class CipherViewCell : ExtendedGrid
|
||||
{
|
||||
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
|
||||
nameof(Cipher), typeof(CipherView), typeof(CipherViewCell), default(CipherView), BindingMode.OneWay);
|
||||
|
||||
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
|
||||
nameof(WebsiteIconsEnabled), typeof(bool), typeof(CipherViewCell), true, BindingMode.OneWay);
|
||||
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(CipherViewCell));
|
||||
|
||||
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||
nameof(ButtonCommand), typeof(Command<CipherView>), typeof(CipherViewCell));
|
||||
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
private CipherViewCellViewModel _viewModel;
|
||||
private bool _usingNativeCell;
|
||||
|
||||
public CipherViewCell()
|
||||
{
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = _grid.BindingContext as CipherViewCellViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
_usingNativeCell = true;
|
||||
}
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public bool WebsiteIconsEnabled
|
||||
public bool? WebsiteIconsEnabled
|
||||
{
|
||||
get => (bool)GetValue(WebsiteIconsEnabledProperty);
|
||||
set => SetValue(WebsiteIconsEnabledProperty, value);
|
||||
@@ -60,130 +41,31 @@ namespace Bit.App.Controls
|
||||
protected override void OnPropertyChanged(string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
if (_usingNativeCell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (propertyName == CipherProperty.PropertyName)
|
||||
{
|
||||
_viewModel.Cipher = Cipher;
|
||||
if (Cipher == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BindingContext = new CipherViewCellViewModel(Cipher, WebsiteIconsEnabled ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBindingContextChanged()
|
||||
{
|
||||
base.OnBindingContextChanged();
|
||||
if (_usingNativeCell)
|
||||
else if (propertyName == WebsiteIconsEnabledProperty.PropertyName)
|
||||
{
|
||||
return;
|
||||
if (Cipher == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
((CipherViewCellViewModel)BindingContext).WebsiteIconsEnabled = WebsiteIconsEnabled ?? false;
|
||||
}
|
||||
|
||||
_image.Source = null;
|
||||
CipherView cipher = null;
|
||||
if (BindingContext is GroupingsPageListItem groupingsPageListItem)
|
||||
{
|
||||
cipher = groupingsPageListItem.Cipher;
|
||||
}
|
||||
else if (BindingContext is CipherView cv)
|
||||
{
|
||||
cipher = cv;
|
||||
}
|
||||
if (cipher != null)
|
||||
{
|
||||
var iconImage = GetIconImage(cipher);
|
||||
if (iconImage.Item2 != null)
|
||||
{
|
||||
_image.IsVisible = true;
|
||||
_icon.IsVisible = false;
|
||||
_image.Source = iconImage.Item2;
|
||||
_image.LoadingPlaceholder = "login.png";
|
||||
}
|
||||
else
|
||||
{
|
||||
_image.IsVisible = false;
|
||||
_icon.IsVisible = true;
|
||||
_icon.Text = iconImage.Item1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Tuple<string, string> GetIconImage(CipherView cipher)
|
||||
{
|
||||
string icon = null;
|
||||
string image = null;
|
||||
switch (cipher.Type)
|
||||
{
|
||||
case CipherType.Login:
|
||||
var loginIconImage = GetLoginIconImage(cipher);
|
||||
icon = loginIconImage.Item1;
|
||||
image = loginIconImage.Item2;
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
icon = "";
|
||||
break;
|
||||
case CipherType.Card:
|
||||
icon = "";
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
icon = "";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return new Tuple<string, string>(icon, image);
|
||||
}
|
||||
|
||||
private Tuple<string, string> GetLoginIconImage(CipherView cipher)
|
||||
{
|
||||
string icon = "";
|
||||
string image = null;
|
||||
if (cipher.Login.Uri != null)
|
||||
{
|
||||
var hostnameUri = cipher.Login.Uri;
|
||||
var isWebsite = false;
|
||||
|
||||
if (hostnameUri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
icon = "";
|
||||
}
|
||||
else if (hostnameUri.StartsWith(Constants.iOSAppProtocol))
|
||||
{
|
||||
icon = "";
|
||||
}
|
||||
else if (WebsiteIconsEnabled && !hostnameUri.Contains("://") && hostnameUri.Contains("."))
|
||||
{
|
||||
hostnameUri = string.Concat("http://", hostnameUri);
|
||||
isWebsite = true;
|
||||
}
|
||||
else if (WebsiteIconsEnabled)
|
||||
{
|
||||
isWebsite = hostnameUri.StartsWith("http") && hostnameUri.Contains(".");
|
||||
}
|
||||
|
||||
if (WebsiteIconsEnabled && isWebsite)
|
||||
{
|
||||
var hostname = CoreHelpers.GetHostname(hostnameUri);
|
||||
var iconsUrl = _environmentService.IconsUrl;
|
||||
if (string.IsNullOrWhiteSpace(iconsUrl))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||
{
|
||||
iconsUrl = string.Format("{0}/icons", _environmentService.BaseUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
iconsUrl = "https://icons.bitwarden.net";
|
||||
}
|
||||
}
|
||||
image = string.Format("{0}/{1}/icon.png", iconsUrl, hostname);
|
||||
}
|
||||
}
|
||||
return new Tuple<string, string>(icon, image);
|
||||
}
|
||||
|
||||
private void MoreButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
ButtonCommand?.Execute(Cipher);
|
||||
var cipher = ((sender as MiButton)?.BindingContext as CipherViewCellViewModel)?.Cipher;
|
||||
if (cipher != null)
|
||||
{
|
||||
ButtonCommand?.Execute(cipher);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,30 @@ namespace Bit.App.Controls
|
||||
public class CipherViewCellViewModel : ExtendedViewModel
|
||||
{
|
||||
private CipherView _cipher;
|
||||
private bool _websiteIconsEnabled;
|
||||
|
||||
public CipherViewCellViewModel(CipherView cipherView, bool websiteIconsEnabled)
|
||||
{
|
||||
Cipher = cipherView;
|
||||
WebsiteIconsEnabled = websiteIconsEnabled;
|
||||
}
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value);
|
||||
}
|
||||
|
||||
public bool WebsiteIconsEnabled
|
||||
{
|
||||
get => _websiteIconsEnabled;
|
||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||
}
|
||||
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled && !string.IsNullOrWhiteSpace(Cipher.Login?.Uri) &&
|
||||
Cipher.Login.Uri.StartsWith("http");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
src/App/Controls/ExtendedCollectionView.cs
Normal file
8
src/App/Controls/ExtendedCollectionView.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedCollectionView : CollectionView
|
||||
{
|
||||
}
|
||||
}
|
||||
8
src/App/Controls/ExtendedGrid.cs
Normal file
8
src/App/Controls/ExtendedGrid.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedGrid : Grid
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedListView : ListView
|
||||
{
|
||||
public ExtendedListView() { }
|
||||
|
||||
public ExtendedListView(ListViewCachingStrategy cachingStrategy)
|
||||
: base(cachingStrategy) { }
|
||||
}
|
||||
}
|
||||
8
src/App/Controls/ExtendedStackLayout.cs
Normal file
8
src/App/Controls/ExtendedStackLayout.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedStackLayout : StackLayout
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,137 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.SendViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities">
|
||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.SendViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:SendViewCellViewModel">
|
||||
|
||||
<Grid
|
||||
x:Name="_grid"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0"
|
||||
x:DataType="controls:SendViewCellViewModel">
|
||||
<Grid.Resources>
|
||||
<u:SendIconGlyphConverter x:Key="sendIconGlyphConverter"/>
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid.BindingContext>
|
||||
<controls:SendViewCellViewModel />
|
||||
</Grid.BindingContext>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:FaLabel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
Text="{Binding Send, Converter={StaticResource sendIconGlyphConverter}}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:FaLabel
|
||||
x:Name="_icon"
|
||||
Grid.Row="0"
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Send.Name, Mode=OneWay}" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="6"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Send.DisplayDate, Mode=OneWay}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.Disabled, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Disabled}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.HasPassword, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Password}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.Expired, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Expired}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="5"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.PendingDelete, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
||||
</Grid>
|
||||
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
IsVisible="{Binding ShowOptions, Mode=OneWay}"
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Send.Name}" />
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="6"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Send.DisplayDate}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.Disabled, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
|
||||
AutomationProperties.Name="{u:I18n Disabled}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.HasPassword, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Password}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.Expired, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Expired}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="5"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Send.PendingDelete, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n PendingDelete}" />
|
||||
</Grid>
|
||||
|
||||
</ViewCell>
|
||||
<controls:MiButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Text=""
|
||||
IsVisible="{Binding ShowOptions, Mode=OneWay}"
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="MoreButton_Clicked"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="EndAndExpand"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
|
||||
</controls:ExtendedGrid>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
using System;
|
||||
using Bit.App.Pages;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class SendViewCell : ViewCell
|
||||
public partial class SendViewCell : ExtendedGrid
|
||||
{
|
||||
public static readonly BindableProperty SendProperty = BindableProperty.Create(
|
||||
nameof(Send), typeof(SendView), typeof(SendViewCell), default(SendView), BindingMode.OneWay);
|
||||
@@ -17,25 +13,11 @@ namespace Bit.App.Controls
|
||||
nameof(ButtonCommand), typeof(Command<SendView>), typeof(SendViewCell));
|
||||
|
||||
public static readonly BindableProperty ShowOptionsProperty = BindableProperty.Create(
|
||||
nameof(ShowOptions), typeof(bool), typeof(SendViewCell));
|
||||
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
private SendViewCellViewModel _viewModel;
|
||||
private bool _usingNativeCell;
|
||||
nameof(ShowOptions), typeof(bool), typeof(SendViewCell), true, BindingMode.OneWay);
|
||||
|
||||
public SendViewCell()
|
||||
{
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = _grid.BindingContext as SendViewCellViewModel;
|
||||
}
|
||||
else
|
||||
{
|
||||
_usingNativeCell = true;
|
||||
}
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public SendView Send
|
||||
@@ -52,72 +34,30 @@ namespace Bit.App.Controls
|
||||
|
||||
public bool ShowOptions
|
||||
{
|
||||
get => GetValue(ShowOptionsProperty) is bool && (bool)GetValue(ShowOptionsProperty);
|
||||
get => (bool)GetValue(ShowOptionsProperty);
|
||||
set => SetValue(ShowOptionsProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(string propertyName = null)
|
||||
{
|
||||
base.OnPropertyChanged(propertyName);
|
||||
if (_usingNativeCell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (propertyName == SendProperty.PropertyName)
|
||||
{
|
||||
_viewModel.Send = Send;
|
||||
if (Send == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BindingContext = new SendViewCellViewModel(Send, ShowOptions);
|
||||
}
|
||||
else if (propertyName == ShowOptionsProperty.PropertyName)
|
||||
{
|
||||
_viewModel.ShowOptions = ShowOptions;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBindingContextChanged()
|
||||
{
|
||||
base.OnBindingContextChanged();
|
||||
if (_usingNativeCell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendView send = null;
|
||||
if (BindingContext is SendGroupingsPageListItem sendGroupingsPageListItem)
|
||||
{
|
||||
send = sendGroupingsPageListItem.Send;
|
||||
}
|
||||
else if (BindingContext is SendView sv)
|
||||
{
|
||||
send = sv;
|
||||
}
|
||||
if (send != null)
|
||||
{
|
||||
var iconImage = GetIconImage(send);
|
||||
_icon.IsVisible = true;
|
||||
_icon.Text = iconImage;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetIconImage(SendView send)
|
||||
{
|
||||
string icon = null;
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
icon = "\uf0f6"; // fa-file-text-o
|
||||
break;
|
||||
case SendType.File:
|
||||
icon = "\uf016"; // fa-file-o
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
private void MoreButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
ButtonCommand?.Execute(Send);
|
||||
var send = ((sender as MiButton)?.BindingContext as SendViewCellViewModel)?.Send;
|
||||
if (send != null)
|
||||
{
|
||||
ButtonCommand?.Execute(send);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace Bit.App.Controls
|
||||
private SendView _send;
|
||||
private bool _showOptions;
|
||||
|
||||
public SendViewCellViewModel(SendView sendView, bool showOptions)
|
||||
{
|
||||
Send = sendView;
|
||||
ShowOptions = showOptions;
|
||||
}
|
||||
|
||||
public SendView Send
|
||||
{
|
||||
get => _send;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Models.Request;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -37,7 +38,6 @@ namespace Bit.App.Pages
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
private int _invalidPinAttempts = 0;
|
||||
private Tuple<bool, bool> _pinSet;
|
||||
|
||||
public LockPageViewModel()
|
||||
@@ -208,6 +208,7 @@ namespace Bit.App.Pages
|
||||
if (!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
@@ -217,6 +218,7 @@ namespace Bit.App.Pages
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
failed = false;
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
@@ -226,8 +228,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (failed)
|
||||
{
|
||||
_invalidPinAttempts++;
|
||||
if (_invalidPinAttempts >= 5)
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
@@ -239,32 +241,31 @@ namespace Bit.App.Pages
|
||||
else
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
var passwordValid = false;
|
||||
if (keyHash != null)
|
||||
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash != null)
|
||||
passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
try
|
||||
{
|
||||
passwordValid = storedKeyHash == keyHash;
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
passwordValid = true;
|
||||
var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
||||
}
|
||||
else
|
||||
catch (Exception e)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
try
|
||||
{
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
passwordValid = true;
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
if (passwordValid)
|
||||
{
|
||||
@@ -278,6 +279,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutService.PinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
|
||||
// Re-enable biometrics
|
||||
@@ -288,6 +290,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
||||
if (invalidUnlockAttempts >= 5)
|
||||
{
|
||||
_messagingService.Send("logout");
|
||||
return;
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace Bit.App.Pages
|
||||
private readonly LoginPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private bool _inputFocused;
|
||||
|
||||
public LoginPage(string email = null, AppOptions appOptions = null)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
@@ -58,13 +60,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
if (string.IsNullOrWhiteSpace(_vm.Email))
|
||||
if (!_inputFocused)
|
||||
{
|
||||
RequestFocus(_email);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestFocus(_masterPassword);
|
||||
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
|
||||
_inputFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,17 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using Xamarin.Essentials;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginPageViewModel : BaseViewModel
|
||||
public class LoginPageViewModel : CaptchaProtectedViewModel
|
||||
{
|
||||
private const string Keys_RememberedEmail = "rememberedEmail";
|
||||
private const string Keys_RememberEmail = "rememberEmail";
|
||||
@@ -21,6 +27,8 @@ namespace Bit.App.Pages
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
private bool _showPassword;
|
||||
private string _email;
|
||||
@@ -34,6 +42,8 @@ namespace Bit.App.Pages
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -69,7 +79,12 @@ namespace Bit.App.Pages
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
@@ -80,7 +95,7 @@ namespace Bit.App.Pages
|
||||
RememberEmail = rememberEmail.GetValueOrDefault(true);
|
||||
}
|
||||
|
||||
public async Task LogInAsync()
|
||||
public async Task LogInAsync(bool showLoading = true)
|
||||
{
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
@@ -114,9 +129,12 @@ namespace Bit.App.Pages
|
||||
ShowPassword = false;
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
var response = await _authService.LogInAsync(Email, MasterPassword);
|
||||
MasterPassword = string.Empty;
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
}
|
||||
|
||||
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
||||
if (RememberEmail)
|
||||
{
|
||||
await _storageService.SaveAsync(Keys_RememberedEmail, Email);
|
||||
@@ -125,7 +143,22 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await _storageService.RemoveAsync(Keys_RememberedEmail);
|
||||
}
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
if (response.CaptchaNeeded)
|
||||
{
|
||||
if (await HandleCaptchaAsync(response.CaptchaSiteKey))
|
||||
{
|
||||
await LogInAsync(false);
|
||||
_captchaToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
_captchaToken = null;
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
@@ -140,6 +173,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_captchaToken = null;
|
||||
MasterPassword = string.Empty;
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
@@ -36,6 +35,10 @@
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Button Text="{u:I18n LogIn}"
|
||||
Clicked="LogIn_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
@@ -122,43 +123,28 @@ namespace Bit.App.Pages
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
bool cancelled = false;
|
||||
try
|
||||
{
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||
new Uri(redirectUri));
|
||||
}
|
||||
catch (TaskCanceledException taskCanceledException)
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, redirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
|
||||
// here we maintain the appearance of a clean cancellation (we don't want to do this across the board
|
||||
// because we still want to present legitimate errors). If/when this is fixed, we can remove this
|
||||
// particular catch block (catching taskCanceledException above must remain)
|
||||
// https://github.com/xamarin/Essentials/issues/1240
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
}
|
||||
if (!cancelled)
|
||||
{
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, redirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +168,7 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
if (RememberOrgIdentifier)
|
||||
{
|
||||
await _storageService.SaveAsync(Keys_RememberedOrgIdentifier, OrgIdentifier);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly RegisterPageViewModel _vm;
|
||||
|
||||
private bool _inputFocused;
|
||||
|
||||
public RegisterPage(HomePage homePage)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
@@ -45,7 +47,11 @@ namespace Bit.App.Pages
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
RequestFocus(_email);
|
||||
if (!_inputFocused)
|
||||
{
|
||||
RequestFocus(_email);
|
||||
_inputFocused = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
|
||||
@@ -12,9 +12,11 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class RegisterPageViewModel : BaseViewModel
|
||||
public class RegisterPageViewModel : CaptchaProtectedViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
@@ -27,6 +29,8 @@ namespace Bit.App.Pages
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
|
||||
PageTitle = AppResources.CreateAccount;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -76,7 +80,12 @@ namespace Bit.App.Pages
|
||||
public Action RegistrationSuccess { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
protected override II18nService i18nService => _i18nService;
|
||||
protected override IEnvironmentService environmentService => _environmentService;
|
||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||
protected override IPlatformUtilsService platformUtilsService => _platformUtilsService;
|
||||
|
||||
public async Task SubmitAsync(bool showLoading = true)
|
||||
{
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
@@ -123,6 +132,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
// TODO: Password strength check?
|
||||
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
}
|
||||
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? null : Name;
|
||||
Email = Email.Trim().ToLower();
|
||||
@@ -145,13 +159,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
}
|
||||
},
|
||||
CaptchaResponse = _captchaToken,
|
||||
};
|
||||
// TODO: org invite?
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
await _apiService.PostRegisterAsync(request);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.AccountCreated,
|
||||
@@ -163,6 +177,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
if (e?.Error != null && e.Error.CaptchaRequired)
|
||||
{
|
||||
if (await HandleCaptchaAsync(e.Error.CaptchaSiteKey))
|
||||
{
|
||||
await SubmitAsync(false);
|
||||
_captchaToken = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
|
||||
@@ -138,7 +138,8 @@ namespace Bit.App.Pages
|
||||
var kdfIterations = 100000;
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization);
|
||||
var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization);
|
||||
|
||||
Tuple<SymmetricCryptoKey, EncString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
@@ -174,7 +175,7 @@ namespace Bit.App.Pages
|
||||
await _userService.SetInformationAsync(await _userService.GetUserIdAsync(),
|
||||
await _userService.GetEmailAsync(), kdf, kdfIterations);
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetKeyHashAsync(masterPasswordHash);
|
||||
await _cryptoService.SetKeyHashAsync(localMasterPasswordHash);
|
||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
@@ -16,14 +16,21 @@
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_cancelItem" />
|
||||
<ToolbarItem Text="{u:I18n Continue}" Clicked="Continue_Clicked" Order="Primary"
|
||||
x:Name="_continueItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNullConverter x:Key="isNull" />
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Order="Secondary"
|
||||
x:Name="_useAnotherTwoStepMethod"
|
||||
x:Key="useAnotherTwoStepMethod" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -45,7 +52,8 @@
|
||||
Keyboard="Numeric"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
ReturnCommand="{Binding SubmitCommand}"
|
||||
TextChanged="Token_TextChanged"/>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
@@ -94,6 +102,30 @@
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20" Padding="0" IsVisible="{Binding Fido2Method, Mode=OneWay}">
|
||||
<Label
|
||||
Text="{u:I18n Fido2Instruction}"
|
||||
Margin="10, 20, 10, 0"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Image
|
||||
Source="yubikey.png"
|
||||
Margin="10, 0"
|
||||
WidthRequest="266"
|
||||
HeightRequest="160"
|
||||
HorizontalOptions="Center" />
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
StyleClass="box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Remember}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<controls:HybridWebView
|
||||
@@ -123,6 +155,12 @@
|
||||
Margin="10, 20, 10, 10"
|
||||
HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n Continue}"
|
||||
IsEnabled="{Binding EnableContinue}"
|
||||
IsVisible="{Binding ShowContinue}"
|
||||
Clicked="Continue_Clicked"
|
||||
Margin="10, 0"
|
||||
x:Name="_continue"></Button>
|
||||
<Button Text="{u:I18n SendVerificationCodeAgain}"
|
||||
IsVisible="{Binding EmailMethod}"
|
||||
Clicked="ResendEmail_Clicked"
|
||||
@@ -131,9 +169,6 @@
|
||||
IsVisible="{Binding ShowTryAgain}"
|
||||
Clicked="TryAgain_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
@@ -45,26 +46,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.Remove(_cancelItem);
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
} else
|
||||
{
|
||||
ToolbarItems.Add(_useAnotherTwoStepMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public HybridWebView DuoWebView { get; set; }
|
||||
|
||||
public void AddContinueButton()
|
||||
{
|
||||
if (!ToolbarItems.Contains(_continueItem))
|
||||
{
|
||||
ToolbarItems.Add(_continueItem);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveContinueButton()
|
||||
{
|
||||
if (ToolbarItems.Contains(_continueItem))
|
||||
{
|
||||
ToolbarItems.Remove(_continueItem);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
@@ -83,6 +75,18 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (message.Command == "gotFido2Token")
|
||||
{
|
||||
var token = (string)message.Data;
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
_vm.Token = token;
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (message.Command == "resumeYubiKey")
|
||||
{
|
||||
if (_vm.YubikeyMethod)
|
||||
@@ -145,6 +149,21 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
||||
|
||||
if (selection == AppResources.UseAnotherTwoStepMethod)
|
||||
{
|
||||
await _vm.AnotherMethodAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
@@ -161,11 +180,22 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAgain_Clicked(object sender, EventArgs e)
|
||||
private async void TryAgain_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (_vm.YubikeyMethod)
|
||||
if (_vm.Fido2Method)
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_messagingService.Send("listenFido2TryAgain", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _vm.Fido2AuthenticateAsync();
|
||||
}
|
||||
}
|
||||
else if (_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
}
|
||||
@@ -195,5 +225,10 @@ namespace Bit.App.Pages
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
||||
private void Token_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
_vm.EnableContinue = !string.IsNullOrWhiteSpace(e.NewTextValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,15 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -27,11 +33,12 @@ namespace Bit.App.Pages
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _u2fSupported = false;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _authingWithSso = false;
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
{
|
||||
@@ -63,6 +70,8 @@ namespace Bit.App.Pages
|
||||
public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo ||
|
||||
SelectedProviderType == TwoFactorProviderType.OrganizationDuo;
|
||||
|
||||
public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn;
|
||||
|
||||
public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey;
|
||||
|
||||
public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator;
|
||||
@@ -71,7 +80,19 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
|
||||
|
||||
public bool ShowTryAgain => YubikeyMethod && Device.RuntimePlatform == Device.iOS;
|
||||
public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method;
|
||||
|
||||
public bool ShowContinue
|
||||
{
|
||||
get => _showContinue;
|
||||
set => SetProperty(ref _showContinue, value);
|
||||
}
|
||||
|
||||
public bool EnableContinue
|
||||
{
|
||||
get => _enableContinue;
|
||||
set => SetProperty(ref _enableContinue, value);
|
||||
}
|
||||
|
||||
public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos :
|
||||
AppResources.YubiKeyInstruction;
|
||||
@@ -83,6 +104,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
nameof(EmailMethod),
|
||||
nameof(DuoMethod),
|
||||
nameof(Fido2Method),
|
||||
nameof(YubikeyMethod),
|
||||
nameof(AuthenticatorMethod),
|
||||
nameof(TotpMethod),
|
||||
@@ -114,10 +136,7 @@ namespace Bit.App.Pages
|
||||
_webVaultUrl = _environmentService.WebVaultUrl;
|
||||
}
|
||||
|
||||
// TODO: init U2F
|
||||
_u2fSupported = false;
|
||||
|
||||
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported);
|
||||
SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2());
|
||||
Load();
|
||||
}
|
||||
|
||||
@@ -133,8 +152,15 @@ namespace Bit.App.Pages
|
||||
var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value];
|
||||
switch (SelectedProviderType.Value)
|
||||
{
|
||||
case TwoFactorProviderType.U2f:
|
||||
// TODO
|
||||
case TwoFactorProviderType.Fido2WebAuthn:
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_messagingService.Send("listenFido2", providerData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Fido2AuthenticateAsync(providerData);
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
@@ -169,17 +195,73 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
}
|
||||
if (SelectedProviderType == null || DuoMethod)
|
||||
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
||||
}
|
||||
|
||||
public async Task Fido2AuthenticateAsync(Dictionary<string, object> providerData = null)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
|
||||
if (providerData == null)
|
||||
{
|
||||
page.RemoveContinueButton();
|
||||
providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn];
|
||||
}
|
||||
|
||||
var callbackUri = "bitwarden://webauthn-callback";
|
||||
var data = AppHelpers.EncodeDataParameter(new
|
||||
{
|
||||
callbackUri = callbackUri,
|
||||
data = JsonConvert.SerializeObject(providerData),
|
||||
btnText = AppResources.Fido2AuthenticateWebAuthn,
|
||||
});
|
||||
|
||||
var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data +
|
||||
"&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2";
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
try
|
||||
{
|
||||
var options = new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(callbackUri),
|
||||
PrefersEphemeralWebBrowserSession = true,
|
||||
};
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(options);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
string response = null;
|
||||
if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData))
|
||||
{
|
||||
response = Uri.UnescapeDataString(resultData);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(response))
|
||||
{
|
||||
Token = response;
|
||||
await SubmitAsync(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
page.AddContinueButton();
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(resultError, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.Fido2SomethingWentWrong,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
public async Task SubmitAsync(bool showLoading = true)
|
||||
{
|
||||
if (SelectedProviderType == null)
|
||||
{
|
||||
@@ -206,7 +288,10 @@ namespace Bit.App.Pages
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
if (showLoading)
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
}
|
||||
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
@@ -245,7 +330,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/lost-two-step-device/");
|
||||
}
|
||||
else if (method != AppResources.Cancel)
|
||||
else if (method != AppResources.Cancel && method != null)
|
||||
{
|
||||
var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type;
|
||||
if (selected == SelectedProviderType)
|
||||
|
||||
64
src/App/Pages/CaptchaProtectedViewModel.cs
Normal file
64
src/App/Pages/CaptchaProtectedViewModel.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public abstract class CaptchaProtectedViewModel : BaseViewModel
|
||||
{
|
||||
protected abstract II18nService i18nService { get; }
|
||||
protected abstract IEnvironmentService environmentService { get; }
|
||||
protected abstract IDeviceActionService deviceActionService { get; }
|
||||
protected abstract IPlatformUtilsService platformUtilsService { get; }
|
||||
protected string _captchaToken = null;
|
||||
|
||||
protected async Task<bool> HandleCaptchaAsync(string CaptchaSiteKey)
|
||||
{
|
||||
var callbackUri = "bitwarden://captcha-callback";
|
||||
var data = AppHelpers.EncodeDataParameter(new
|
||||
{
|
||||
siteKey = CaptchaSiteKey,
|
||||
locale = i18nService.Culture.TwoLetterISOLanguageName,
|
||||
callbackUri = callbackUri,
|
||||
captchaRequiredText = AppResources.CaptchaRequired,
|
||||
});
|
||||
|
||||
var url = environmentService.GetWebVaultUrl() + "/captcha-mobile-connector.html?" + "data=" + data +
|
||||
"&parent=" + Uri.EscapeDataString(callbackUri) + "&v=1";
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
bool cancelled = false;
|
||||
try
|
||||
{
|
||||
var options = new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(callbackUri),
|
||||
PrefersEphemeralWebBrowserSession = true,
|
||||
};
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(options);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
await deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
if (cancelled == false && authResult != null &&
|
||||
authResult.Properties.TryGetValue("token", out _captchaToken))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await platformUtilsService.ShowDialogAsync(AppResources.CaptchaFailed,
|
||||
AppResources.CaptchaRequired);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,57 +43,53 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
<ListView x:Name="_listView"
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="domain:GeneratedPasswordHistory">
|
||||
<ViewCell>
|
||||
<Grid
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
<Grid
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:MonoLabel LineBreakMode="CharacterWrap"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
TextType="Html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
<controls:MonoLabel LineBreakMode="CharacterWrap"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
TextType="Html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
Clicked="Select_Clicked"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNullConverter x:Key="null" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Key="closeItem" x:Name="_closeItem" />
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
@@ -137,8 +137,6 @@
|
||||
AutomationProperties.Name="{u:I18n File}"
|
||||
Grid.Column="0">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
|
||||
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal">
|
||||
<VisualState.Setters>
|
||||
@@ -163,8 +161,6 @@
|
||||
AutomationProperties.Name="{u:I18n Text}"
|
||||
Grid.Column="1">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<!-- Rider users, if the x:Name values below are red, it's a known issue: -->
|
||||
<!-- https://youtrack.jetbrains.com/issue/RSRP-479388 -->
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal">
|
||||
<VisualState.Setters>
|
||||
@@ -294,24 +290,23 @@
|
||||
x:Name="_btnOptions"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{StaticResource PrimaryColor}"
|
||||
Margin="0"
|
||||
Clicked="ToggleOptions_Clicked" />
|
||||
Margin="0" />
|
||||
<controls:FaButton
|
||||
x:Name="_btnOptionsUp"
|
||||
Text=""
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{StaticResource PrimaryColor}"
|
||||
Clicked="ToggleOptions_Clicked"
|
||||
IsVisible="{Binding ShowOptions}" />
|
||||
IsVisible="False" />
|
||||
<controls:FaButton
|
||||
x:Name="_btnOptionsDown"
|
||||
Text=""
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{StaticResource PrimaryColor}"
|
||||
Clicked="ToggleOptions_Clicked"
|
||||
IsVisible="{Binding ShowOptions, Converter={StaticResource inverseBool}}" />
|
||||
IsVisible="False" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding ShowOptions}">
|
||||
<StackLayout IsVisible="True">
|
||||
<StackLayout
|
||||
StyleClass="box-row"
|
||||
Margin="0,10,0,0">
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -16,6 +17,7 @@ namespace Bit.App.Pages
|
||||
public partial class SendAddEditPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private SendAddEditPageViewModel _vm;
|
||||
@@ -26,6 +28,7 @@ namespace Bit.App.Pages
|
||||
SendType? type = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SendAddEditPageViewModel;
|
||||
@@ -79,6 +82,14 @@ namespace Bit.App.Pages
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _vm.InitAsync();
|
||||
_broadcasterService.Subscribe(nameof(SendAddEditPage), message =>
|
||||
{
|
||||
@@ -109,6 +120,15 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
if (_vm.IsAddFromShare && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_appOptions.CreateSend = null;
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace Bit.App.Pages
|
||||
};
|
||||
private bool _disableHideEmail;
|
||||
private bool _sendOptionsPolicyInEffect;
|
||||
private bool _disableHideEmailControl;
|
||||
|
||||
public SendAddEditPageViewModel()
|
||||
{
|
||||
|
||||
@@ -44,34 +44,32 @@
|
||||
<controls:SendViewCell
|
||||
Send="{Binding Send}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding ShowOptions}" />
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="sendGroupTemplate"
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<ViewCell>
|
||||
<StackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform">
|
||||
<controls:FaLabel.Effects>
|
||||
<effects:FixedSizeEffect />
|
||||
</controls:FaLabel.Effects>
|
||||
</controls:FaLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform">
|
||||
<controls:FaLabel.Effects>
|
||||
<effects:FixedSizeEffect />
|
||||
</controls:FaLabel.Effects>
|
||||
</controls:FaLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title" />
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<pages:SendGroupingsPageListItemSelector x:Key="sendListItemDataTemplateSelector"
|
||||
@@ -103,32 +101,25 @@
|
||||
Text="{Binding NoDataText}"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<Button
|
||||
Text="{u:I18n AddAnItem}"
|
||||
Clicked="AddButton_Clicked"
|
||||
IsVisible="{Binding ShowAddSendButton}" />
|
||||
Text="{u:I18n AddASend}"
|
||||
Clicked="AddButton_Clicked" />
|
||||
</StackLayout>
|
||||
|
||||
<controls:ExtendedListView
|
||||
x:Name="_listView"
|
||||
<RefreshView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding GroupedSends}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="True"
|
||||
RowHeight="-1"
|
||||
RefreshCommand="{Binding RefreshCommand}"
|
||||
IsPullToRefreshEnabled="True"
|
||||
IsRefreshing="{Binding Refreshing}"
|
||||
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
ItemSelected="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<x:Arguments>
|
||||
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
|
||||
</x:Arguments>
|
||||
Command="{Binding RefreshCommand}">
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding GroupedSends}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
|
||||
IsGrouped="True"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:SendGroupingsPageListGroup">
|
||||
<ViewCell>
|
||||
<CollectionView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:SendGroupingsPageListGroup">
|
||||
<StackLayout
|
||||
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
@@ -146,10 +137,10 @@
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
</ListView.GroupHeaderTemplate>
|
||||
</controls:ExtendedListView>
|
||||
</DataTemplate>
|
||||
</CollectionView.GroupHeaderTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</RefreshView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -20,14 +20,13 @@ namespace Bit.App.Pages
|
||||
private readonly string _pageName;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private PreviousPageInfo _previousPage;
|
||||
|
||||
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
|
||||
AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
||||
AppOptions appOptions = null)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
_pageName = string.Concat(nameof(SendGroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
ListView = _listView;
|
||||
SetActivityIndicator(_mainContent);
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
@@ -37,7 +36,6 @@ namespace Bit.App.Pages
|
||||
_vm.MainPage = mainPage;
|
||||
_vm.Type = type;
|
||||
_appOptions = appOptions;
|
||||
_previousPage = previousPage;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
_vm.PageTitle = pageTitle;
|
||||
@@ -46,7 +44,10 @@ namespace Bit.App.Pages
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Add(_aboutIconItem);
|
||||
if (type == null)
|
||||
{
|
||||
ToolbarItems.Add(_aboutIconItem);
|
||||
}
|
||||
ToolbarItems.Add(_addItem);
|
||||
}
|
||||
else
|
||||
@@ -57,8 +58,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public ExtendedListView ListView { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
@@ -136,14 +135,14 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.SelectedItem is SendGroupingsPageListItem item))
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is SendGroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace Bit.App.Pages
|
||||
public SendGroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||
: this(new List<SendGroupingsPageListItem>(), name, count, doUpper, first) { }
|
||||
|
||||
public SendGroupingsPageListGroup(List<SendGroupingsPageListItem> groupItems, string name, int count,
|
||||
public SendGroupingsPageListGroup(List<SendGroupingsPageListItem> sendGroupItems, string name, int count,
|
||||
bool doUpper = true, bool first = false)
|
||||
{
|
||||
AddRange(groupItems);
|
||||
AddRange(sendGroupItems);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Name = "-";
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Bit.App.Pages
|
||||
private bool _doingLoad;
|
||||
private bool _loading;
|
||||
private bool _loaded;
|
||||
private bool _showAddSendButton;
|
||||
private bool _showNoData;
|
||||
private bool _showList;
|
||||
private bool _syncRefreshing;
|
||||
@@ -91,11 +90,6 @@ namespace Bit.App.Pages
|
||||
get => _loaded;
|
||||
set => SetProperty(ref _loaded, value);
|
||||
}
|
||||
public bool ShowAddSendButton
|
||||
{
|
||||
get => _showAddSendButton;
|
||||
set => SetProperty(ref _showAddSendButton, value);
|
||||
}
|
||||
public bool ShowNoData
|
||||
{
|
||||
get => _showNoData;
|
||||
|
||||
@@ -60,23 +60,22 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<ListView x:Name="_listView"
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Sends}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
ItemSelected="RowSelected"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:SendView">
|
||||
<controls:SendViewCell
|
||||
Send="{Binding .}"
|
||||
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}"
|
||||
ShowOptions="{Binding BindingContext.SendEnabled, Source={x:Reference _page}}" />
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Models.View;
|
||||
using Xamarin.Forms;
|
||||
@@ -87,15 +89,15 @@ namespace Bit.App.Pages
|
||||
Navigation.PopModalAsync(false);
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.SelectedItem is SendView send)
|
||||
if (e.CurrentSelection?.FirstOrDefault() is SendView send)
|
||||
{
|
||||
await _vm.SelectSendAsync(send);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
|
||||
@@ -103,11 +103,9 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(_masterPassword, null);
|
||||
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(_masterPassword, null);
|
||||
MasterPassword = string.Empty;
|
||||
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash == null || keyHash == null || storedKeyHash != keyHash)
|
||||
if (!passwordValid)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword"));
|
||||
return;
|
||||
@@ -128,7 +126,7 @@ namespace Bit.App.Pages
|
||||
fileFormat = fileFormat == "encrypted_json" ? "json" : fileFormat;
|
||||
|
||||
_defaultFilename = _exportService.GetFileName(null, fileFormat);
|
||||
_exportResult = Encoding.ASCII.GetBytes(data);
|
||||
_exportResult = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
if (!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" />
|
||||
<ToolbarItem Text="{u:I18n Delete}"
|
||||
Clicked="Delete_Clicked"
|
||||
|
||||
@@ -32,27 +32,25 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
<ListView x:Name="_listView"
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding Folders}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
CachingStrategy="RecycleElement"
|
||||
ItemSelected="RowSelected"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:FolderView">
|
||||
<ViewCell>
|
||||
<StackLayout
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10">
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
<controls:ExtendedStackLayout
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10">
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Name, Mode=OneWay}" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.Models.View;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -32,14 +34,14 @@ namespace Bit.App.Pages
|
||||
}, _mainContent);
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.SelectedItem is FolderView folder))
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is FolderView folder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,23 +19,21 @@
|
||||
<DataTemplate
|
||||
x:Key="regularTemplate"
|
||||
x:DataType="pages:SettingsPageListItem">
|
||||
<ViewCell>
|
||||
<StackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding SubLabel, Mode=OneWay}"
|
||||
IsVisible="{Binding SubLabel, Converter={StaticResource stringHasValue}}"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding SubLabel, Mode=OneWay}"
|
||||
IsVisible="{Binding SubLabel, Converter={StaticResource stringHasValue}}"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
TextColor="{Binding SubLabelColor}"
|
||||
StyleClass="list-sub" />
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<pages:SettingsPageListItemSelector
|
||||
@@ -44,35 +42,32 @@
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ListView
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="True"
|
||||
RowHeight="-1"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
ItemSelected="RowSelected"
|
||||
IsGrouped="True"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<CollectionView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:SettingsPageListGroup">
|
||||
<ViewCell>
|
||||
<StackLayout
|
||||
Padding="0" Spacing="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform"
|
||||
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Name}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
<StackLayout
|
||||
Padding="0" Spacing="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform"
|
||||
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Name}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</ListView.GroupHeaderTemplate>
|
||||
</ListView>
|
||||
</CollectionView.GroupHeaderTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -41,14 +43,14 @@ namespace Bit.App.Pages
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.SelectedItem is SettingsPageListItem item))
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +103,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage()));
|
||||
}
|
||||
else if (item.Name == AppResources.ShareVault)
|
||||
else if (item.Name == AppResources.LearnOrg)
|
||||
{
|
||||
await _vm.ShareAsync();
|
||||
}
|
||||
|
||||
@@ -150,8 +150,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task ShareAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ShareVaultConfirmation,
|
||||
AppResources.ShareVault, AppResources.Yes, AppResources.Cancel);
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LearnOrgConfirmation,
|
||||
AppResources.LearnOrg, AppResources.Yes, AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/what-is-an-organization/");
|
||||
@@ -379,17 +379,23 @@ namespace Bit.App.Pages
|
||||
}
|
||||
var accountItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.ChangeMasterPassword },
|
||||
new SettingsPageListItem { Name = AppResources.FingerprintPhrase },
|
||||
new SettingsPageListItem { Name = AppResources.LogOut }
|
||||
};
|
||||
if (IncludeLinksWithSubscriptionInfo())
|
||||
{
|
||||
accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword });
|
||||
}
|
||||
var toolsItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.ImportItems },
|
||||
new SettingsPageListItem { Name = AppResources.ExportVault },
|
||||
new SettingsPageListItem { Name = AppResources.ShareVault },
|
||||
new SettingsPageListItem { Name = AppResources.WebVault }
|
||||
new SettingsPageListItem { Name = AppResources.ExportVault }
|
||||
};
|
||||
if (IncludeLinksWithSubscriptionInfo())
|
||||
{
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg });
|
||||
toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault });
|
||||
}
|
||||
var otherItems = new List<SettingsPageListItem>
|
||||
{
|
||||
new SettingsPageListItem { Name = AppResources.Options },
|
||||
@@ -408,6 +414,15 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
private bool IncludeLinksWithSubscriptionInfo()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string GetVaultTimeoutActionFromKey(string key)
|
||||
{
|
||||
return _vaultTimeoutActions.FirstOrDefault(o => o.Key == key).Value;
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:StringHasValueConverter x:Key="stringHasValue" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Key="closeItem" x:Name="_closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Collections}"
|
||||
x:Key="collectionsItem"
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n Share}"
|
||||
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
@@ -219,15 +219,40 @@
|
||||
Text="{Binding Cipher.Card.CardholderName}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n Number}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_cardNumberEntry"
|
||||
Text="{Binding Cipher.Card.Number}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Keyboard="Numeric"
|
||||
IsPassword="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
Command="{Binding ToggleCardNumberCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n Brand}"
|
||||
@@ -530,6 +555,24 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n PasswordPrompt}"
|
||||
StyleClass="box-label-regular" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding PasswordPromptHelpCommand}"
|
||||
TextColor="{StaticResource MutedColor}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding PasswordPrompt}"
|
||||
Toggled="PasswordPrompt_Toggled"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
@@ -667,7 +710,10 @@
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}">
|
||||
<controls:RepeaterView
|
||||
x:Name="_collectionsRepeaterView"
|
||||
ItemsSource="{Binding Collections}"
|
||||
IsVisible="{Binding HasCollections}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:CollectionViewModel">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Bit.App.Pages
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
|
||||
private AddEditPageViewModel _vm;
|
||||
private bool _fromAutofill;
|
||||
@@ -38,6 +39,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_appOptions = appOptions;
|
||||
_fromAutofill = fromAutofill;
|
||||
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
|
||||
@@ -47,6 +49,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.CollectionsRepeaterView = _collectionsRepeaterView;
|
||||
_vm.Type = type;
|
||||
_vm.DefaultName = name ?? appOptions?.SaveName;
|
||||
_vm.DefaultUri = uri ?? appOptions?.Uri;
|
||||
@@ -144,6 +147,14 @@ namespace Bit.App.Pages
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await LoadOnAppearedAsync(_scrollView, true, async () =>
|
||||
{
|
||||
var success = await _vm.LoadAsync(_appOptions);
|
||||
@@ -158,6 +169,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
RequestFocus(_nameEntry);
|
||||
}
|
||||
_scrollView.Scrolled += (sender, args) => _vm.HandleScroll();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,7 +284,7 @@ namespace Bit.App.Pages
|
||||
var options = new List<string> { AppResources.Attachments };
|
||||
if (_vm.EditMode)
|
||||
{
|
||||
options.Add(_vm.Cipher.OrganizationId == null ? AppResources.Share : AppResources.Collections);
|
||||
options.Add(_vm.Cipher.OrganizationId == null ? AppResources.MoveToOrganization : AppResources.Collections);
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
(_vm.EditMode && !_vm.CloneMode) ? AppResources.Delete : null, options.ToArray());
|
||||
@@ -293,7 +305,7 @@ namespace Bit.App.Pages
|
||||
var page = new CollectionsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
else if (selection == AppResources.Share)
|
||||
else if (selection == AppResources.MoveToOrganization)
|
||||
{
|
||||
var page = new SharePage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
@@ -376,5 +388,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PasswordPrompt_Toggled(object sender, ToggledEventArgs e)
|
||||
{
|
||||
_vm.Cipher.Reprompt = e.Value ? CipherRepromptType.Password : CipherRepromptType.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -9,6 +10,7 @@ using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
using View = Xamarin.Forms.View;
|
||||
|
||||
@@ -29,6 +31,7 @@ namespace Bit.App.Pages
|
||||
private CipherView _cipher;
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
private bool _showCardCode;
|
||||
private int _typeSelectedIndex;
|
||||
private int _cardBrandSelectedIndex;
|
||||
@@ -38,6 +41,7 @@ namespace Bit.App.Pages
|
||||
private int _ownershipSelectedIndex;
|
||||
private bool _hasCollections;
|
||||
private string _previousCipherId;
|
||||
private DateTime _lastHandledScrollTime;
|
||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||
private string[] _additionalCipherProperties = new string[]
|
||||
{
|
||||
@@ -82,10 +86,12 @@ namespace Bit.App.Pages
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
||||
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
||||
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
|
||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
@@ -141,10 +147,12 @@ namespace Bit.App.Pages
|
||||
|
||||
public Command GeneratePasswordCommand { get; set; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
public Command ToggleCardCodeCommand { get; set; }
|
||||
public Command CheckPasswordCommand { get; set; }
|
||||
public Command UriOptionsCommand { get; set; }
|
||||
public Command FieldOptionsCommand { get; set; }
|
||||
public Command PasswordPromptHelpCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
@@ -161,6 +169,7 @@ namespace Bit.App.Pages
|
||||
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
|
||||
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
|
||||
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
||||
public RepeaterView CollectionsRepeaterView { get; set; }
|
||||
public int TypeSelectedIndex
|
||||
{
|
||||
get => _typeSelectedIndex;
|
||||
@@ -246,6 +255,15 @@ namespace Bit.App.Pages
|
||||
nameof(ShowPasswordIcon)
|
||||
});
|
||||
}
|
||||
public bool ShowCardNumber
|
||||
{
|
||||
get => _showCardNumber;
|
||||
set => SetProperty(ref _showCardNumber, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowCardNumberIcon)
|
||||
});
|
||||
}
|
||||
public bool ShowCardCode
|
||||
{
|
||||
get => _showCardCode;
|
||||
@@ -277,10 +295,12 @@ namespace Bit.App.Pages
|
||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||
public bool ShowAttachments => Cipher.HasAttachments;
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string ShowCardNumberIcon => ShowCardNumber ? "" : "";
|
||||
public string ShowCardCodeIcon => ShowCardCode ? "" : "";
|
||||
public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4;
|
||||
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
|
||||
public bool AllowPersonal { get; set; }
|
||||
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
@@ -691,6 +711,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleCardNumber()
|
||||
{
|
||||
ShowCardNumber = !ShowCardNumber;
|
||||
if (EditMode && ShowCardNumber)
|
||||
{
|
||||
var task = _eventService.CollectAsync(
|
||||
Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleCardCode()
|
||||
{
|
||||
ShowCardCode = !ShowCardCode;
|
||||
@@ -717,6 +747,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public void PasswordPromptHelp()
|
||||
{
|
||||
_platformUtilsService.LaunchUri("https://bitwarden.com/help/article/managing-items/#protect-individual-items");
|
||||
}
|
||||
|
||||
private void TypeChanged()
|
||||
{
|
||||
if (Cipher != null && TypeSelectedIndex > -1)
|
||||
@@ -769,13 +804,30 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var cols = _writeableCollections.Where(c => c.OrganizationId == Cipher.OrganizationId)
|
||||
.Select(c => new CollectionViewModel { Collection = c }).ToList();
|
||||
HasCollections = cols.Any();
|
||||
Collections.ResetWithRange(cols);
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>(cols);
|
||||
}
|
||||
else
|
||||
{
|
||||
HasCollections = false;
|
||||
Collections.ResetWithRange(new List<CollectionViewModel>());
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>(new List<CollectionViewModel>());
|
||||
}
|
||||
HasCollections = Collections.Any();
|
||||
}
|
||||
|
||||
public void HandleScroll()
|
||||
{
|
||||
// workaround for https://github.com/xamarin/Xamarin.Forms/issues/13607
|
||||
// required for org ownership/collections to render properly in XF4.5+
|
||||
if (!HasCollections ||
|
||||
EditMode ||
|
||||
(DateTime.Now - _lastHandledScrollTime < TimeSpan.FromMilliseconds(200)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
CollectionsRepeaterView.ItemsSource = Collections;
|
||||
_lastHandledScrollTime = DateTime.Now;
|
||||
}
|
||||
|
||||
private void TriggerCipherChanged()
|
||||
|
||||
@@ -47,40 +47,35 @@
|
||||
Clicked="AddButton_Clicked"></Button>
|
||||
</StackLayout>
|
||||
|
||||
<controls:ExtendedListView
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
ItemSelected="RowSelected"
|
||||
IsGrouped="True"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<x:Arguments>
|
||||
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
|
||||
</x:Arguments>
|
||||
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<CollectionView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:GroupingsPageListGroup">
|
||||
<ViewCell>
|
||||
<StackLayout
|
||||
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Name}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
<Label
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
<BoxView
|
||||
StyleClass="list-section-separator-top, list-section-separator-top-platform" />
|
||||
<StackLayout StyleClass="list-row-header, list-row-header-platform">
|
||||
<Label
|
||||
Text="{Binding Name}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
<Label
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
</StackLayout>
|
||||
</DataTemplate>
|
||||
</ListView.GroupHeaderTemplate>
|
||||
</controls:ExtendedListView>
|
||||
</CollectionView.GroupHeaderTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -4,7 +4,10 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -13,6 +16,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
|
||||
private AutofillCiphersPageViewModel _vm;
|
||||
|
||||
@@ -25,11 +29,20 @@ namespace Bit.App.Pages
|
||||
_vm.Init(appOptions);
|
||||
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (!await AppHelpers.IsVaultTimeoutImmediateAsync())
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
}
|
||||
if (await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await LoadOnAppearedAsync(_mainLayout, false, async () =>
|
||||
{
|
||||
try
|
||||
@@ -44,14 +57,23 @@ namespace Bit.App.Pages
|
||||
}, _mainContent);
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
protected override bool OnBackButtonPressed()
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
_appOptions.Uri = null;
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (e.SelectedItem is GroupingsPageListItem item && item.Cipher != null)
|
||||
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item && item.Cipher != null)
|
||||
{
|
||||
await _vm.SelectCipherAsync(item.Cipher, item.FuzzyAutofill);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Bit.App.Pages
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
|
||||
private AppOptions _appOptions;
|
||||
private bool _showList;
|
||||
@@ -35,6 +36,7 @@ namespace Bit.App.Pages
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
|
||||
GroupedItems = new ExtendedObservableCollection<GroupingsPageListGroup>();
|
||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||
@@ -118,10 +120,14 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_deviceActionService.SystemMajorVersion() < 21)
|
||||
{
|
||||
await AppHelpers.CipherListOptions(Page, cipher);
|
||||
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var autofillResponse = AppResources.Yes;
|
||||
if (fuzzy)
|
||||
{
|
||||
@@ -175,7 +181,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
await AppHelpers.CipherListOptions(Page, cipher);
|
||||
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,15 +60,14 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<ListView x:Name="_listView"
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Ciphers}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
ItemSelected="RowSelected"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:CipherView">
|
||||
<controls:CipherViewCell
|
||||
Cipher="{Binding .}"
|
||||
@@ -76,8 +75,8 @@
|
||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -3,6 +3,8 @@ using Bit.App.Resources;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -119,15 +121,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.SelectedItem is CipherView cipher)
|
||||
if (e.CurrentSelection?.FirstOrDefault() is CipherView cipher)
|
||||
{
|
||||
await _vm.SelectCipherAsync(cipher);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Bit.App.Pages
|
||||
private readonly ISearchService _searchService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private CancellationTokenSource _searchCancellationTokenSource;
|
||||
|
||||
private bool _showNoData;
|
||||
@@ -35,6 +36,7 @@ namespace Bit.App.Pages
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
|
||||
Ciphers = new ExtendedObservableCollection<CipherView>();
|
||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||
@@ -182,7 +184,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_deviceActionService.SystemMajorVersion() < 21)
|
||||
{
|
||||
await Utilities.AppHelpers.CipherListOptions(Page, cipher);
|
||||
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -195,7 +197,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
await Utilities.AppHelpers.CipherListOptions(Page, cipher);
|
||||
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?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.GroupingsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:DataType="pages:GroupingsPageViewModel"
|
||||
Title="{Binding PageTitle}"
|
||||
x:Name="_page">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.GroupingsPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:effects="clr-namespace:Bit.App.Effects"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
x:DataType="pages:GroupingsPageViewModel"
|
||||
Title="{Binding PageTitle}"
|
||||
x:Name="_page">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:GroupingsPageViewModel />
|
||||
@@ -45,29 +45,27 @@
|
||||
|
||||
<DataTemplate x:Key="groupTemplate"
|
||||
x:DataType="pages:GroupingsPageListItem">
|
||||
<ViewCell>
|
||||
<StackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform">
|
||||
<controls:FaLabel.Effects>
|
||||
<effects:FixedSizeEffect />
|
||||
</controls:FaLabel.Effects>
|
||||
</controls:FaLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub"/>
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform">
|
||||
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform">
|
||||
<controls:FaLabel.Effects>
|
||||
<effects:FixedSizeEffect />
|
||||
</controls:FaLabel.Effects>
|
||||
</controls:FaLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="End"
|
||||
StyleClass="list-sub"/>
|
||||
</controls:ExtendedStackLayout>
|
||||
</DataTemplate>
|
||||
|
||||
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
||||
@@ -89,27 +87,21 @@
|
||||
IsVisible="{Binding ShowAddCipherButton}"></Button>
|
||||
</StackLayout>
|
||||
|
||||
<controls:ExtendedListView
|
||||
x:Name="_listView"
|
||||
<RefreshView
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="True"
|
||||
RowHeight="-1"
|
||||
RefreshCommand="{Binding RefreshCommand}"
|
||||
IsPullToRefreshEnabled="True"
|
||||
IsRefreshing="{Binding Refreshing}"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
ItemSelected="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<x:Arguments>
|
||||
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
|
||||
</x:Arguments>
|
||||
Command="{Binding RefreshCommand}">
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGrouped="True"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:GroupingsPageListGroup">
|
||||
<ViewCell>
|
||||
<CollectionView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:GroupingsPageListGroup">
|
||||
<StackLayout
|
||||
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
|
||||
StyleClass="list-row-header-container, list-row-header-container-platform">
|
||||
@@ -126,10 +118,10 @@
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
</ListView.GroupHeaderTemplate>
|
||||
</controls:ExtendedListView>
|
||||
</DataTemplate>
|
||||
</CollectionView.GroupHeaderTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</RefreshView>
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
@@ -9,6 +8,7 @@ using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -33,7 +33,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
ListView = _listView;
|
||||
SetActivityIndicator(_mainContent);
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
@@ -73,8 +72,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public ExtendedListView ListView { get; set; }
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
@@ -197,14 +194,14 @@ namespace Bit.App.Pages
|
||||
_vm.DisableRefreshing();
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ListView)sender).SelectedItem = null;
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.SelectedItem is GroupingsPageListItem item))
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,30 +75,30 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (Folder != null)
|
||||
{
|
||||
_icon = Folder.Id == null ? "" : "";
|
||||
_icon = Folder.Id == null ? "\uf115" : "\uf07c"; // fa-folder-open-o : fa-folder-open
|
||||
}
|
||||
else if (Collection != null)
|
||||
{
|
||||
_icon = "";
|
||||
_icon = "\uf1b2"; // fa-cube
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
{
|
||||
case CipherType.Login:
|
||||
_icon = "";
|
||||
_icon = "\uf0ac"; // fa-globe
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
_icon = "";
|
||||
_icon = "\uf24a"; // fa-sticky-note-o
|
||||
break;
|
||||
case CipherType.Card:
|
||||
_icon = "";
|
||||
_icon = "\uf09d"; // fa-credit-card
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
_icon = "";
|
||||
_icon = "\uf2c3"; // fa-id-card-o
|
||||
break;
|
||||
default:
|
||||
_icon = "";
|
||||
_icon = "\uf0ac"; // fa-globe
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
|
||||
public GroupingsPageViewModel()
|
||||
{
|
||||
@@ -60,6 +61,7 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
|
||||
Loading = true;
|
||||
PageTitle = AppResources.MyVault;
|
||||
@@ -514,7 +516,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if ((Page as BaseContentPage).DoOnce())
|
||||
{
|
||||
await AppHelpers.CipherListOptions(Page, cipher);
|
||||
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,57 +34,53 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
<ListView x:Name="_listView"
|
||||
<controls:ExtendedCollectionView
|
||||
IsVisible="{Binding ShowNoData, Converter={StaticResource inverseBool}}"
|
||||
ItemsSource="{Binding History}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:PasswordHistoryView">
|
||||
<ViewCell>
|
||||
<Grid
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
<Grid
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Padding="10"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="10">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:MonoLabel LineBreakMode="CharacterWrap"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
TextType="Html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding LastUsedDate, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
<controls:MonoLabel LineBreakMode="CharacterWrap"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
TextType="Html"
|
||||
Text="{Binding Password, Mode=OneWay, Converter={StaticResource coloredPassword}}" />
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding LastUsedDate, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</CollectionView.ItemTemplate>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Title="{u:I18n ScanQrTitle}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<Grid
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Move}" Clicked="Save_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -46,7 +46,7 @@
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n ShareDesc}"
|
||||
Text="{u:I18n MoveToOrgDesc}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box"
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Bit.App.Pages
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
OrganizationOptions = new List<KeyValuePair<string, string>>();
|
||||
PageTitle = AppResources.Share;
|
||||
PageTitle = AppResources.MoveToOrganization;
|
||||
}
|
||||
|
||||
public string CipherId { get; set; }
|
||||
@@ -109,7 +109,9 @@ namespace Bit.App.Pages
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
await _cipherService.ShareWithServerAsync(cipherView, OrganizationId, checkedCollectionIds);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemShared);
|
||||
var movedItemToOrgText = string.Format(AppResources.MovedItemToOrg, cipherView.Name,
|
||||
(await _userService.GetOrganizationAsync(OrganizationId)).Name);
|
||||
_platformUtilsService.ShowToast("success", null, movedItemToOrgText);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n Share}"
|
||||
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
@@ -224,24 +224,41 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n Number}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<Label
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.MaskedNumber, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber, Converter={StaticResource inverseBool}}" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding Cipher.Card.Number, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
Command="{Binding ToggleCardNumberCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="CardNumber"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyNumber}" />
|
||||
|
||||
@@ -128,6 +128,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
|
||||
}
|
||||
}
|
||||
@@ -142,6 +146,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new AttachmentsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
@@ -151,6 +159,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new SharePage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
@@ -160,6 +172,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (await _vm.DeleteAsync())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
@@ -171,6 +187,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new CollectionsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
@@ -180,6 +200,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
@@ -196,7 +220,7 @@ namespace Bit.App.Pages
|
||||
if (_vm.Cipher.OrganizationId == null)
|
||||
{
|
||||
options.Add(AppResources.Clone);
|
||||
options.Add(AppResources.Share);
|
||||
options.Add(AppResources.MoveToOrganization);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -205,6 +229,12 @@ namespace Bit.App.Pages
|
||||
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
AppResources.Delete, options.ToArray());
|
||||
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection == AppResources.Delete)
|
||||
{
|
||||
if (await _vm.DeleteAsync())
|
||||
@@ -222,7 +252,7 @@ namespace Bit.App.Pages
|
||||
var page = new CollectionsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
else if (selection == AppResources.Share)
|
||||
else if (selection == AppResources.MoveToOrganization)
|
||||
{
|
||||
var page = new SharePage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -23,10 +24,12 @@ namespace Bit.App.Pages
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private CipherView _cipher;
|
||||
private List<ViewPageFieldViewModel> _fields;
|
||||
private bool _canAccessPremium;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
private bool _showCardCode;
|
||||
private string _totpCode;
|
||||
private string _totpCodeFormatted;
|
||||
@@ -36,6 +39,7 @@ namespace Bit.App.Pages
|
||||
private string _previousCipherId;
|
||||
private byte[] _attachmentData;
|
||||
private string _attachmentFilename;
|
||||
private bool _passwordReprompted;
|
||||
|
||||
public ViewPageViewModel()
|
||||
{
|
||||
@@ -47,11 +51,13 @@ namespace Bit.App.Pages
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
|
||||
CopyUriCommand = new Command<LoginUriView>(CopyUri);
|
||||
CopyFieldCommand = new Command<FieldView>(CopyField);
|
||||
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
||||
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
|
||||
@@ -64,6 +70,7 @@ namespace Bit.App.Pages
|
||||
public Command CopyFieldCommand { get; set; }
|
||||
public Command LaunchUriCommand { get; set; }
|
||||
public Command TogglePasswordCommand { get; set; }
|
||||
public Command ToggleCardNumberCommand { get; set; }
|
||||
public Command ToggleCardCodeCommand { get; set; }
|
||||
public Command CheckPasswordCommand { get; set; }
|
||||
public Command DownloadAttachmentCommand { get; set; }
|
||||
@@ -109,6 +116,15 @@ namespace Bit.App.Pages
|
||||
nameof(ShowPasswordIcon)
|
||||
});
|
||||
}
|
||||
public bool ShowCardNumber
|
||||
{
|
||||
get => _showCardNumber;
|
||||
set => SetProperty(ref _showCardNumber, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowCardNumberIcon)
|
||||
});
|
||||
}
|
||||
public bool ShowCardCode
|
||||
{
|
||||
get => _showCardCode;
|
||||
@@ -188,6 +204,7 @@ namespace Bit.App.Pages
|
||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string ShowCardNumberIcon => ShowCardNumber ? "" : "";
|
||||
public string ShowCardCodeIcon => ShowCardCode ? "" : "";
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
@@ -226,7 +243,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
Cipher = await cipher.DecryptAsync();
|
||||
CanAccessPremium = await _userService.CanAccessPremiumAsync();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(Cipher, f)).ToList();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
|
||||
|
||||
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||
@@ -259,8 +276,13 @@ namespace Bit.App.Pages
|
||||
_totpInterval = null;
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
public async void TogglePassword()
|
||||
{
|
||||
if (! await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ShowPassword = !ShowPassword;
|
||||
if (ShowPassword)
|
||||
{
|
||||
@@ -268,8 +290,26 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleCardCode()
|
||||
public async void ToggleCardNumber()
|
||||
{
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowCardNumber = !ShowCardNumber;
|
||||
if (ShowCardNumber)
|
||||
{
|
||||
var task = _eventService.CollectAsync(
|
||||
Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public async void ToggleCardCode()
|
||||
{
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowCardCode = !ShowCardCode;
|
||||
if (ShowCardCode)
|
||||
{
|
||||
@@ -474,6 +514,11 @@ namespace Bit.App.Pages
|
||||
canOpenFile = false;
|
||||
}
|
||||
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
try
|
||||
{
|
||||
@@ -564,6 +609,11 @@ namespace Bit.App.Pages
|
||||
|
||||
private async void CopyAsync(string id, string text = null)
|
||||
{
|
||||
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string name = null;
|
||||
if (id == "LoginUsername")
|
||||
{
|
||||
@@ -638,16 +688,28 @@ namespace Bit.App.Pages
|
||||
_platformUtilsService.LaunchUri(uri.LaunchUri);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<bool> PromptPasswordAsync()
|
||||
{
|
||||
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ViewPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private ViewPageViewModel _vm;
|
||||
private FieldView _field;
|
||||
private CipherView _cipher;
|
||||
private bool _showHiddenValue;
|
||||
|
||||
public ViewPageFieldViewModel(CipherView cipher, FieldView field)
|
||||
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
|
||||
{
|
||||
_vm = vm;
|
||||
_cipher = cipher;
|
||||
Field = field;
|
||||
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
|
||||
@@ -688,8 +750,12 @@ namespace Bit.App.Pages
|
||||
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
|
||||
!string.IsNullOrWhiteSpace(_field.Value) && !(IsHiddenType && !_cipher.ViewPassword);
|
||||
|
||||
public void ToggleHiddenValue()
|
||||
public async void ToggleHiddenValue()
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowHiddenValue = !ShowHiddenValue;
|
||||
if (ShowHiddenValue)
|
||||
{
|
||||
|
||||
146
src/App/Resources/AppResources.Designer.cs
generated
146
src/App/Resources/AppResources.Designer.cs
generated
@@ -1,7 +1,6 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
@@ -10,7 +9,6 @@
|
||||
|
||||
namespace Bit.App.Resources {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
@@ -365,6 +363,12 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string Move {
|
||||
get {
|
||||
return ResourceManager.GetString("Move", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Saving {
|
||||
get {
|
||||
return ResourceManager.GetString("Saving", resourceCulture);
|
||||
@@ -1259,9 +1263,9 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string ShareVault {
|
||||
public static string LearnOrg {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareVault", resourceCulture);
|
||||
return ResourceManager.GetString("LearnOrg", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2399,6 +2403,12 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string MovedItemToOrg {
|
||||
get {
|
||||
return ResourceManager.GetString("MovedItemToOrg", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ItemShared {
|
||||
get {
|
||||
return ResourceManager.GetString("ItemShared", resourceCulture);
|
||||
@@ -2423,15 +2433,21 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string MoveToOrganization {
|
||||
get {
|
||||
return ResourceManager.GetString("MoveToOrganization", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string NoOrgsToList {
|
||||
get {
|
||||
return ResourceManager.GetString("NoOrgsToList", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ShareDesc {
|
||||
public static string MoveToOrgDesc {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareDesc", resourceCulture);
|
||||
return ResourceManager.GetString("MoveToOrgDesc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2483,9 +2499,9 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string ShareVaultConfirmation {
|
||||
public static string LearnOrgConfirmation {
|
||||
get {
|
||||
return ResourceManager.GetString("ShareVaultConfirmation", resourceCulture);
|
||||
return ResourceManager.GetString("LearnOrgConfirmation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3371,6 +3387,12 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string AddASend {
|
||||
get {
|
||||
return ResourceManager.GetString("AddASend", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CopyLink {
|
||||
get {
|
||||
return ResourceManager.GetString("CopyLink", resourceCulture);
|
||||
@@ -3508,5 +3530,113 @@ namespace Bit.App.Resources {
|
||||
return ResourceManager.GetString("SendFileEmailVerificationRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PasswordPrompt {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordPrompt", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PasswordConfirmation {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordConfirmation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PasswordConfirmationDesc {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordConfirmationDesc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CaptchaRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("CaptchaRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CaptchaFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("CaptchaFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2Title {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2Instruction {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2Instruction", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2Desc {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2Desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2AuthenticateWebAuthn {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2AuthenticateWebAuthn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2SomethingWentWrong {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2SomethingWentWrong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2AbortError {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2AbortError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2NetworkFail {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2NetworkFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2NoPermission {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2NoPermission", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2NotSupportedError {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2NotSupportedError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2PrivacyError {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2PrivacyError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2SecurityError {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2SecurityError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2ServerDataFail {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2ServerDataFail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Fido2TimeoutError {
|
||||
get {
|
||||
return ResourceManager.GetString("Fido2TimeoutError", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2002
src/App/Resources/AppResources.az.resx
Normal file
2002
src/App/Resources/AppResources.az.resx
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user