mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59d5314164 | ||
|
|
9c08a37772 | ||
|
|
b13f5356fe | ||
|
|
5f0c9725ce | ||
|
|
f951fea555 | ||
|
|
4b989b01e9 | ||
|
|
aed3ec5474 | ||
|
|
b354986199 | ||
|
|
e1983a7d66 | ||
|
|
0400d79f43 | ||
|
|
c911484632 | ||
|
|
713e441d2e | ||
|
|
d4b577732b | ||
|
|
440a410d7f | ||
|
|
37a536b138 | ||
|
|
a0aca3e837 | ||
|
|
b58c29111a | ||
|
|
b0f86ea161 | ||
|
|
93b59a75a4 | ||
|
|
54fcabaea6 | ||
|
|
0e966c0304 | ||
|
|
a363712127 | ||
|
|
4d8c665917 | ||
|
|
53bdd92e72 | ||
|
|
e51aa39ede | ||
|
|
33c82129ff | ||
|
|
7c5b8c0e9f | ||
|
|
9dc01bca1c | ||
|
|
3c7920b84c | ||
|
|
b92f3abbaf | ||
|
|
b6747a63ed | ||
|
|
41a44548d2 | ||
|
|
a79d3a0d7c | ||
|
|
f3a17709e5 | ||
|
|
ced9d33d2e | ||
|
|
23b1373f80 | ||
|
|
a80eb1f533 | ||
|
|
f657edf195 | ||
|
|
d34279dca5 | ||
|
|
954aa1112a | ||
|
|
b35a3339cb | ||
|
|
b59433debd | ||
|
|
fb2db9c652 | ||
|
|
2507f3301b | ||
|
|
bdad5e4f0a | ||
|
|
b5dcdc74d7 | ||
|
|
e2d1da02d3 | ||
|
|
8253f18312 | ||
|
|
f4a98a2031 | ||
|
|
224845cfd3 | ||
|
|
fc8c2ad67a | ||
|
|
c9d6f58563 | ||
|
|
325b557506 | ||
|
|
ce751cfc87 | ||
|
|
0f451fd4b9 | ||
|
|
b7819838b8 | ||
|
|
67c6cf6b8c | ||
|
|
d91d71333b | ||
|
|
431804ea80 | ||
|
|
7a7ab7bd0e | ||
|
|
b8cdee383b | ||
|
|
580fa02ee1 | ||
|
|
421834153d | ||
|
|
011f04e1dc | ||
|
|
3d8056704c | ||
|
|
41263f3419 | ||
|
|
9fe9210cb7 | ||
|
|
2272b10820 | ||
|
|
9d6fc73fcc | ||
|
|
73ecd67b20 | ||
|
|
e5ce3dbd32 | ||
|
|
a0a5e30f48 | ||
|
|
0eddee5816 | ||
|
|
6d2dcb73ae | ||
|
|
0d6cc91b67 | ||
|
|
ae52922698 | ||
|
|
236496e69f | ||
|
|
fe5cdb0004 | ||
|
|
f9547f158e | ||
|
|
0c75374c0f | ||
|
|
0b249d4dd4 | ||
|
|
a2bac9d368 | ||
|
|
392e429dfd | ||
|
|
50623b9b29 | ||
|
|
e7ce050324 | ||
|
|
762b574d49 | ||
|
|
d73bf6d225 | ||
|
|
c2108fdda0 | ||
|
|
2062a284e3 | ||
|
|
9164c9b946 | ||
|
|
13ddd10c40 | ||
|
|
e407acd2a7 | ||
|
|
11cdf52ec8 | ||
|
|
40a3541e8e | ||
|
|
7da13e22ad | ||
|
|
38d702b6fe | ||
|
|
df2af5459e | ||
|
|
40d68b1654 | ||
|
|
a240a4ac66 | ||
|
|
416ec3812d | ||
|
|
ff24891903 | ||
|
|
ddcbe298ac | ||
|
|
6d8f647aee | ||
|
|
a654987175 | ||
|
|
a5f960d8a1 | ||
|
|
1f707cda68 | ||
|
|
4e7f195fd2 | ||
|
|
7728e930be | ||
|
|
ab84200347 | ||
|
|
62d8824450 | ||
|
|
cf35d20adb | ||
|
|
65725b5a38 | ||
|
|
eca4777b99 | ||
|
|
066b3aba5b | ||
|
|
8e485ff26f | ||
|
|
341b66f44f | ||
|
|
19c62d3320 | ||
|
|
13ffbd7675 | ||
|
|
9af6aae699 | ||
|
|
2e562e8318 | ||
|
|
c6db763716 | ||
|
|
a3383af4ae | ||
|
|
6c56e44b61 | ||
|
|
64506a7080 | ||
|
|
fac9ae4b6c | ||
|
|
a2dc73afef | ||
|
|
59c5a34cd0 | ||
|
|
3321e6b0e2 | ||
|
|
8b7ac179fa | ||
|
|
ea745665c8 | ||
|
|
ca8f6ee10b | ||
|
|
3e51ff46f3 | ||
|
|
fa2e814559 | ||
|
|
87e337cbeb | ||
|
|
abb39df547 | ||
|
|
43e15bf911 | ||
|
|
4d79d0af89 | ||
|
|
69100d7db5 | ||
|
|
a064a6cf9b | ||
|
|
7953a9a3ce | ||
|
|
be3c6f210d | ||
|
|
f7cbddab4b | ||
|
|
d423818764 | ||
|
|
519acd43aa | ||
|
|
2682a0d9e4 | ||
|
|
8629ae048c | ||
|
|
905d01e804 | ||
|
|
0588bbc41d | ||
|
|
b308b4c54f | ||
|
|
c2c73d5460 | ||
|
|
e01bf57874 | ||
|
|
7ced93225b | ||
|
|
b5e61864af | ||
|
|
1e5aaea8f4 | ||
|
|
ab3bebf06a | ||
|
|
4a294d6a77 | ||
|
|
e0fda1a0bc | ||
|
|
d17da80f19 | ||
|
|
2e7658f857 | ||
|
|
53d0b28c7c | ||
|
|
33ba4d3871 | ||
|
|
225db6397d | ||
|
|
73b5d1b3f1 | ||
|
|
8da2eac6d0 | ||
|
|
fbd62153ee | ||
|
|
9145fa1c48 | ||
|
|
caa0af1258 | ||
|
|
7a230ee5f5 | ||
|
|
f237fa98d2 | ||
|
|
be4ae605a9 | ||
|
|
9c2cbc0ecb | ||
|
|
fb3009fc66 | ||
|
|
04c32e28cd | ||
|
|
645576c949 | ||
|
|
775bee3546 | ||
|
|
88aea96034 | ||
|
|
5f474dfaf5 | ||
|
|
fe7aad0835 | ||
|
|
79746efa2d | ||
|
|
a158021f46 | ||
|
|
2d91a893f7 | ||
|
|
dd4561d985 | ||
|
|
92764eeae0 | ||
|
|
b72808ab40 | ||
|
|
14f3f99218 | ||
|
|
d7130d9b67 | ||
|
|
3f94eee4d5 | ||
|
|
72cbdcbc8d | ||
|
|
e33b49e78c | ||
|
|
8e04945d4e | ||
|
|
3ca5da55cb | ||
|
|
ea30373a09 | ||
|
|
4b4757d0e5 | ||
|
|
4bc837509d | ||
|
|
c9d1e8dc65 | ||
|
|
88b8a192b5 | ||
|
|
94fbf627ba | ||
|
|
45fbdb8411 | ||
|
|
d9c947ccd0 | ||
|
|
2b670a5ae1 | ||
|
|
1af0178b50 | ||
|
|
3ec5d894b3 | ||
|
|
d81585ccc3 | ||
|
|
38f91bce1c | ||
|
|
2d41dd6ae0 | ||
|
|
1705a21f68 | ||
|
|
164d79898a | ||
|
|
50f809d290 | ||
|
|
39284b475d | ||
|
|
d44950d46c | ||
|
|
e9b55bc207 | ||
|
|
5470f08fee | ||
|
|
f9a3bbd7fa | ||
|
|
9d3165dc65 | ||
|
|
3475d39f37 | ||
|
|
44782b1ddf | ||
|
|
dd8d5fd84c | ||
|
|
e8f2d9d0dd | ||
|
|
a2de3b5d80 | ||
|
|
a2960c45bc | ||
|
|
dc91624597 | ||
|
|
223ec180fc | ||
|
|
0116572fec |
111
.editorconfig
111
.editorconfig
@@ -1,3 +1,112 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Don't use tabs for indentation.
|
||||
[*]
|
||||
indent_style = space
|
||||
# (Please don't specify an indent_size here; that has too many unintended consequences.)
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
charset = utf-8-bom
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Prefix private members with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_members
|
||||
dotnet_naming_rule.private_members_with_underscore.style = underscore_prefix
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_members.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_members.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_members.required_modifiers = readonly
|
||||
|
||||
dotnet_naming_style.underscore_prefix.capitalization = camel_case
|
||||
dotnet_naming_style.underscore_prefix.required_prefix = _
|
||||
dotnet_naming_style.underscore_prefix.required_suffix =
|
||||
dotnet_naming_style.underscore_prefix.word_separator =
|
||||
|
||||
# Async methods should have "Async" suffix
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.any_async_methods.required_modifiers = async
|
||||
|
||||
dotnet_naming_style.end_in_async.required_prefix =
|
||||
dotnet_naming_style.end_in_async.required_suffix = Async
|
||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||
dotnet_naming_style.end_in_async.word_separator =
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a expression-body
|
||||
csharp_style_expression_bodied_methods = true:none
|
||||
csharp_style_expression_bodied_constructors = true:none
|
||||
csharp_style_expression_bodied_operators = true:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# All files
|
||||
[*]
|
||||
guidelines = 120
|
||||
guidelines = 120
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin iOS, and Xamarin Forms.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-android-myvault.png" alt="" width="300" height="533" /> <img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-ios.png" alt="" width="300" height="533" />
|
||||
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-android-myvault.png" alt="" width="300" height="533" /> <img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/mobile-ios-myvault.png" alt="" width="300" height="533" />
|
||||
|
||||
# Build/Run
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
- Visual Studio 2019
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
@@ -95,7 +95,9 @@ build_script:
|
||||
msbuild bitwarden-mobile.sln `
|
||||
"/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" `
|
||||
"/p:Configuration=Release"
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
.\src\Android\ci-build-apks.ps1
|
||||
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden.apk
|
||||
Push-AppveyorArtifact .\com.x8bit.bitwarden-fdroid.apk
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.28307.539
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29009.5
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}"
|
||||
EndProject
|
||||
@@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "google", "google", "{2E3996
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{76690DFB-B7F4-4781-83E4-113FDC450AFE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
appveyor.yml = appveyor.yml
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
@@ -35,6 +36,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Core", "src\iOS.Core\iO
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS", "src\iOS\iOS.csproj", "{599E0201-420A-4C3E-A7BA-5349F72E0B15}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Extension", "src\iOS.Extension\iOS.Extension.csproj", "{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Autofill", "src\iOS.Autofill\iOS.Autofill.csproj", "{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
|
||||
@@ -99,24 +104,24 @@ Global
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{304400AF-F0ED-40FA-B102-EA3C3EC43E4F}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhone.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|Any CPU.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhone.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.AppStore|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
@@ -141,18 +146,18 @@ Global
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{EE44C6A1-2A85-45FE-8D9B-BF1D5F88809C}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|Any CPU.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhone.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4B8A8C41-9820-4341-974C-41E65B7F4366}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
@@ -231,18 +236,18 @@ Global
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhone.Build.0 = Release|Any CPU
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
@@ -286,6 +291,62 @@ Global
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|Any CPU.Build.0 = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhone.Build.0 = AppStore|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|Any CPU.ActiveCfg = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhone.ActiveCfg = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhone.Build.0 = Debug|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|Any CPU.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|Any CPU.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhone.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhone.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|Any CPU.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhone.ActiveCfg = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|Any CPU.Build.0 = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhone.Build.0 = AppStore|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|Any CPU.ActiveCfg = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhone.ActiveCfg = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhone.Build.0 = Debug|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|Any CPU.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|Any CPU.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhone.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhone.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.FDroid|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|Any CPU.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.ActiveCfg = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.Build.0 = Release|iPhone
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -299,6 +360,8 @@ Global
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D} = {2E399654-26A2-46F6-B9CA-1B496A3F370A}
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{599E0201-420A-4C3E-A7BA-5349F72E0B15} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A} = {D10CA4A9-F866-40E1-B658-F69051236C71}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7D436EA3-8B7E-45D2-8D14-0730BD2E0410}
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bitwarden-fdroid",
|
||||
"name": "bitwarden-mobile",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
@@ -196,9 +196,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
|
||||
@@ -37,9 +37,11 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("org.mozilla.firefox", "url_bar_title"),
|
||||
new Browser("org.mozilla.firefox_beta", "url_bar_title"),
|
||||
new Browser("org.mozilla.fennec_aurora", "url_bar_title"),
|
||||
new Browser("org.mozilla.fennec_fdroid", "url_bar_title"),
|
||||
new Browser("org.mozilla.focus", "display_url"),
|
||||
new Browser("org.mozilla.klar", "display_url"),
|
||||
new Browser("org.mozilla.fenix", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("org.mozilla.fenix.nightly", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("org.mozilla.reference.browser", "mozac_browser_toolbar_url_view"),
|
||||
new Browser("com.ghostery.android.ghostery", "search_field"),
|
||||
new Browser("org.adblockplus.browser", "url_bar_title"),
|
||||
@@ -61,6 +63,11 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||
new Browser("com.ecosia.android", "url_bar"),
|
||||
new Browser("com.qwant.liberty", "url_bar_title"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_black", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_test", "url_text"),
|
||||
new Browser("com.vivaldi.browser", "url_bar"),
|
||||
new Browser("com.feedback.browser.wjbrowser", "addressbar_url"),
|
||||
}.ToDictionary(n => n.PackageName);
|
||||
|
||||
// Known packages to skip
|
||||
@@ -80,13 +87,18 @@ namespace Bit.Droid.Accessibility
|
||||
"com.teslacoilsw.launcher.prime",
|
||||
"is.shortcut",
|
||||
"me.craftsapp.nlauncher",
|
||||
"com.ss.squarehome2"
|
||||
"com.ss.squarehome2",
|
||||
"com.treydev.pns"
|
||||
};
|
||||
|
||||
public static void PrintTestData(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
|
||||
var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
|
||||
foreach(var node in testNodesData)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("Node: {0} = {1}", node.id, node.text);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetUri(AccessibilityNodeInfo root)
|
||||
@@ -243,4 +255,4 @@ namespace Bit.Droid.Accessibility
|
||||
return allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
|
||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
@@ -74,7 +74,6 @@
|
||||
<Reference Include="Mono.Android.Export" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
@@ -117,10 +116,10 @@
|
||||
<Compile Include="Effects\FixedSizeEffect.cs" />
|
||||
<Compile Include="Effects\SelectableLabelEffect.cs" />
|
||||
<Compile Include="Effects\TabBarEffect.cs" />
|
||||
<Compile Include="Migration\AndroidKeyStoreStorageService.cs" />
|
||||
<Compile Include="Push\FirebaseInstanceIdService.cs" />
|
||||
<Compile Include="Push\FirebaseMessagingService.cs" />
|
||||
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\EventUploadReceiver.cs" />
|
||||
<Compile Include="Receivers\LockAlarmReceiver.cs" />
|
||||
<Compile Include="Receivers\PackageReplacedReceiver.cs" />
|
||||
<Compile Include="Renderers\CipherViewCellRenderer.cs" />
|
||||
@@ -145,6 +144,7 @@
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
<Compile Include="Utilities\CustomFingerprintDialogFragment.cs" />
|
||||
<Compile Include="Utilities\HockeyAppCrashManagerListener.cs" />
|
||||
<Compile Include="Utilities\StaticStore.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\FontAwesome.ttf" />
|
||||
|
||||
@@ -53,11 +53,14 @@ namespace Bit.Droid.Autofill
|
||||
"com.ecosia.android",
|
||||
"com.opera.mini.native.beta",
|
||||
"org.mozilla.fennec_aurora",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"com.qwant.liberty",
|
||||
"com.opera.touch",
|
||||
"org.mozilla.fenix",
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"com.vivaldi.browser",
|
||||
};
|
||||
|
||||
// The URLs are blacklisted from autofilling
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core;
|
||||
using Android.Content;
|
||||
using Bit.Core.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
@@ -17,7 +18,7 @@ namespace Bit.Droid.Autofill
|
||||
private readonly AssistStructure _structure;
|
||||
private string _uri;
|
||||
private string _packageName;
|
||||
private string _webDomain;
|
||||
private string _website;
|
||||
|
||||
public Parser(AssistStructure structure, Context applicationContext)
|
||||
{
|
||||
@@ -36,14 +37,14 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
return _uri;
|
||||
}
|
||||
var webDomainNull = string.IsNullOrWhiteSpace(WebDomain);
|
||||
if(webDomainNull && string.IsNullOrWhiteSpace(PackageName))
|
||||
var websiteNull = string.IsNullOrWhiteSpace(Website);
|
||||
if(websiteNull && string.IsNullOrWhiteSpace(PackageName))
|
||||
{
|
||||
_uri = null;
|
||||
}
|
||||
else if(!webDomainNull)
|
||||
else if(!websiteNull)
|
||||
{
|
||||
_uri = string.Concat("http://", WebDomain);
|
||||
_uri = Website;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -66,16 +67,16 @@ namespace Bit.Droid.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
public string WebDomain
|
||||
public string Website
|
||||
{
|
||||
get => _webDomain;
|
||||
get => _website;
|
||||
set
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
_webDomain = _uri = null;
|
||||
_website = _uri = null;
|
||||
}
|
||||
_webDomain = value;
|
||||
_website = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,15 +97,24 @@ namespace Bit.Droid.Autofill
|
||||
|
||||
public void Parse()
|
||||
{
|
||||
string titlePackageId = null;
|
||||
for(var i = 0; i < _structure.WindowNodeCount; i++)
|
||||
{
|
||||
var node = _structure.GetWindowNodeAt(i);
|
||||
if(i == 0)
|
||||
{
|
||||
titlePackageId = GetTitlePackageId(node);
|
||||
}
|
||||
ParseNode(node.RootViewNode);
|
||||
}
|
||||
if(string.IsNullOrWhiteSpace(PackageName) && string.IsNullOrWhiteSpace(Website))
|
||||
{
|
||||
PackageName = titlePackageId;
|
||||
}
|
||||
if(!AutofillHelpers.TrustedBrowsers.Contains(PackageName) &&
|
||||
!AutofillHelpers.CompatBrowsers.Contains(PackageName))
|
||||
{
|
||||
WebDomain = null;
|
||||
Website = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +145,32 @@ namespace Bit.Droid.Autofill
|
||||
{
|
||||
PackageName = node.IdPackage;
|
||||
}
|
||||
if(string.IsNullOrWhiteSpace(WebDomain) && !string.IsNullOrWhiteSpace(node.WebDomain))
|
||||
if(string.IsNullOrWhiteSpace(Website) && !string.IsNullOrWhiteSpace(node.WebDomain))
|
||||
{
|
||||
WebDomain = node.WebDomain;
|
||||
var scheme = "http";
|
||||
if((int)Build.VERSION.SdkInt >= 28)
|
||||
{
|
||||
scheme = node.WebScheme;
|
||||
}
|
||||
Website = string.Format("{0}://{1}", scheme, node.WebDomain);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTitlePackageId(WindowNode node)
|
||||
{
|
||||
if(node != null && !string.IsNullOrWhiteSpace(node.Title))
|
||||
{
|
||||
var slashPosition = node.Title.IndexOf('/');
|
||||
if(slashPosition > -1)
|
||||
{
|
||||
var packageId = node.Title.Substring(0, slashPosition);
|
||||
if(packageId.Contains("."))
|
||||
{
|
||||
return packageId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using Bit.Core.Enums;
|
||||
using Android.Nfc;
|
||||
using Bit.App.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Support.V4.Content;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
@@ -29,17 +30,16 @@ namespace Bit.Droid
|
||||
[Register("com.x8bit.bitwarden.MainActivity")]
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
{
|
||||
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
|
||||
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IMessagingService _messagingService;
|
||||
private IBroadcasterService _broadcasterService;
|
||||
private IUserService _userService;
|
||||
private IAppIdService _appIdService;
|
||||
private IStorageService _storageService;
|
||||
private IStateService _stateService;
|
||||
private IEventService _eventService;
|
||||
private PendingIntent _lockAlarmPendingIntent;
|
||||
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 =
|
||||
@@ -47,6 +47,9 @@ namespace Bit.Droid
|
||||
|
||||
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));
|
||||
_lockAlarmPendingIntent = PendingIntent.GetBroadcast(this, 0, alarmIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
@@ -63,12 +66,12 @@ namespace Bit.Droid
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
|
||||
TabLayoutResource = Resource.Layout.Tabbar;
|
||||
ToolbarResource = Resource.Layout.Toolbar;
|
||||
|
||||
UpdateTheme(ThemeManager.GetTheme());
|
||||
UpdateTheme(ThemeManager.GetTheme(true));
|
||||
base.OnCreate(savedInstanceState);
|
||||
if(!CoreHelpers.InDebugMode())
|
||||
{
|
||||
@@ -77,8 +80,7 @@ namespace Bit.Droid
|
||||
|
||||
#if !FDROID
|
||||
var hockeyAppListener = new HockeyAppCrashManagerListener(_appIdService, _userService);
|
||||
var hockeyAppTask = hockeyAppListener.InitAsync();
|
||||
HockeyApp.Android.CrashManager.Register(this, HockeyAppId, hockeyAppListener);
|
||||
var hockeyAppTask = hockeyAppListener.InitAsync(this);
|
||||
#endif
|
||||
|
||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||
@@ -90,10 +92,10 @@ namespace Bit.Droid
|
||||
{
|
||||
if(message.Command == "scheduleLockTimer")
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
var lockOptionMinutes = (int)message.Data;
|
||||
var lockOptionMs = lockOptionMinutes * 60000;
|
||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + lockOptionMs + 10;
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Set(AlarmType.RtcWakeup, triggerMs, _lockAlarmPendingIntent);
|
||||
}
|
||||
else if(message.Command == "cancelLockTimer")
|
||||
@@ -101,6 +103,14 @@ namespace Bit.Droid
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Cancel(_lockAlarmPendingIntent);
|
||||
}
|
||||
else if(message.Command == "startEventTimer")
|
||||
{
|
||||
StartEventAlarm();
|
||||
}
|
||||
else if(message.Command == "stopEventTimer")
|
||||
{
|
||||
var task = StopEventAlarmAsync();
|
||||
}
|
||||
else if(message.Command == "finishMainActivity")
|
||||
{
|
||||
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => Finish());
|
||||
@@ -202,9 +212,8 @@ namespace Bit.Droid
|
||||
else
|
||||
{
|
||||
// camera
|
||||
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
uri = Android.Net.Uri.FromFile(file);
|
||||
var file = new Java.IO.File(FilesDir, "temp_camera_photo.jpg");
|
||||
uri = FileProvider.GetUriForFile(this, "com.x8bit.bitwarden.fileprovider", file);
|
||||
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
|
||||
}
|
||||
|
||||
@@ -367,10 +376,23 @@ namespace Bit.Droid
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _stateService.SaveAsync(Constants.LastClipboardValueKey, data.Item1);
|
||||
StaticStore.LastClipboardValue = data.Item1;
|
||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs.Value;
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent);
|
||||
}
|
||||
|
||||
private void StartEventAlarm()
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.SetInexactRepeating(AlarmType.ElapsedRealtime, 120000, 300000, _eventUploadPendingIntent);
|
||||
}
|
||||
|
||||
private async Task StopEventAlarmAsync()
|
||||
{
|
||||
var alarmManager = GetSystemService(AlarmService) as AlarmManager;
|
||||
alarmManager.Cancel(_eventUploadPendingIntent);
|
||||
await _eventService.UploadEventsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,12 +40,8 @@ namespace Bit.Droid
|
||||
if(ServiceContainer.RegisteredServices.Count == 0)
|
||||
{
|
||||
RegisterLocalServices();
|
||||
ServiceContainer.Init(new AndroidClientHandler());
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
var task = App.Migration.MigrationHelpers.PerformMigrationAsync();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
ServiceContainer.Init(deviceActionService.DeviceUserAgent);
|
||||
}
|
||||
#if !FDROID
|
||||
if(Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@@ -73,12 +69,6 @@ namespace Bit.Droid
|
||||
private void RegisterLocalServices()
|
||||
{
|
||||
ServiceContainer.Register<ILogService>("logService", new AndroidLogService());
|
||||
ServiceContainer.Register("settingsShim", new App.Migration.SettingsShim());
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
ServiceContainer.Register<App.Migration.Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService", new Migration.AndroidKeyStoreStorageService());
|
||||
}
|
||||
|
||||
Refractored.FabControl.Droid.FloatingActionButtonViewRenderer.Init();
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
@@ -107,7 +97,7 @@ namespace Bit.Droid
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService, messagingService,
|
||||
broadcasterService);
|
||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
|
||||
@@ -148,11 +138,11 @@ namespace Bit.Droid
|
||||
|
||||
private async Task BootstrapAsync()
|
||||
{
|
||||
var disableFavicon = await ServiceContainer.Resolve<IStorageService>("storageService").GetAsync<bool?>(
|
||||
Constants.DisableFaviconKey);
|
||||
await ServiceContainer.Resolve<IStateService>("stateService").SaveAsync(Constants.DisableFaviconKey,
|
||||
disableFavicon);
|
||||
var disableFavicon = await ServiceContainer.Resolve<IStorageService>("storageService")
|
||||
.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await ServiceContainer.Resolve<IStateService>("stateService").SaveAsync(
|
||||
Constants.DisableFaviconKey, disableFavicon);
|
||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Android.OS;
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Android.Security;
|
||||
using Javax.Security.Auth.X500;
|
||||
using Java.Math;
|
||||
using Android.Security.Keystore;
|
||||
using Android.App;
|
||||
using Java.Util;
|
||||
using Javax.Crypto.Spec;
|
||||
using Android.Preferences;
|
||||
using Bit.App.Migration;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Migration.Abstractions;
|
||||
|
||||
namespace Bit.Droid.Migration
|
||||
{
|
||||
public class AndroidKeyStoreStorageService : IOldSecureStorageService
|
||||
{
|
||||
private const string AndroidKeyStore = "AndroidKeyStore";
|
||||
private const string AesMode = "AES/GCM/NoPadding";
|
||||
|
||||
private const string KeyAlias = "bitwardenKey2";
|
||||
private const string KeyAliasV1 = "bitwardenKey";
|
||||
|
||||
private const string SettingsFormat = "ksSecured2:{0}";
|
||||
private const string SettingsFormatV1 = "ksSecured:{0}";
|
||||
|
||||
private const string AesKey = "ksSecured2:aesKeyForService";
|
||||
private const string AesKeyV1 = "ksSecured:aesKeyForService";
|
||||
|
||||
private readonly string _rsaMode;
|
||||
private readonly bool _oldAndroid;
|
||||
private readonly SettingsShim _settings;
|
||||
private readonly KeyStore _keyStore;
|
||||
|
||||
public AndroidKeyStoreStorageService()
|
||||
{
|
||||
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
|
||||
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
_settings = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
|
||||
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
|
||||
_keyStore.Load(null);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
GenerateStoreKey(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
GenerateStoreKey(false);
|
||||
}
|
||||
|
||||
GenerateAesKey();
|
||||
*/
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _settings.Contains(string.Format(SettingsFormat, key)) ||
|
||||
_settings.Contains(string.Format(SettingsFormatV1, key));
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
CleanupOld(key);
|
||||
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(_settings.Contains(formattedKey))
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Retrieve(string key)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(!_settings.Contains(formattedKey))
|
||||
{
|
||||
return TryGetAndMigrate(key);
|
||||
}
|
||||
|
||||
var cs = _settings.GetValueOrDefault(formattedKey, null);
|
||||
if(string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt from secure storage.");
|
||||
_settings.Remove(formattedKey);
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string key, byte[] dataBytes)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
CleanupOld(key);
|
||||
if(dataBytes == null)
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cipherString = App.Migration.Crypto.AesCbcEncrypt(dataBytes, aesKey);
|
||||
_settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to encrypt to secure storage.");
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStoreKey(bool withDate)
|
||||
{
|
||||
if(_keyStore.ContainsAlias(KeyAlias))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearSettings();
|
||||
|
||||
var end = Calendar.Instance;
|
||||
end.Add(CalendarField.Year, 99);
|
||||
|
||||
if(_oldAndroid)
|
||||
{
|
||||
var subject = new X500Principal($"CN={KeyAlias}");
|
||||
|
||||
var builder = new KeyPairGeneratorSpec.Builder(Application.Context)
|
||||
.SetAlias(KeyAlias)
|
||||
.SetSubject(subject)
|
||||
.SetSerialNumber(BigInteger.Ten);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetStartDate(new Date(0)).SetEndDate(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore);
|
||||
gen.Initialize(spec);
|
||||
gen.GenerateKeyPair();
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetKeyValidityStart(new Date(0)).SetKeyValidityEnd(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore);
|
||||
gen.Init(spec);
|
||||
gen.GenerateKey();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore.PrivateKeyEntry GetRsaKeyEntry(string alias)
|
||||
{
|
||||
return _keyStore.GetEntry(alias, null) as KeyStore.PrivateKeyEntry;
|
||||
}
|
||||
|
||||
private void GenerateAesKey()
|
||||
{
|
||||
if(_settings.Contains(AesKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = App.Migration.Crypto.RandomBytes(512 / 8);
|
||||
var encKey = _oldAndroid ? RsaEncrypt(key) : AesEncrypt(key);
|
||||
_settings.AddOrUpdateValue(AesKey, encKey);
|
||||
}
|
||||
|
||||
private App.Migration.Models.SymmetricCryptoKey GetAesKey(bool v1 = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var aesKey = v1 ? AesKeyV1 : AesKey;
|
||||
if(!_settings.Contains(aesKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var encKey = _settings.GetValueOrDefault(aesKey, null);
|
||||
if(string.IsNullOrWhiteSpace(encKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_oldAndroid || v1)
|
||||
{
|
||||
var encKeyBytes = Convert.FromBase64String(encKey);
|
||||
var key = RsaDecrypt(encKeyBytes, v1);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = encKey.Split('|');
|
||||
if(parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ivBytes = Convert.FromBase64String(parts[0]);
|
||||
var encKeyBytes = Convert.FromBase64String(parts[1]);
|
||||
var key = AesDecrypt(ivBytes, encKeyBytes);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Cannot get AesKey.");
|
||||
_keyStore.DeleteEntry(KeyAlias);
|
||||
_settings.Remove(AesKey);
|
||||
if(!v1)
|
||||
{
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string AesEncrypt(byte[] input)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry);
|
||||
var encBytes = cipher.DoFinal(input);
|
||||
var ivBytes = cipher.GetIV();
|
||||
return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}";
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] AesDecrypt(byte[] iv, byte[] encData)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
var spec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.DecryptMode, entry, spec);
|
||||
var decBytes = cipher.DoFinal(encData);
|
||||
return decBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private string RsaEncrypt(byte[] data)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey);
|
||||
var cipherText = cipher.DoFinal(data);
|
||||
return Convert.ToBase64String(cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaDecrypt(byte[] encData, bool v1)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(v1 ? KeyAliasV1 : KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
if(_oldAndroid)
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default);
|
||||
}
|
||||
|
||||
var plainText = cipher.DoFinal(encData);
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetAndMigrate(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
var aesKeyV1 = GetAesKey(true);
|
||||
if(aesKeyV1 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = _settings.GetValueOrDefault(formattedKeyV1, null);
|
||||
var value = App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKeyV1);
|
||||
Store(key, value);
|
||||
return value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt v1 from secure storage.");
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOld(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSettings(string format = SettingsFormat)
|
||||
{
|
||||
var prefix = string.Format(format, string.Empty);
|
||||
|
||||
using(var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(Application.Context))
|
||||
using(var sharedPreferencesEditor = sharedPreferences.Edit())
|
||||
{
|
||||
var removed = false;
|
||||
foreach(var pref in sharedPreferences.All)
|
||||
{
|
||||
if(pref.Key.StartsWith(prefix))
|
||||
{
|
||||
removed = true;
|
||||
sharedPreferencesEditor.Remove(pref.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if(removed)
|
||||
{
|
||||
sharedPreferencesEditor.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,17 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.0.6"
|
||||
android:versionName="2.2.6"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="29" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.ClearClipboardAlarmReceiver", Exported = false)]
|
||||
public class ClearClipboardAlarmReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
public override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
||||
var lastClipboardValue = await stateService.GetAsync<string>(Constants.LastClipboardValueKey);
|
||||
await stateService.RemoveAsync(Constants.LastClipboardValueKey);
|
||||
if(lastClipboardValue == clipboardManager.Text)
|
||||
if(StaticStore.LastClipboardValue != null && StaticStore.LastClipboardValue == clipboardManager.Text)
|
||||
{
|
||||
clipboardManager.Text = string.Empty;
|
||||
}
|
||||
StaticStore.LastClipboardValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
src/Android/Receivers/EventUploadReceiver.cs
Normal file
16
src/Android/Receivers/EventUploadReceiver.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Android.Content;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Receivers
|
||||
{
|
||||
[BroadcastReceiver(Name = "com.x8bit.bitwarden.EventUploadReceiver", Exported = false)]
|
||||
public class EventUploadReceiver : BroadcastReceiver
|
||||
{
|
||||
public async override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
await eventService.UploadEventsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,14 @@ namespace Bit.Droid.Renderers
|
||||
var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.slider_thumb, null);
|
||||
if(t is GradientDrawable thumb)
|
||||
{
|
||||
thumb.SetColor(view.ThumbColor.ToAndroid());
|
||||
if(view.ThumbColor == Color.Default)
|
||||
{
|
||||
thumb.SetColor(Color.White.ToAndroid());
|
||||
}
|
||||
else
|
||||
{
|
||||
thumb.SetColor(view.ThumbColor.ToAndroid());
|
||||
}
|
||||
thumb.SetStroke(3, view.ThumbBorderColor.ToAndroid());
|
||||
Control.SetThumb(thumb);
|
||||
}
|
||||
|
||||
13303
src/Android/Resources/Resource.designer.cs
generated
13303
src/Android/Resources/Resource.designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,9 @@
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.fenix"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.fenix.nightly"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.mozilla.reference.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -87,4 +90,7 @@
|
||||
<compatibility-package
|
||||
android:name="com.ecosia.android"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.vivaldi.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
</autofill-service>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="internal" path="." />
|
||||
</paths>
|
||||
|
||||
@@ -8,9 +8,12 @@ using Android.App;
|
||||
using Android.App.Assist;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Hardware.Biometrics;
|
||||
using Android.Hardware.Fingerprints;
|
||||
using Android.Nfc;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Runtime;
|
||||
using Android.Support.V4.App;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Text;
|
||||
@@ -28,6 +31,7 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Autofill;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -36,18 +40,22 @@ namespace Bit.Droid.Services
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly Func<IEventService> _eventServiceFunc;
|
||||
private ProgressDialog _progressDialog;
|
||||
private bool _cameraPermissionsDenied;
|
||||
private Toast _toast;
|
||||
private string _userAgent;
|
||||
|
||||
public DeviceActionService(
|
||||
IStorageService storageService,
|
||||
IMessagingService messagingService,
|
||||
IBroadcasterService broadcasterService)
|
||||
IBroadcasterService broadcasterService,
|
||||
Func<IEventService> eventServiceFunc)
|
||||
{
|
||||
_storageService = storageService;
|
||||
_messagingService = messagingService;
|
||||
_broadcasterService = broadcasterService;
|
||||
_eventServiceFunc = eventServiceFunc;
|
||||
|
||||
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
||||
{
|
||||
@@ -58,6 +66,19 @@ namespace Bit.Droid.Services
|
||||
});
|
||||
}
|
||||
|
||||
public string DeviceUserAgent
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_userAgent))
|
||||
{
|
||||
_userAgent = $"Bitwarden_Mobile/{Xamarin.Essentials.AppInfo.VersionString} " +
|
||||
$"(Android {Build.VERSION.Release}; SDK {Build.VERSION.Sdk}; Model {Build.Model})";
|
||||
}
|
||||
return _userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Android;
|
||||
|
||||
public void Toast(string text, bool longDuration = false)
|
||||
@@ -111,38 +132,14 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
if(!CanOpenFile(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
activity.StartActivity(intent);
|
||||
return true;
|
||||
}
|
||||
@@ -151,22 +148,57 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||
PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||
{
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
intent.SetType(mimeType);
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
return intent;
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
@@ -182,7 +214,8 @@ namespace Bit.Droid.Services
|
||||
public Task SelectFileAsync()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
||||
HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var additionalIntents = new List<IParcelable>();
|
||||
if(activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
||||
{
|
||||
@@ -201,14 +234,14 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
|
||||
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
|
||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||
if(!file.Exists())
|
||||
{
|
||||
file.ParentFile.Mkdirs();
|
||||
file.CreateNewFile();
|
||||
}
|
||||
var outputFileUri = Android.Net.Uri.FromFile(file);
|
||||
var outputFileUri = FileProvider.GetUriForFile(activity,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
||||
}
|
||||
catch(Java.IO.IOException) { }
|
||||
@@ -306,11 +339,72 @@ namespace Bit.Droid.Services
|
||||
Application.Context.PackageName, 0).VersionCode.ToString();
|
||||
}
|
||||
|
||||
public bool SupportsFaceId()
|
||||
public bool SupportsFaceBiometric()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> SupportsFaceBiometricAsync()
|
||||
{
|
||||
return Task.FromResult(SupportsFaceBiometric());
|
||||
}
|
||||
|
||||
public async Task<bool> BiometricAvailableAsync()
|
||||
{
|
||||
if(UseNativeBiometric())
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var manager = activity.GetSystemService(Context.BiometricService) as BiometricManager;
|
||||
return manager.CanAuthenticate() == BiometricCode.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CrossFingerprint.Current.IsAvailableAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseNativeBiometric()
|
||||
{
|
||||
return (int)Build.VERSION.SdkInt >= 29;
|
||||
}
|
||||
|
||||
public Task<bool> AuthenticateBiometricAsync(string text = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
text = AppResources.BiometricsDirection;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
using(var builder = new BiometricPrompt.Builder(activity))
|
||||
{
|
||||
builder.SetTitle(text);
|
||||
builder.SetConfirmationRequired(false);
|
||||
builder.SetNegativeButton(AppResources.Cancel, activity.MainExecutor,
|
||||
new DialogInterfaceOnClickListener
|
||||
{
|
||||
Clicked = () => { }
|
||||
});
|
||||
var prompt = builder.Build();
|
||||
var result = new TaskCompletionSource<bool>();
|
||||
prompt.Authenticate(new CancellationSignal(), activity.MainExecutor,
|
||||
new BiometricAuthenticationCallback
|
||||
{
|
||||
Success = authResult => result.TrySetResult(true),
|
||||
Failed = () => result.TrySetResult(false),
|
||||
Help = (helpCode, helpString) => { }
|
||||
});
|
||||
return result.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsNfc()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
@@ -463,6 +557,7 @@ namespace Bit.Droid.Services
|
||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
||||
activity.SetResult(Result.Ok, replyIntent);
|
||||
activity.Finish();
|
||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -488,6 +583,10 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
activity.Finish();
|
||||
_messagingService.Send("finishMainActivity");
|
||||
if(cipher != null)
|
||||
{
|
||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +634,8 @@ namespace Bit.Droid.Services
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var afm = (AutofillManager)activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
var afm = (AutofillManager)activity.GetSystemService(
|
||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||
}
|
||||
catch
|
||||
@@ -577,6 +677,11 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool UsingDarkTheme()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if(dir != null && dir.IsDirectory)
|
||||
@@ -675,5 +780,41 @@ namespace Bit.Droid.Services
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
clipboardManager.Text = text;
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback
|
||||
{
|
||||
public Action<BiometricPrompt.AuthenticationResult> Success { get; set; }
|
||||
public Action Failed { get; set; }
|
||||
public Action<BiometricAcquiredStatus, Java.Lang.ICharSequence> Help { get; set; }
|
||||
|
||||
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult)
|
||||
{
|
||||
base.OnAuthenticationSucceeded(authResult);
|
||||
Success?.Invoke(authResult);
|
||||
}
|
||||
|
||||
public override void OnAuthenticationFailed()
|
||||
{
|
||||
base.OnAuthenticationFailed();
|
||||
Failed?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnAuthenticationHelp([GeneratedEnum] BiometricAcquiredStatus helpCode,
|
||||
Java.Lang.ICharSequence helpString)
|
||||
{
|
||||
base.OnAuthenticationHelp(helpCode, helpString);
|
||||
Help?.Invoke(helpCode, helpString);
|
||||
}
|
||||
}
|
||||
|
||||
private class DialogInterfaceOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
|
||||
{
|
||||
public Action Clicked { get; set; }
|
||||
|
||||
public void OnClick(IDialogInterface dialog, int which)
|
||||
{
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ using Newtonsoft.Json;
|
||||
using Android.Runtime;
|
||||
using Bit.Core.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Android.Content;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public class HockeyAppCrashManagerListener : CrashManagerListener
|
||||
{
|
||||
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
|
||||
|
||||
private readonly IAppIdService _appIdService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
@@ -31,10 +34,11 @@ namespace Bit.Droid.Utilities
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
public async Task InitAsync(Context context)
|
||||
{
|
||||
_userId = await _userService.GetUserIdAsync();
|
||||
_appId = await _appIdService.GetAppIdAsync();
|
||||
CrashManager.Register(context, HockeyAppId, this);
|
||||
}
|
||||
|
||||
public override string Description
|
||||
|
||||
19
src/Android/Utilities/StaticStore.cs
Normal file
19
src/Android/Utilities/StaticStore.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
|
||||
namespace Bit.Droid.Utilities
|
||||
{
|
||||
public static class StaticStore
|
||||
{
|
||||
public static string LastClipboardValue { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDeviceActionService
|
||||
{
|
||||
string DeviceUserAgent { get; }
|
||||
DeviceType DeviceType { get; }
|
||||
void Toast(string text, bool longDuration = false);
|
||||
bool LaunchApp(string appName);
|
||||
@@ -19,7 +20,11 @@ namespace Bit.App.Abstractions
|
||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||
bool autofocus = true);
|
||||
void RateApp();
|
||||
bool SupportsFaceId();
|
||||
bool SupportsFaceBiometric();
|
||||
Task<bool> SupportsFaceBiometricAsync();
|
||||
Task<bool> BiometricAvailableAsync();
|
||||
bool UseNativeBiometric();
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null);
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsAutofillService();
|
||||
@@ -34,5 +39,6 @@ namespace Bit.App.Abstractions
|
||||
string GetBuildNumber();
|
||||
void OpenAccessibilitySettings();
|
||||
void OpenAutofillSettings();
|
||||
bool UsingDarkTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PackageReference Include="Refractored.FloatingActionButtonForms" Version="2.1.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="3.6.0.709228" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.1.47" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
<Compile Update="Pages\Generator\GeneratorHistoryPage.xaml.cs">
|
||||
<DependentUpon>GeneratorHistoryPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\AutofillPage.xaml.cs">
|
||||
<DependentUpon>AutofillPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\ExtensionPage.xaml.cs">
|
||||
<DependentUpon>ExtensionPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\AutofillServicePage.xaml.cs">
|
||||
<DependentUpon>AutofillServicePage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@@ -89,9 +89,7 @@ namespace Bit.App
|
||||
}
|
||||
else if(message.Command == "locked")
|
||||
{
|
||||
await _stateService.PurgeAsync();
|
||||
var lockPage = new LockPage(_appOptions, !(message.Data as bool?).GetValueOrDefault());
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
await LockedAsync(!(message.Data as bool?).GetValueOrDefault());
|
||||
}
|
||||
else if(message.Command == "lockVault")
|
||||
{
|
||||
@@ -99,11 +97,8 @@ namespace Bit.App
|
||||
}
|
||||
else if(message.Command == "logout")
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(false));
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
await LogOutAsync((message.Data as bool?).GetValueOrDefault()));
|
||||
}
|
||||
else if(message.Command == "loggedOut")
|
||||
{
|
||||
@@ -114,7 +109,14 @@ namespace Bit.App
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
SyncIfNeeded();
|
||||
ResumedAsync();
|
||||
}
|
||||
}
|
||||
else if(message.Command == "slept")
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await SleptAsync();
|
||||
}
|
||||
}
|
||||
else if(message.Command == "migrated")
|
||||
@@ -153,6 +155,7 @@ namespace Bit.App
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||
await ClearCacheIfNeededAsync();
|
||||
await TryClearCiphersCacheAsync();
|
||||
Prime();
|
||||
if(string.IsNullOrWhiteSpace(_appOptions.Uri))
|
||||
{
|
||||
@@ -163,6 +166,7 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
}
|
||||
}
|
||||
_messagingService.Send("startEventTimer");
|
||||
}
|
||||
|
||||
protected async override void OnSleep()
|
||||
@@ -170,22 +174,39 @@ namespace Bit.App
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnSleep");
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow);
|
||||
var isLocked = await _lockService.IsLockedAsync();
|
||||
if(!isLocked)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow);
|
||||
}
|
||||
SetTabsPageFromAutofill(isLocked);
|
||||
await SleptAsync();
|
||||
}
|
||||
SetTabsPageFromAutofill();
|
||||
await HandleLockingAsync();
|
||||
}
|
||||
|
||||
protected async override void OnResume()
|
||||
protected override void OnResume()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||
_messagingService.Send("cancelLockTimer");
|
||||
await ClearCacheIfNeededAsync();
|
||||
Prime();
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
SyncIfNeeded();
|
||||
ResumedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SleptAsync()
|
||||
{
|
||||
await HandleLockingAsync();
|
||||
_messagingService.Send("stopEventTimer");
|
||||
}
|
||||
|
||||
private async void ResumedAsync()
|
||||
{
|
||||
_messagingService.Send("cancelLockTimer");
|
||||
_messagingService.Send("startEventTimer");
|
||||
await ClearCacheIfNeededAsync();
|
||||
await TryClearCiphersCacheAsync();
|
||||
Prime();
|
||||
SyncIfNeeded();
|
||||
if(Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage)
|
||||
{
|
||||
await lockPage.PromptFingerprintAfterResumeAsync();
|
||||
@@ -213,13 +234,17 @@ namespace Bit.App
|
||||
_folderService.ClearAsync(userId),
|
||||
_collectionService.ClearAsync(userId),
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_lockService.ClearAsync());
|
||||
_lockService.PinLocked = false;
|
||||
_lockService.ClearAsync(),
|
||||
_stateService.PurgeAsync());
|
||||
_lockService.FingerprintLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
Current.MainPage = new HomePage();
|
||||
if(expired)
|
||||
{
|
||||
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,7 +312,7 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTabsPageFromAutofill()
|
||||
private void SetTabsPageFromAutofill(bool isLocked)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(_appOptions.Uri) &&
|
||||
!_appOptions.FromAutofillFramework)
|
||||
@@ -296,8 +321,15 @@ namespace Bit.App
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Current.MainPage = new TabsPage();
|
||||
_appOptions.Uri = null;
|
||||
if(isLocked)
|
||||
{
|
||||
Current.MainPage = new NavigationPage(new LockPage());
|
||||
}
|
||||
else
|
||||
{
|
||||
Current.MainPage = new TabsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -316,7 +348,7 @@ namespace Bit.App
|
||||
{
|
||||
InitializeComponent();
|
||||
SetCulture();
|
||||
ThemeManager.SetTheme();
|
||||
ThemeManager.SetTheme(Device.RuntimePlatform == Device.Android);
|
||||
Current.MainPage = new HomePage();
|
||||
var mainPageTask = SetMainPageAsync();
|
||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||
@@ -324,10 +356,6 @@ namespace Bit.App
|
||||
|
||||
private void SyncIfNeeded()
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
return;
|
||||
@@ -335,11 +363,66 @@ namespace Bit.App
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if(DateTime.UtcNow - lastSync > TimeSpan.FromMinutes(30))
|
||||
if(lastSync == null || ((DateTime.UtcNow - lastSync) > TimeSpan.FromMinutes(30)))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _syncService.FullSyncAsync(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TryClearCiphersCacheAsync()
|
||||
{
|
||||
if(Device.RuntimePlatform != Device.iOS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var clearCache = await _storageService.GetAsync<bool?>(Constants.ClearCiphersCacheKey);
|
||||
if(clearCache.GetValueOrDefault())
|
||||
{
|
||||
_cipherService.ClearCache();
|
||||
await _storageService.RemoveAsync(Constants.ClearCiphersCacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LockedAsync(bool autoPromptFingerprint)
|
||||
{
|
||||
await _stateService.PurgeAsync();
|
||||
if(autoPromptFingerprint && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var lockOptions = await _storageService.GetAsync<int?>(Constants.LockOptionKey);
|
||||
if(lockOptions == 0)
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
}
|
||||
}
|
||||
PreviousPageInfo lastPageBeforeLock = null;
|
||||
if(Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||
if(topPage is NavigationPage navPage)
|
||||
{
|
||||
if(navPage.CurrentPage is ViewPage viewPage)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "view",
|
||||
CipherId = viewPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
else if(navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "edit",
|
||||
CipherId = addEditPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
await _storageService.SaveAsync(Constants.PreviousPageKey, lastPageBeforeLock);
|
||||
var lockPage = new LockPage(_appOptions, autoPromptFingerprint);
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,84 +3,112 @@
|
||||
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">
|
||||
<Grid x:Name="_grid"
|
||||
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.BindingContext>
|
||||
<controls:CipherViewCellViewModel />
|
||||
</Grid.BindingContext>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="60" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<controls:FaLabel x:Name="_icon"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-icon, list-icon-platform" />
|
||||
<ff:CachedImage x:Name="_image"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
IsVisible="False"/>
|
||||
<Label LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
<controls:FaLabel
|
||||
x:Name="_icon"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalOptions="Center"
|
||||
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="1"
|
||||
<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="2"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.Shared, Mode=OneWay}" />
|
||||
<controls:FaLabel
|
||||
Grid.Column="3"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Start"
|
||||
VerticalOptions="Center"
|
||||
StyleClass="list-title-icon"
|
||||
Margin="5, 0, 0, 0"
|
||||
Text=""
|
||||
IsVisible="{Binding Cipher.HasAttachments, 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
|
||||
Text=""
|
||||
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
|
||||
Clicked="ImageButton_Clicked"
|
||||
Grid.Column="4"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2" />
|
||||
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}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</ViewCell>
|
||||
|
||||
@@ -181,7 +181,7 @@ namespace Bit.App.Controls
|
||||
return new Tuple<string, string>(icon, image);
|
||||
}
|
||||
|
||||
private void ImageButton_Clicked(object sender, EventArgs e)
|
||||
private void MoreButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
ButtonCommand?.Execute(Cipher);
|
||||
}
|
||||
|
||||
21
src/App/Controls/ExtendedSearchBar.cs
Normal file
21
src/App/Controls/ExtendedSearchBar.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedSearchBar : SearchBar
|
||||
{
|
||||
public ExtendedSearchBar()
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService", true);
|
||||
if(!deviceActionService?.UsingDarkTheme() ?? false)
|
||||
{
|
||||
TextColor = Color.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Bit.App.Migration.Abstractions
|
||||
{
|
||||
public interface IOldSecureStorageService
|
||||
{
|
||||
bool Contains(string key);
|
||||
void Delete(string key);
|
||||
byte[] Retrieve(string key);
|
||||
void Store(string key, byte[] dataBytes);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
using Bit.App.Migration.Models;
|
||||
using Bit.Core.Enums;
|
||||
using PCLCrypto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class Crypto
|
||||
{
|
||||
public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
return new CipherString(parts.Item1, Convert.ToBase64String(parts.Item2),
|
||||
Convert.ToBase64String(parts.Item4), parts.Item3 != null ? Convert.ToBase64String(parts.Item3) : null);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcEncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
var macLength = parts.Item3?.Length ?? 0;
|
||||
|
||||
var encBytes = new byte[1 + parts.Item2.Length + macLength + parts.Item4.Length];
|
||||
encBytes[0] = (byte)parts.Item1;
|
||||
parts.Item2.CopyTo(encBytes, 1);
|
||||
if(parts.Item3 != null)
|
||||
{
|
||||
parts.Item3.CopyTo(encBytes, 1 + parts.Item2.Length);
|
||||
}
|
||||
parts.Item4.CopyTo(encBytes, 1 + parts.Item2.Length + macLength);
|
||||
return encBytes;
|
||||
}
|
||||
|
||||
private static Tuple<EncryptionType, byte[], byte[], byte[]> AesCbcEncryptToParts(byte[] plainBytes,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(plainBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(plainBytes));
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var iv = RandomBytes(provider.BlockLength);
|
||||
var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null;
|
||||
|
||||
return new Tuple<EncryptionType, byte[], byte[], byte[]>(key.EncryptionType, iv, mac, ct);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key)
|
||||
{
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
return AesCbcDecrypt(encyptedValue.EncryptionType, encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes, encyptedValue.MacBytes, key);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(EncryptionType type, byte[] ct, byte[] iv, byte[] mac,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(ct == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ct));
|
||||
}
|
||||
|
||||
if(iv == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(iv));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mac));
|
||||
}
|
||||
|
||||
if(key.EncryptionType != type)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(type));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac != null)
|
||||
{
|
||||
var computedMacBytes = ComputeMac(ct, iv, key.MacKey);
|
||||
if(!MacsEqual(computedMacBytes, mac))
|
||||
{
|
||||
throw new InvalidOperationException("MAC failed.");
|
||||
}
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, ct, iv);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
public static byte[] RandomBytes(int length)
|
||||
{
|
||||
return WinRTCrypto.CryptographicBuffer.GenerateRandom(length);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
|
||||
{
|
||||
if(ctBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ctBytes));
|
||||
}
|
||||
|
||||
if(ivBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ivBytes));
|
||||
}
|
||||
|
||||
return ComputeMac(ivBytes.Concat(ctBytes), macKey);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(IEnumerable<byte> dataBytes, byte[] macKey)
|
||||
{
|
||||
if(macKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(macKey));
|
||||
}
|
||||
|
||||
if(dataBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataBytes));
|
||||
}
|
||||
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(macKey);
|
||||
hasher.Append(dataBytes.ToArray());
|
||||
var mac = hasher.GetValueAndReset();
|
||||
return mac;
|
||||
}
|
||||
|
||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
public static bool MacsEqual(byte[] mac1, byte[] mac2)
|
||||
{
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(RandomBytes(32));
|
||||
|
||||
hasher.Append(mac1);
|
||||
mac1 = hasher.GetValueAndReset();
|
||||
|
||||
hasher.Append(mac2);
|
||||
mac2 = hasher.GetValueAndReset();
|
||||
|
||||
if(mac1.Length != mac2.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int i = 0; i < mac2.Length; i++)
|
||||
{
|
||||
if(mac1[i] != mac2[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
public static byte[] HkdfExpand(byte[] prk, byte[] info, int size)
|
||||
{
|
||||
var hashLen = 32; // sha256
|
||||
var okm = new byte[size];
|
||||
var previousT = new byte[0];
|
||||
var n = (int)Math.Ceiling((double)size / hashLen);
|
||||
for(int i = 0; i < n; i++)
|
||||
{
|
||||
var t = new byte[previousT.Length + info.Length + 1];
|
||||
previousT.CopyTo(t, 0);
|
||||
info.CopyTo(t, previousT.Length);
|
||||
t[t.Length - 1] = (byte)(i + 1);
|
||||
previousT = ComputeMac(t, prk);
|
||||
previousT.CopyTo(okm, i * hashLen);
|
||||
}
|
||||
return okm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class MigrationHelpers
|
||||
{
|
||||
public static bool Migrating = false;
|
||||
|
||||
public static bool NeedsMigration()
|
||||
{
|
||||
return ServiceContainer.Resolve<SettingsShim>("settingsShim")
|
||||
.GetValueOrDefault(Constants.OldUserIdKey, null) != null; ;
|
||||
}
|
||||
|
||||
public static async Task<bool> PerformMigrationAsync()
|
||||
{
|
||||
if(!NeedsMigration() || Migrating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Migrating = true;
|
||||
var settingsShim = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
var oldSecureStorageService = ServiceContainer.Resolve<Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService");
|
||||
|
||||
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
var syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
|
||||
// Get old data
|
||||
|
||||
var oldTokenBytes = oldSecureStorageService.Retrieve("accessToken");
|
||||
var oldToken = oldTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTokenBytes, 0, oldTokenBytes.Length);
|
||||
var oldKeyBytes = oldSecureStorageService.Retrieve("key");
|
||||
var oldKey = oldKeyBytes == null ? null : new Models.SymmetricCryptoKey(oldKeyBytes);
|
||||
var oldUserId = settingsShim.GetValueOrDefault("userId", null);
|
||||
|
||||
var isAuthenticated = oldKey != null && !string.IsNullOrWhiteSpace(oldToken) &&
|
||||
!string.IsNullOrWhiteSpace(oldUserId);
|
||||
if(!isAuthenticated)
|
||||
{
|
||||
Migrating = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldRefreshTokenBytes = oldSecureStorageService.Retrieve("refreshToken");
|
||||
var oldRefreshToken = oldRefreshTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldRefreshTokenBytes, 0, oldRefreshTokenBytes.Length);
|
||||
var oldPinBytes = oldSecureStorageService.Retrieve("pin");
|
||||
var oldPin = oldPinBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldPinBytes, 0, oldPinBytes.Length);
|
||||
|
||||
var oldEncKey = settingsShim.GetValueOrDefault("encKey", null);
|
||||
var oldEncPrivateKey = settingsShim.GetValueOrDefault("encPrivateKey", null);
|
||||
var oldEmail = settingsShim.GetValueOrDefault("email", null);
|
||||
var oldKdf = (KdfType)settingsShim.GetValueOrDefault("kdf", (int)KdfType.PBKDF2_SHA256);
|
||||
var oldKdfIterations = settingsShim.GetValueOrDefault("kdfIterations", 5000);
|
||||
|
||||
var oldTwoFactorTokenBytes = oldSecureStorageService.Retrieve(
|
||||
string.Format("twoFactorToken_{0}", Convert.ToBase64String(Encoding.UTF8.GetBytes(oldEmail))));
|
||||
var oldTwoFactorToken = oldTwoFactorTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTwoFactorTokenBytes, 0, oldTwoFactorTokenBytes.Length);
|
||||
|
||||
var oldAppIdBytes = oldSecureStorageService.Retrieve("appId");
|
||||
var oldAppId = oldAppIdBytes == null ? null : new Guid(oldAppIdBytes).ToString();
|
||||
var oldAnonAppIdBytes = oldSecureStorageService.Retrieve("anonymousAppId");
|
||||
var oldAnonAppId = oldAnonAppIdBytes == null ? null : new Guid(oldAnonAppIdBytes).ToString();
|
||||
var oldFingerprint = settingsShim.GetValueOrDefault("setting:fingerprintUnlockOn", false);
|
||||
|
||||
// Save settings
|
||||
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPersistNotificationKey,
|
||||
settingsShim.GetValueOrDefault("setting:persistNotification", false));
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPasswordFieldKey,
|
||||
settingsShim.GetValueOrDefault("setting:autofillPasswordField", false));
|
||||
await storageService.SaveAsync(Constants.DisableAutoTotpCopyKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableAutoCopyTotp", false));
|
||||
await storageService.SaveAsync(Constants.DisableFaviconKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableWebsiteIcons", false));
|
||||
await storageService.SaveAsync(Constants.AddSitePromptShownKey,
|
||||
settingsShim.GetValueOrDefault("addedSiteAlert", false));
|
||||
await storageService.SaveAsync(Constants.PushInitialPromptShownKey,
|
||||
settingsShim.GetValueOrDefault("push:initialPromptShown", false));
|
||||
await storageService.SaveAsync(Constants.PushCurrentTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:currentToken", null));
|
||||
await storageService.SaveAsync(Constants.PushRegisteredTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:registeredToken", null));
|
||||
// For some reason "push:lastRegistrationDate" isn't getting pulled from settingsShim correctly.
|
||||
// We don't really need it anyways.
|
||||
// var lastReg = settingsShim.GetValueOrDefault("push:lastRegistrationDate", DateTime.MinValue);
|
||||
// await storageService.SaveAsync(Constants.PushLastRegistrationDateKey, lastReg);
|
||||
await storageService.SaveAsync("rememberedEmail",
|
||||
settingsShim.GetValueOrDefault("other:lastLoginEmail", null));
|
||||
|
||||
await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||
{
|
||||
Base = settingsShim.GetValueOrDefault("other:baseUrl", null),
|
||||
Api = settingsShim.GetValueOrDefault("other:apiUrl", null),
|
||||
WebVault = settingsShim.GetValueOrDefault("other:webVaultUrl", null),
|
||||
Identity = settingsShim.GetValueOrDefault("other:identityUrl", null),
|
||||
Icons = settingsShim.GetValueOrDefault("other:iconsUrl", null)
|
||||
});
|
||||
|
||||
await passwordGenerationService.SaveOptionsAsync(new Core.Models.Domain.PasswordGenerationOptions
|
||||
{
|
||||
Ambiguous = settingsShim.GetValueOrDefault("pwGenerator:ambiguous", false),
|
||||
Length = settingsShim.GetValueOrDefault("pwGenerator:length", 15),
|
||||
Uppercase = settingsShim.GetValueOrDefault("pwGenerator:uppercase", true),
|
||||
Lowercase = settingsShim.GetValueOrDefault("pwGenerator:lowercase", true),
|
||||
Number = settingsShim.GetValueOrDefault("pwGenerator:numbers", true),
|
||||
MinNumber = settingsShim.GetValueOrDefault("pwGenerator:minNumbers", 0),
|
||||
Special = settingsShim.GetValueOrDefault("pwGenerator:special", true),
|
||||
MinSpecial = settingsShim.GetValueOrDefault("pwGenerator:minSpecial", 0),
|
||||
WordSeparator = "-",
|
||||
NumWords = 3
|
||||
});
|
||||
|
||||
// Save lock options
|
||||
|
||||
int? lockOptionsSeconds = settingsShim.GetValueOrDefault("setting:lockSeconds", -10);
|
||||
if(lockOptionsSeconds == -10)
|
||||
{
|
||||
lockOptionsSeconds = 60 * 15;
|
||||
}
|
||||
else if(lockOptionsSeconds == -1)
|
||||
{
|
||||
lockOptionsSeconds = null;
|
||||
}
|
||||
await storageService.SaveAsync(Constants.LockOptionKey,
|
||||
lockOptionsSeconds == null ? (int?)null : lockOptionsSeconds.Value / 60);
|
||||
|
||||
// Save app ids
|
||||
|
||||
await storageService.SaveAsync("appId", oldAppId);
|
||||
await storageService.SaveAsync("anonymousAppId", oldAnonAppId);
|
||||
|
||||
// Save new authed data
|
||||
|
||||
await tokenService.SetTwoFactorTokenAsync(oldTwoFactorToken, oldEmail);
|
||||
await tokenService.SetTokensAsync(oldToken, oldRefreshToken);
|
||||
await userService.SetInformationAsync(oldUserId, oldEmail, oldKdf, oldKdfIterations);
|
||||
|
||||
var newKey = new Core.Models.Domain.SymmetricCryptoKey(oldKey.Key);
|
||||
await cryptoService.SetKeyAsync(newKey);
|
||||
// Key hash is unavailable in old version, store old key until we can move it to key hash
|
||||
await secureStorageService.SaveAsync("oldKey", newKey.KeyB64);
|
||||
await cryptoService.SetEncKeyAsync(oldEncKey);
|
||||
await cryptoService.SetEncPrivateKeyAsync(oldEncPrivateKey);
|
||||
|
||||
// Save fingerprint/pin
|
||||
|
||||
if(oldFingerprint)
|
||||
{
|
||||
await storageService.SaveAsync(Constants.FingerprintUnlockKey, true);
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(oldPin))
|
||||
{
|
||||
var pinKey = await cryptoService.MakePinKeyAysnc(oldPin, oldEmail, oldKdf, oldKdfIterations);
|
||||
var pinProtectedKey = await cryptoService.EncryptAsync(oldKeyBytes, pinKey);
|
||||
await storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString);
|
||||
}
|
||||
|
||||
// Post migration tasks
|
||||
await cryptoService.ToggleKeyAsync();
|
||||
await storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow.AddYears(-1));
|
||||
await lockService.CheckLockAsync();
|
||||
|
||||
// Remove "needs migration" flag
|
||||
settingsShim.Remove(Constants.OldUserIdKey);
|
||||
await storageService.SaveAsync(Constants.MigratedFromV1, true);
|
||||
Migrating = false;
|
||||
messagingService.Send("migrated");
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
var task = Task.Run(() => syncService.FullSyncAsync(true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class CipherString
|
||||
{
|
||||
private string _decryptedValue;
|
||||
|
||||
public CipherString(string encryptedString)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(encryptedString))
|
||||
{
|
||||
throw new ArgumentException(nameof(encryptedString));
|
||||
}
|
||||
|
||||
var headerPieces = encryptedString.Split('.');
|
||||
string[] encPieces;
|
||||
|
||||
EncryptionType encType;
|
||||
if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out encType))
|
||||
{
|
||||
EncryptionType = encType;
|
||||
encPieces = headerPieces[1].Split('|');
|
||||
}
|
||||
else if(headerPieces.Length == 1)
|
||||
{
|
||||
encPieces = headerPieces[0].Split('|');
|
||||
EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 :
|
||||
EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Malformed header.");
|
||||
}
|
||||
|
||||
switch(EncryptionType)
|
||||
{
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
break;
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if(encPieces.Length != 3)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
Mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if(encPieces.Length != 1)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
Mac = encPieces[1];
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown encType.");
|
||||
}
|
||||
|
||||
EncryptedString = encryptedString;
|
||||
}
|
||||
|
||||
public CipherString(EncryptionType encryptionType, string initializationVector, string cipherText,
|
||||
string mac = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(initializationVector))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initializationVector));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cipherText));
|
||||
}
|
||||
|
||||
EncryptionType = encryptionType;
|
||||
EncryptedString = string.Format("{0}.{1}|{2}", (byte)EncryptionType, initializationVector, cipherText);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(mac))
|
||||
{
|
||||
EncryptedString = string.Format("{0}|{1}", EncryptedString, mac);
|
||||
}
|
||||
|
||||
CipherText = cipherText;
|
||||
InitializationVector = initializationVector;
|
||||
Mac = mac;
|
||||
}
|
||||
|
||||
public EncryptionType EncryptionType { get; private set; }
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector { get; private set; }
|
||||
public string CipherText { get; private set; }
|
||||
public string Mac { get; private set; }
|
||||
public byte[] InitializationVectorBytes => string.IsNullOrWhiteSpace(InitializationVector) ?
|
||||
null : Convert.FromBase64String(InitializationVector);
|
||||
public byte[] CipherTextBytes => Convert.FromBase64String(CipherText);
|
||||
public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class SymmetricCryptoKey
|
||||
{
|
||||
public SymmetricCryptoKey(byte[] rawBytes, EncryptionType? encType = null)
|
||||
{
|
||||
if(rawBytes == null || rawBytes.Length == 0)
|
||||
{
|
||||
throw new Exception("Must provide keyBytes.");
|
||||
}
|
||||
|
||||
if(encType == null)
|
||||
{
|
||||
if(rawBytes.Length == 32)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else if(rawBytes.Length == 64)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionType = encType.Value;
|
||||
Key = rawBytes;
|
||||
|
||||
if(EncryptionType == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key;
|
||||
MacKey = null;
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key.Take(16).ToArray();
|
||||
MacKey = Key.Skip(16).Take(16).ToArray();
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
{
|
||||
EncKey = Key.Take(32).ToArray();
|
||||
MacKey = Key.Skip(32).Take(32).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Key { get; set; }
|
||||
public string B64Key => Convert.ToBase64String(Key);
|
||||
public byte[] EncKey { get; set; }
|
||||
public byte[] MacKey { get; set; }
|
||||
public EncryptionType EncryptionType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public class SettingsShim
|
||||
{
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.ContainsKey(key);
|
||||
}
|
||||
|
||||
public string GetValueOrDefault(string key, string defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public DateTime GetValueOrDefault(string key, DateTime defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public bool GetValueOrDefault(string key, bool defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public int GetValueOrDefault(string key, int defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public long GetValueOrDefault(string key, long defaultValue)
|
||||
{
|
||||
return Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, string value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, DateTime value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, bool value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, long value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, int value)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/App/Models/PreviousPageInfo.cs
Normal file
9
src/App/Models/PreviousPageInfo.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.App.Models
|
||||
{
|
||||
public class PreviousPageInfo
|
||||
{
|
||||
public string Page { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string SearchText { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n SelfHostedEnvironment}"
|
||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
@@ -42,7 +42,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomEnvironment}"
|
||||
<Label Text="{u:I18n CustomEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class EnvironmentPage : BaseContentPage
|
||||
{
|
||||
private EnvironmentPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly EnvironmentPageViewModel _vm;
|
||||
|
||||
public EnvironmentPage()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as EnvironmentPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -33,10 +38,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
<controls:FaButton Text=""
|
||||
StyleClass="btn-muted, btn-icon, btn-icon-platform"
|
||||
HorizontalOptions="Start"
|
||||
Clicked="Settings_Clicked" />
|
||||
Clicked="Settings_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}">
|
||||
<controls:FaButton.Margin>
|
||||
<OnPlatform x:TypeArguments="Thickness">
|
||||
<On Platform="iOS" Value="0, 10, 0, 0" />
|
||||
<On Platform="Android" Value="0" />
|
||||
</OnPlatform>
|
||||
</controls:FaButton.Margin>
|
||||
</controls:FaButton>
|
||||
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
|
||||
<Image
|
||||
x:Name="_logo"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
@@ -7,12 +9,14 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HomePage : BaseContentPage
|
||||
{
|
||||
private IMessagingService _messagingService;
|
||||
|
||||
public HomePage()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
InitializeComponent();
|
||||
var theme = ThemeManager.GetTheme();
|
||||
var darkbasedTheme = theme == "dark" || theme == "black" || theme == "nord";
|
||||
_logo.Source = darkbasedTheme ? "logo_white.png" : "logo.png";
|
||||
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
public async Task DismissRegisterPageAndLogInAsync(string email)
|
||||
@@ -21,6 +25,12 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(new LoginPage(email)));
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
}
|
||||
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
Text="{Binding Pin}"
|
||||
StyleClass="box-value"
|
||||
Keyboard="Numeric"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -56,7 +58,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row" IsVisible="{Binding PinLock, Converter={StaticResource inverseBool}}">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -76,6 +80,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -87,7 +93,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{Binding LockedVerifyText}"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
@@ -7,6 +10,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LockPage : BaseContentPage
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly bool _autoPromptFingerprint;
|
||||
private readonly LockPageViewModel _vm;
|
||||
@@ -16,28 +20,13 @@ namespace Bit.App.Pages
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptFingerprint = true)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_appOptions = appOptions;
|
||||
_autoPromptFingerprint = autoPromptFingerprint;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.UnlockedAction = () =>
|
||||
{
|
||||
if(_appOptions != null)
|
||||
{
|
||||
if(_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
else if(_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions);
|
||||
};
|
||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
PinEntry = _pin;
|
||||
}
|
||||
@@ -87,10 +76,7 @@ namespace Bit.App.Pages
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
Device.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -110,5 +96,28 @@ namespace Bit.App.Pages
|
||||
await _vm.PromptFingerprintAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
if(_appOptions != null)
|
||||
{
|
||||
if(_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
else if(_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if(previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _hasKey;
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
@@ -46,6 +46,7 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -102,8 +103,7 @@ namespace Bit.App.Pages
|
||||
public async Task InitAsync(bool autoPromptFingerprint)
|
||||
{
|
||||
_pinSet = await _lockService.IsPinLockSetAsync();
|
||||
_hasKey = await _cryptoService.HasKeyAsync();
|
||||
PinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2;
|
||||
PinLock = (_pinSet.Item1 && _lockService.PinProtectedKey != null) || _pinSet.Item2;
|
||||
FingerprintLock = await _lockService.IsFingerprintLockSetAsync();
|
||||
_email = await _userService.GetEmailAsync();
|
||||
var webVault = _environmentService.GetWebVaultUrl();
|
||||
@@ -126,8 +126,19 @@ namespace Bit.App.Pages
|
||||
|
||||
if(FingerprintLock)
|
||||
{
|
||||
FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
if(Device.RuntimePlatform == Device.iOS && supportsFace)
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFaceIDToUnlock;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseBiometricsToUnlock;
|
||||
}
|
||||
else
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
if(autoPromptFingerprint)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
@@ -167,14 +178,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000),
|
||||
_lockService.PinProtectedKey);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin));
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
failed = decPin != Pin;
|
||||
_lockService.PinLocked = failed;
|
||||
if(!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
DoContinue();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -219,6 +233,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
_lockService.PinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
@@ -254,7 +277,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
||||
{
|
||||
var page = Page as LockPage;
|
||||
@@ -270,21 +293,25 @@ namespace Bit.App.Pages
|
||||
_lockService.FingerprintLocked = !success;
|
||||
if(success)
|
||||
{
|
||||
DoContinue();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
if(!_hasKey)
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
if(!hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
}
|
||||
DoContinue();
|
||||
await DoContinueAsync();
|
||||
}
|
||||
|
||||
private void DoContinue()
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_lockService.FingerprintLocked = false;
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
_messagingService.Send("unlocked");
|
||||
UnlockedAction?.Invoke();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
@@ -66,7 +68,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginPage : BaseContentPage
|
||||
{
|
||||
private LoginPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly LoginPageViewModel _vm;
|
||||
|
||||
public LoginPage(string email = null)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -55,10 +60,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -18,6 +19,7 @@ namespace Bit.App.Pages
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _showPassword;
|
||||
private string _email;
|
||||
@@ -30,6 +32,7 @@ namespace Bit.App.Pages
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -123,6 +126,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
Application.Current.MainPage = new TabsPage();
|
||||
}
|
||||
@@ -130,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -64,7 +66,9 @@
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
@@ -89,6 +93,8 @@
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -98,7 +104,9 @@
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class RegisterPage : BaseContentPage
|
||||
{
|
||||
private RegisterPageViewModel _vm;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly RegisterPageViewModel _vm;
|
||||
|
||||
public RegisterPage(HomePage homePage)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RegisterPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -51,10 +56,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@
|
||||
x:Name="_yubikeyTokenEntry"
|
||||
Text="{Binding Token}"
|
||||
StyleClass="box-value"
|
||||
IsPassword="True"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
@@ -124,6 +127,10 @@
|
||||
IsVisible="{Binding EmailMethod}"
|
||||
Clicked="ResendEmail_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n TryAgain}"
|
||||
IsVisible="{Binding ShowTryAgain}"
|
||||
Clicked="TryAgain_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
<Button Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Margin="10, 0"></Button>
|
||||
|
||||
@@ -55,11 +55,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(message.Command == "gotYubiKeyOTP")
|
||||
{
|
||||
if(_vm.YubikeyMethod)
|
||||
var token = (string)message.Data;
|
||||
if(_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) &&
|
||||
token.Length == 44 && !token.Contains(" "))
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
_vm.Token = (string)message.Data;
|
||||
_vm.Token = token;
|
||||
await _vm.SubmitAsync();
|
||||
});
|
||||
}
|
||||
@@ -138,5 +140,16 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryAgain_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
if(_vm.YubikeyMethod)
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -23,6 +24,7 @@ namespace Bit.App.Pages
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _u2fSupported = false;
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
@@ -40,6 +42,7 @@ namespace Bit.App.Pages
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
@@ -66,6 +69,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool TotpMethod => AuthenticatorMethod || EmailMethod;
|
||||
|
||||
public bool ShowTryAgain => YubikeyMethod && Device.RuntimePlatform == Device.iOS;
|
||||
|
||||
public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos :
|
||||
AppResources.YubiKeyInstruction;
|
||||
|
||||
@@ -79,6 +84,7 @@ namespace Bit.App.Pages
|
||||
nameof(YubikeyMethod),
|
||||
nameof(AuthenticatorMethod),
|
||||
nameof(TotpMethod),
|
||||
nameof(ShowTryAgain),
|
||||
});
|
||||
}
|
||||
public Command SubmitCommand { get; }
|
||||
@@ -157,7 +163,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
}
|
||||
if(DuoMethod)
|
||||
if(SelectedProviderType == null || DuoMethod)
|
||||
{
|
||||
page.RemoveContinueButton();
|
||||
}
|
||||
@@ -200,13 +206,18 @@ namespace Bit.App.Pages
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
Application.Current.MainPage = new TabsPage();
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -11,8 +13,16 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private IStorageService _storageService;
|
||||
|
||||
protected int AndroidShowModalAnimationDelay = 400;
|
||||
protected int AndroidShowPageAnimationDelay = 100;
|
||||
protected int ShowModalAnimationDelay = 400;
|
||||
protected int ShowPageAnimationDelay = 100;
|
||||
|
||||
public BaseContentPage()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.FullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastPageAction { get; set; }
|
||||
|
||||
@@ -77,21 +87,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(fromModal ? AndroidShowModalAnimationDelay : AndroidShowPageAnimationDelay);
|
||||
await Task.Delay(fromModal ? ShowModalAnimationDelay : ShowPageAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(async () => await DoWorkAsync());
|
||||
});
|
||||
}
|
||||
|
||||
protected void RequestFocus(InputView input)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
input.Focus();
|
||||
return;
|
||||
}
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(AndroidShowModalAnimationDelay);
|
||||
await Task.Delay(ShowModalAnimationDelay);
|
||||
Device.BeginInvokeOnMainThread(() => input.Focus());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +84,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Text="{u:I18n Select}"
|
||||
Clicked="Select_Clicked"
|
||||
Order="Primary"
|
||||
@@ -52,7 +54,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Options}"
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -60,6 +62,7 @@
|
||||
Text="{u:I18n Type}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_typePicker"
|
||||
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
@@ -93,8 +96,31 @@
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n Capitalize}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Capitalize}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n IncludeNumber}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding IncludeNumber}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding IsPassword}">
|
||||
<StackLayout StyleClass="box-row, box-row-slider">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -20,17 +22,29 @@ namespace Bit.App.Pages
|
||||
_vm.Page = this;
|
||||
_fromTabPage = fromTabPage;
|
||||
_selectAction = selectAction;
|
||||
var isIos = Device.RuntimePlatform == Device.iOS;
|
||||
if(selectAction != null)
|
||||
{
|
||||
if(isIos)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
}
|
||||
ToolbarItems.Add(_selectItem);
|
||||
}
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_historyItem);
|
||||
if(isIos)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
ToolbarItems.Add(_historyItem);
|
||||
}
|
||||
}
|
||||
if(isIos)
|
||||
{
|
||||
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +93,7 @@ namespace Bit.App.Pages
|
||||
if(selection == AppResources.PasswordHistory)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +105,20 @@ namespace Bit.App.Pages
|
||||
private async void History_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var page = new GeneratorHistoryPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
|
||||
private async void LengthSlider_DragCompleted(object sender, EventArgs e)
|
||||
{
|
||||
await _vm.SliderChangedAsync();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -29,6 +27,8 @@ namespace Bit.App.Pages
|
||||
private int _length = 5;
|
||||
private int _numWords = 3;
|
||||
private string _wordSeparator;
|
||||
private bool _capitalize;
|
||||
private bool _includeNumber;
|
||||
private int _typeSelectedIndex;
|
||||
private bool _doneIniting;
|
||||
|
||||
@@ -196,6 +196,32 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public bool Capitalize
|
||||
{
|
||||
get => _capitalize;
|
||||
set
|
||||
{
|
||||
if(SetProperty(ref _capitalize, value))
|
||||
{
|
||||
_options.Capitalize = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IncludeNumber
|
||||
{
|
||||
get => _includeNumber;
|
||||
set
|
||||
{
|
||||
if(SetProperty(ref _includeNumber, value))
|
||||
{
|
||||
_options.Number = value;
|
||||
var task = SaveOptionsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int TypeSelectedIndex
|
||||
{
|
||||
get => _typeSelectedIndex;
|
||||
@@ -273,6 +299,8 @@ namespace Bit.App.Pages
|
||||
Uppercase = _options.Uppercase.GetValueOrDefault();
|
||||
Lowercase = _options.Lowercase.GetValueOrDefault();
|
||||
Length = _options.Length.GetValueOrDefault(5);
|
||||
Capitalize = _options.Capitalize.GetValueOrDefault();
|
||||
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
|
||||
}
|
||||
|
||||
private void SetOptions()
|
||||
@@ -288,6 +316,8 @@ namespace Bit.App.Pages
|
||||
_options.Uppercase = Uppercase;
|
||||
_options.Lowercase = Lowercase;
|
||||
_options.Length = Length;
|
||||
_options.Capitalize = Capitalize;
|
||||
_options.IncludeNumber = IncludeNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
src/App/Pages/Settings/AutofillPage.xaml
Normal file
48
src/App/Pages/Settings/AutofillPage.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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.AutofillPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
Title="{u:I18n PasswordAutofill}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="5"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
StyleClass="text-lg"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap"
|
||||
Margin="0, 0, 0, 15" />
|
||||
<Label Text="{u:I18n AutofillTurnOn1}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn2}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn3}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn4}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n AutofillTurnOn5}"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="autofill-kb.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, 10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
20
src/App/Pages/Settings/AutofillPage.xaml.cs
Normal file
20
src/App/Pages/Settings/AutofillPage.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillPage : BaseContentPage
|
||||
{
|
||||
public AutofillPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/App/Pages/Settings/ExtensionPage.xaml
Normal file
95
src/App/Pages/Settings/ExtensionPage.xaml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?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.ExtensionPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:ExtensionPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ExtensionPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Padding="0" Spacing="0" VerticalOptions="FillAndExpand">
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding NotStarted}">
|
||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTurnOn}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-more.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndNotActivated}">
|
||||
<Label Text="{u:I18n ExtensionAlmostDone}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionTapIcon}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-act.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExtensionEnable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="20"
|
||||
Padding="20, 20, 20, 30"
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding StartedAndActivated}">
|
||||
<Label Text="{u:I18n ExtensionReady}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Label Text="{u:I18n ExtensionInSafari}"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
LineBreakMode="WordWrap" />
|
||||
<Image Source="ext-use.png"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0, -10, 0, 0"
|
||||
WidthRequest="290"
|
||||
HeightRequest="252" />
|
||||
<Button Text="{u:I18n ExntesionReenable}"
|
||||
Clicked="Show_Clicked"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Fill" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
38
src/App/Pages/Settings/ExtensionPage.xaml.cs
Normal file
38
src/App/Pages/Settings/ExtensionPage.xaml.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ExtensionPage : BaseContentPage
|
||||
{
|
||||
private readonly ExtensionPageViewModel _vm;
|
||||
|
||||
public ExtensionPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as ExtensionPageViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private void Show_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
_vm.ShowExtension();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/App/Pages/Settings/ExtensionPageViewModel.cs
Normal file
75
src/App/Pages/Settings/ExtensionPageViewModel.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ExtensionPageViewModel : BaseViewModel
|
||||
{
|
||||
private const string StartedKey = "appExtensionStarted";
|
||||
private const string ActivatedKey = "appExtensionActivated";
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
private bool _started;
|
||||
private bool _activated;
|
||||
|
||||
public ExtensionPageViewModel()
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
PageTitle = AppResources.AppExtension;
|
||||
}
|
||||
|
||||
public bool Started
|
||||
{
|
||||
get => _started;
|
||||
set => SetProperty(ref _started, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(NotStarted),
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool Activated
|
||||
{
|
||||
get => _activated;
|
||||
set => SetProperty(ref _activated, value, additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(StartedAndNotActivated),
|
||||
nameof(StartedAndActivated)
|
||||
});
|
||||
}
|
||||
|
||||
public bool NotStarted => !Started;
|
||||
public bool StartedAndNotActivated => Started && !Activated;
|
||||
public bool StartedAndActivated => Started && Activated;
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var started = await _storageService.GetAsync<bool?>(StartedKey);
|
||||
var activated = await _storageService.GetAsync<bool?>(ActivatedKey);
|
||||
Started = started.GetValueOrDefault();
|
||||
Activated = activated.GetValueOrDefault();
|
||||
}
|
||||
|
||||
public void ShowExtension()
|
||||
{
|
||||
_messagingService.Send("showAppExtension", this);
|
||||
}
|
||||
|
||||
public void EnabledExtension(bool enabled)
|
||||
{
|
||||
Started = true;
|
||||
if(!Activated && enabled)
|
||||
{
|
||||
Activated = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,15 @@
|
||||
IsDestructive="True"
|
||||
x:Name="_deleteItem" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
|
||||
x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Xamarin.Forms;
|
||||
using Bit.App.Resources;
|
||||
using System.Collections.Generic;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -19,6 +21,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.Remove(_deleteItem);
|
||||
}
|
||||
if(_vm.EditMode && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
@@ -61,5 +67,20 @@ namespace Bit.App.Pages
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if(!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
var options = new List<string> { };
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||
if(selection == AppResources.Delete)
|
||||
{
|
||||
await _vm.DeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +132,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class FoldersPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
private bool _showNoData;
|
||||
|
||||
public FoldersPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
|
||||
PageTitle = AppResources.Folders;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n Theme}"
|
||||
StyleClass="box-label" />
|
||||
@@ -35,7 +35,7 @@
|
||||
x:Name="_themeDescriptionLabel" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n DefaultUriMatchDetection}"
|
||||
StyleClass="box-label" />
|
||||
@@ -50,7 +50,7 @@
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n ClearClipboard}"
|
||||
StyleClass="box-label" />
|
||||
@@ -96,7 +96,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n AutofillService}"
|
||||
<Label Text="{u:I18n AutofillService, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
@@ -134,7 +134,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAccessibilitySettings}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n AutofillAccessibilityService}"
|
||||
<Label Text="{u:I18n AutofillAccessibilityService, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -27,6 +29,12 @@ namespace Bit.App.Pages
|
||||
_themeDescriptionLabel.Text = string.Concat(_themeDescriptionLabel.Text, " ",
|
||||
AppResources.RestartIsRequired);
|
||||
}
|
||||
else
|
||||
{
|
||||
_themePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_uriMatchPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_clearClipboardPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
}
|
||||
|
||||
protected async override void OnAppearing()
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
|
||||
PageTitle = AppResources.Options;
|
||||
var iosIos = Device.RuntimePlatform == Device.iOS;
|
||||
|
||||
ClearClipboardOptions = new List<KeyValuePair<int?, string>>
|
||||
{
|
||||
@@ -53,21 +54,21 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<int?, string>(10, AppResources.TenSeconds),
|
||||
new KeyValuePair<int?, string>(20, AppResources.TwentySeconds),
|
||||
new KeyValuePair<int?, string>(30, AppResources.ThirtySeconds),
|
||||
new KeyValuePair<int?, string>(60, AppResources.OneMinute),
|
||||
new KeyValuePair<int?, string>(120, AppResources.TwoMinutes),
|
||||
new KeyValuePair<int?, string>(300, AppResources.FiveMinutes),
|
||||
new KeyValuePair<int?, string>(60, AppResources.OneMinute)
|
||||
};
|
||||
if(!iosIos)
|
||||
{
|
||||
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(120, AppResources.TwoMinutes));
|
||||
ClearClipboardOptions.Add(new KeyValuePair<int?, string>(300, AppResources.FiveMinutes));
|
||||
}
|
||||
ThemeOptions = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(null, AppResources.Default),
|
||||
new KeyValuePair<string, string>("light", AppResources.Light),
|
||||
new KeyValuePair<string, string>("dark", AppResources.Dark),
|
||||
new KeyValuePair<string, string>("black", AppResources.Black),
|
||||
new KeyValuePair<string, string>("nord", "Nord"),
|
||||
};
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ThemeOptions.Add(new KeyValuePair<string, string>("black", AppResources.Black));
|
||||
}
|
||||
ThemeOptions.Add(new KeyValuePair<string, string>("nord", "Nord"));
|
||||
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Domain, AppResources.BaseDomain),
|
||||
@@ -300,6 +301,11 @@ namespace Bit.App.Pages
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
_messagingService.Send("updatedTheme", theme);
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.ThemeAppliedOnRestart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
<ListView
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
HasUnevenRows="True"
|
||||
RowHeight="-1"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
ItemSelected="RowSelected"
|
||||
@@ -56,14 +57,18 @@
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:SettingsPageListGroup">
|
||||
<ViewCell>
|
||||
<StackLayout Padding="0" Spacing="0">
|
||||
<BoxView StyleClass="list-section-separator"
|
||||
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
|
||||
<StackLayout StyleClass="list-row-header">
|
||||
<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>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -67,11 +67,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if(item.Name == AppResources.PasswordAutofill)
|
||||
{
|
||||
// await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AutofillPage()));
|
||||
}
|
||||
else if(item.Name == AppResources.AppExtension)
|
||||
{
|
||||
// await Navigation.PushModalAsync(new NavigationPage(new OptionsPage()));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage()));
|
||||
}
|
||||
else if(item.Name == AppResources.Options)
|
||||
{
|
||||
@@ -137,10 +137,22 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await _vm.UpdatePinAsync();
|
||||
}
|
||||
else if(item.Name.Contains(AppResources.Fingerprint) || item.Name.Contains(AppResources.TouchID) ||
|
||||
item.Name.Contains(AppResources.FaceID))
|
||||
else
|
||||
{
|
||||
await _vm.UpdateFingerprintAsync();
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
fingerprintName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
if(item.Name == string.Format(AppResources.UnlockWith, fingerprintName))
|
||||
{
|
||||
await _vm.UpdateFingerprintAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Bit.App.Pages
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ISyncService _syncService;
|
||||
|
||||
private string _fingerprintName;
|
||||
private bool _supportsFingerprint;
|
||||
private bool _pin;
|
||||
private bool _fingerprint;
|
||||
@@ -57,19 +56,13 @@ namespace Bit.App.Pages
|
||||
|
||||
GroupedItems = new ExtendedObservableCollection<SettingsPageListGroup>();
|
||||
PageTitle = AppResources.Settings;
|
||||
|
||||
_fingerprintName = AppResources.Fingerprint;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
_fingerprintName = _deviceActionService.SupportsFaceId() ? AppResources.FaceID : AppResources.TouchID;
|
||||
}
|
||||
}
|
||||
|
||||
public ExtendedObservableCollection<SettingsPageListGroup> GroupedItems { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_supportsFingerprint = await _platformUtilsService.SupportsFingerprintAsync();
|
||||
_supportsFingerprint = await _platformUtilsService.SupportsBiometricAsync();
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if(lastSync != null)
|
||||
{
|
||||
@@ -221,21 +214,24 @@ namespace Bit.App.Pages
|
||||
var masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
|
||||
AppResources.Yes, AppResources.No);
|
||||
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email,
|
||||
kdf.GetValueOrDefault(Core.Enums.KdfType.PBKDF2_SHA256),
|
||||
kdfIterations.GetValueOrDefault(5000));
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
|
||||
if(masterPassOnRestart)
|
||||
{
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _storageService.SaveAsync(Constants.ProtectedPin, encPin.EncryptedString);
|
||||
_lockService.PinProtectedKey = pinProtectedKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email,
|
||||
kdf.GetValueOrDefault(Core.Enums.KdfType.PBKDF2_SHA256),
|
||||
kdfIterations.GetValueOrDefault(5000));
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
await _storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString);
|
||||
}
|
||||
}
|
||||
@@ -246,8 +242,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(!_pin)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PinProtectedKey);
|
||||
await _storageService.RemoveAsync(Constants.ProtectedPin);
|
||||
await _cryptoService.ClearPinProtectedKeyAsync();
|
||||
await _lockService.ClearAsync();
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
@@ -259,9 +255,9 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_fingerprint = false;
|
||||
}
|
||||
else if(await _platformUtilsService.SupportsFingerprintAsync())
|
||||
else if(await _platformUtilsService.SupportsBiometricAsync())
|
||||
{
|
||||
_fingerprint = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
_fingerprint = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
|
||||
}
|
||||
if(_fingerprint == current)
|
||||
@@ -327,11 +323,21 @@ namespace Bit.App.Pages
|
||||
new SettingsPageListItem { Name = AppResources.LockNow },
|
||||
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
|
||||
};
|
||||
if(_supportsFingerprint)
|
||||
if(_supportsFingerprint || _fingerprint)
|
||||
{
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
fingerprintName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
|
||||
AppResources.TouchID;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, _fingerprintName),
|
||||
Name = string.Format(AppResources.UnlockWith, fingerprintName),
|
||||
SubLabel = _fingerprint ? AppResources.Enabled : AppResources.Disabled
|
||||
};
|
||||
securityItems.Insert(1, item);
|
||||
|
||||
@@ -70,7 +70,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace Bit.App.Pages
|
||||
private NavigationPage _groupingsPage;
|
||||
private NavigationPage _generatorPage;
|
||||
|
||||
public TabsPage(AppOptions appOptions = null)
|
||||
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
|
||||
{
|
||||
_groupingsPage = new NavigationPage(new GroupingsPage(true))
|
||||
_groupingsPage = new NavigationPage(new GroupingsPage(true, previousPage: previousPage))
|
||||
{
|
||||
Title = AppResources.MyVault,
|
||||
Icon = "lock.png"
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n ItemInformation}"
|
||||
<Label Text="{u:I18n ItemInformation, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
@@ -112,28 +112,36 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}" />
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding CheckPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Command="{Binding GeneratePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}" />
|
||||
</Grid>
|
||||
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
@@ -153,6 +161,8 @@
|
||||
<controls:MonoEntry
|
||||
x:Name="_loginTotpEntry"
|
||||
Text="{Binding Cipher.Login.Totp}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -162,7 +172,9 @@
|
||||
Clicked="ScanTotp_Clicked"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsCard}" Spacing="0" Padding="0">
|
||||
@@ -236,14 +248,18 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Keyboard="Numeric"
|
||||
IsPassword="{Binding ShowCardCode, Converter={StaticResource inverseBool}}" />
|
||||
IsPassword="{Binding ShowCardCode, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardCodeIcon}"
|
||||
Command="{Binding ToggleCardCodeCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsIdentity}" Spacing="0" Padding="0">
|
||||
@@ -335,6 +351,7 @@
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_identityEmailEntry"
|
||||
Keyboard="Email"
|
||||
Text="{Binding Cipher.Identity.Email}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
@@ -414,7 +431,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding IsLogin}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n URIs}"
|
||||
<Label Text="{u:I18n URIs, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Uris}">
|
||||
@@ -447,7 +464,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</controls:RepeaterView.ItemTemplate>
|
||||
@@ -457,7 +476,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Miscellaneous}"
|
||||
<Label Text="{u:I18n Miscellaneous, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -484,7 +503,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Notes}"
|
||||
<Label Text="{u:I18n Notes, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -494,10 +513,11 @@
|
||||
Text="{Binding Cipher.Notes}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowNotesSeparator}" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomFields}"
|
||||
<Label Text="{u:I18n CustomFields, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||
@@ -541,7 +561,9 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
<Switch
|
||||
IsToggled="{Binding BooleanValue}"
|
||||
Grid.Row="0"
|
||||
@@ -555,7 +577,9 @@
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -563,7 +587,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsBooleanType}" />
|
||||
</StackLayout>
|
||||
@@ -575,7 +601,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Ownership}"
|
||||
<Label Text="{u:I18n Ownership, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
@@ -591,7 +617,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowCollections}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Collections}"
|
||||
<Label Text="{u:I18n Collections, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0" Padding="0"
|
||||
|
||||
@@ -8,6 +8,8 @@ using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -39,7 +41,7 @@ namespace Bit.App.Pages
|
||||
_vm = BindingContext as AddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.CipherId = cipherId;
|
||||
_vm.FolderId = folderId;
|
||||
_vm.FolderId = folderId == "none" ? null : folderId;
|
||||
_vm.CollectionIds = collectionId != null ? new HashSet<string>(new List<string> { collectionId }) : null;
|
||||
_vm.Type = type;
|
||||
_vm.DefaultName = name ?? appOptions?.SaveName;
|
||||
@@ -58,6 +60,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.Add(_moreItem);
|
||||
}
|
||||
_vm.ShowNotesSeparator = true;
|
||||
|
||||
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
_ownershipPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
|
||||
_typePicker.ItemDisplayBinding = new Binding("Key");
|
||||
@@ -125,6 +131,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
public bool FromAutofillFramework { get; set; }
|
||||
public AddEditPageViewModel ViewModel => _vm;
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
@@ -155,7 +162,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(FromAutofillFramework)
|
||||
{
|
||||
Application.Current.MainPage = new TabsPage();
|
||||
Xamarin.Forms.Application.Current.MainPage = new TabsPage();
|
||||
return true;
|
||||
}
|
||||
return base.OnBackButtonPressed();
|
||||
@@ -165,7 +172,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new PasswordHistoryPage(_vm.CipherId)));
|
||||
await Navigation.PushModalAsync(
|
||||
new Xamarin.Forms.NavigationPage(new PasswordHistoryPage(_vm.CipherId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +200,7 @@ namespace Bit.App.Pages
|
||||
if(DoOnce())
|
||||
{
|
||||
var page = new AttachmentsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +209,7 @@ namespace Bit.App.Pages
|
||||
if(DoOnce())
|
||||
{
|
||||
var page = new SharePage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +229,7 @@ namespace Bit.App.Pages
|
||||
if(DoOnce())
|
||||
{
|
||||
var page = new CollectionsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +245,7 @@ namespace Bit.App.Pages
|
||||
await _vm.UpdateTotpKeyAsync(key);
|
||||
});
|
||||
});
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +258,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.Share : AppResources.Collections);
|
||||
}
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
|
||||
@@ -264,17 +272,17 @@ namespace Bit.App.Pages
|
||||
else if(selection == AppResources.Attachments)
|
||||
{
|
||||
var page = new AttachmentsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
else if(selection == AppResources.Collections)
|
||||
{
|
||||
var page = new CollectionsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
else if(selection == AppResources.Share)
|
||||
{
|
||||
var page = new SharePage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private CipherView _cipher;
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
private bool _showCardCode;
|
||||
private int _typeSelectedIndex;
|
||||
@@ -33,6 +35,7 @@ namespace Bit.App.Pages
|
||||
private int _folderSelectedIndex;
|
||||
private int _ownershipSelectedIndex;
|
||||
private bool _hasCollections;
|
||||
private string _previousCipherId;
|
||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||
private string[] _additionalCipherProperties = new string[]
|
||||
{
|
||||
@@ -73,6 +76,7 @@ namespace Bit.App.Pages
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
@@ -224,6 +228,11 @@ namespace Bit.App.Pages
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
|
||||
}
|
||||
public bool ShowNotesSeparator
|
||||
{
|
||||
get => _showNotesSeparator;
|
||||
set => SetProperty(ref _showNotesSeparator, value);
|
||||
}
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
@@ -359,9 +368,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(Cipher.Fields != null)
|
||||
{
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(f)));
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
|
||||
}
|
||||
}
|
||||
|
||||
if(EditMode && _previousCipherId != CipherId)
|
||||
{
|
||||
var task = _eventService.CollectAsync(EventType.Cipher_ClientViewed, CipherId);
|
||||
}
|
||||
_previousCipherId = CipherId;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -385,12 +401,13 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
Cipher.Fields = Fields != null && Fields.Any() ? Fields.Select(f => f.Field).ToList() : null;
|
||||
Cipher.Fields = Fields != null && Fields.Any() ?
|
||||
Fields.Where(f => f != null).Select(f => f.Field).ToList() : null;
|
||||
if(Cipher.Login != null)
|
||||
{
|
||||
Cipher.Login.Uris = Uris?.ToList();
|
||||
if(!EditMode && Cipher.Type == CipherType.Login && (Cipher.Login.Uris?.Count ?? 0) == 1 &&
|
||||
string.IsNullOrWhiteSpace(Cipher.Login.Uris.First().Uri))
|
||||
if(!EditMode && Cipher.Type == CipherType.Login && Cipher.Login.Uris != null &&
|
||||
Cipher.Login.Uris.Count == 1 && string.IsNullOrWhiteSpace(Cipher.Login.Uris[0].Uri))
|
||||
{
|
||||
Cipher.Login.Uris = null;
|
||||
}
|
||||
@@ -398,7 +415,7 @@ namespace Bit.App.Pages
|
||||
|
||||
if(!EditMode && Cipher.OrganizationId != null)
|
||||
{
|
||||
if(!Collections?.Any(c => c.Checked) ?? true)
|
||||
if(Collections == null || !Collections.Any(c => c != null && c.Checked))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
|
||||
AppResources.Ok);
|
||||
@@ -406,7 +423,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
Cipher.CollectionIds = Collections.Any() ?
|
||||
new HashSet<string>(Collections.Where(c => c.Checked).Select(c => c.Collection.Id)) : null;
|
||||
new HashSet<string>(Collections.Where(c => c != null && c.Checked && c.Collection?.Id != null)
|
||||
.Select(c => c.Collection.Id)) : null;
|
||||
}
|
||||
|
||||
var cipher = await _cipherService.EncryptAsync(Cipher);
|
||||
@@ -422,9 +440,9 @@ namespace Bit.App.Pages
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null,
|
||||
EditMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
|
||||
_messagingService.Send(EditMode ? "editedCipher" : "addedCipher");
|
||||
_messagingService.Send(EditMode ? "editedCipher" : "addedCipher", Cipher.Id);
|
||||
|
||||
if((Page as AddEditPage).FromAutofillFramework)
|
||||
if(Page is AddEditPage page && page.FromAutofillFramework)
|
||||
{
|
||||
// Close and go back to app
|
||||
_deviceActionService.CloseAutofill();
|
||||
@@ -438,7 +456,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -463,13 +485,17 @@ namespace Bit.App.Pages
|
||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
||||
_messagingService.Send("deletedCipher");
|
||||
_messagingService.Send("deletedCipher", Cipher);
|
||||
return true;
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -585,7 +611,7 @@ namespace Bit.App.Pages
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
}
|
||||
var type = _fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
|
||||
Fields.Add(new AddEditPageFieldViewModel(new FieldView
|
||||
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
|
||||
{
|
||||
Type = type,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name
|
||||
@@ -596,11 +622,19 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
if(EditMode && ShowPassword)
|
||||
{
|
||||
var task = _eventService.CollectAsync(EventType.Cipher_ClientToggledPasswordVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleCardCode()
|
||||
{
|
||||
ShowCardCode = !ShowCardCode;
|
||||
if(EditMode && ShowCardCode)
|
||||
{
|
||||
var task = _eventService.CollectAsync(EventType.Cipher_ClientToggledCardCodeVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateTotpKeyAsync(string key)
|
||||
@@ -714,6 +748,7 @@ namespace Bit.App.Pages
|
||||
public class AddEditPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private FieldView _field;
|
||||
private CipherView _cipher;
|
||||
private bool _showHiddenValue;
|
||||
private bool _booleanValue;
|
||||
private string[] _additionalFieldProperties = new string[]
|
||||
@@ -723,8 +758,9 @@ namespace Bit.App.Pages
|
||||
nameof(IsTextType),
|
||||
};
|
||||
|
||||
public AddEditPageFieldViewModel(FieldView field)
|
||||
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
|
||||
{
|
||||
_cipher = cipher;
|
||||
Field = field;
|
||||
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
|
||||
BooleanValue = IsBooleanType && field.Value == "true";
|
||||
@@ -769,6 +805,11 @@ namespace Bit.App.Pages
|
||||
public void ToggleHiddenValue()
|
||||
{
|
||||
ShowHiddenValue = !ShowHiddenValue;
|
||||
if(ShowHiddenValue && _cipher?.Id != null)
|
||||
{
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
var task = eventService.CollectAsync(EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerFieldChanged()
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row"
|
||||
<StackLayout StyleClass="box-row" Padding="10, 20"
|
||||
IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoAttachments}" />
|
||||
<Label Text="{u:I18n NoAttachments}" HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
@@ -54,7 +54,9 @@
|
||||
Text=""
|
||||
Command="{Binding BindingContext.DeleteAttachmentCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center" />
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Delete}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -64,7 +66,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n AddNewAttachment}"
|
||||
<Label Text="{u:I18n AddNewAttachment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
|
||||
@@ -45,7 +45,10 @@ namespace Bit.App.Pages
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
_broadcasterService.Unsubscribe(nameof(AttachmentsPage));
|
||||
if(Device.RuntimePlatform != Device.iOS)
|
||||
{
|
||||
_broadcasterService.Unsubscribe(nameof(AttachmentsPage));
|
||||
}
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, EventArgs e)
|
||||
|
||||
@@ -124,7 +124,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -164,7 +168,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
|
||||
|
||||
<DataTemplate x:Key="cipherTemplate"
|
||||
x:DataType="pages:GroupingsPageListItem">
|
||||
<controls:CipherViewCell
|
||||
@@ -63,13 +63,19 @@
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:GroupingsPageListGroup">
|
||||
<ViewCell>
|
||||
<StackLayout StyleClass="list-row-header">
|
||||
<Label
|
||||
Text="{Binding Name}"
|
||||
StyleClass="list-header, list-header-platform" />
|
||||
<Label
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
<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>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch(Exception e) when(e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}, _mainContent);
|
||||
|
||||
@@ -157,8 +157,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(),
|
||||
AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
||||
|
||||
@@ -20,34 +20,33 @@
|
||||
<u:DateTimeConverter x:Key="dateTime" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
x:Name="_titleLayout"
|
||||
x:Key="titleLayout">
|
||||
<controls:MiButton
|
||||
StyleClass="btn-title, btn-title-platform"
|
||||
Text=""
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Clicked="BackButton_Clicked"
|
||||
x:Name="_backButton" />
|
||||
<controls:ExtendedSearchBar
|
||||
x:Name="_searchBar"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
TextChanged="SearchBar_TextChanged"
|
||||
SearchButtonPressed="SearchBar_SearchButtonPressed"
|
||||
Placeholder="{Binding PageTitle}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
|
||||
x:Name="_separator" x:Key="separator" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<NavigationPage.TitleView>
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
x:Name="_titleLayout">
|
||||
<controls:MiButton
|
||||
StyleClass="btn-title, btn-title-platform"
|
||||
Text=""
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Clicked="BackButton_Clicked"
|
||||
x:Name="_backButton" />
|
||||
<SearchBar
|
||||
x:Name="_searchBar"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Transparent"
|
||||
TextChanged="SearchBar_TextChanged"
|
||||
SearchButtonPressed="SearchBar_SearchButtonPressed"
|
||||
Placeholder="{Binding PageTitle}" />
|
||||
</StackLayout>
|
||||
</NavigationPage.TitleView>
|
||||
|
||||
<StackLayout x:Name="_mainLayout">
|
||||
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
|
||||
<controls:FaLabel IsVisible="{Binding ShowSearchDirection}"
|
||||
Text=""
|
||||
StyleClass="text-muted"
|
||||
@@ -62,13 +61,13 @@
|
||||
HorizontalOptions="CenterAndExpand"
|
||||
HorizontalTextAlignment="Center" />
|
||||
<ListView x:Name="_listView"
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Ciphers}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
ItemSelected="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding Ciphers}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
CachingStrategy="RecycleElement"
|
||||
ItemSelected="RowSelected"
|
||||
StyleClass="list, list-platform">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="views:CipherView">
|
||||
<controls:CipherViewCell
|
||||
|
||||
@@ -43,7 +43,13 @@ namespace Bit.App.Pages
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
ToolbarItems.Add(_closeItem);
|
||||
_titleLayout.Children.Remove(_backButton);
|
||||
_searchBar.Placeholder = AppResources.Search;
|
||||
_mainLayout.Children.Insert(0, _searchBar);
|
||||
_mainLayout.Children.Insert(1, _separator);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationPage.SetTitleView(this, _titleLayout);
|
||||
}
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
}
|
||||
|
||||
@@ -75,6 +75,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
WebsiteIconsEnabled = !(await _stateService.GetAsync<bool?>(Constants.DisableFaviconKey))
|
||||
.GetValueOrDefault();
|
||||
if(!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
|
||||
{
|
||||
Search((Page as CiphersPage).SearchBar.Text, 500);
|
||||
}
|
||||
}
|
||||
|
||||
public void Search(string searchText, int? timeout = null)
|
||||
@@ -126,7 +130,7 @@ namespace Bit.App.Pages
|
||||
if(!string.IsNullOrWhiteSpace(AutofillUrl))
|
||||
{
|
||||
var options = new List<string> { AppResources.Autofill };
|
||||
if(cipher.Type == CipherType.Login &&
|
||||
if(cipher.Type == CipherType.Login &&
|
||||
Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
options.Add(AppResources.AutofillAndSave);
|
||||
@@ -164,8 +168,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(),
|
||||
AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(_deviceActionService.SystemMajorVersion() < 21)
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row"
|
||||
<StackLayout StyleClass="box-row" Padding="10, 20"
|
||||
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoCollectionsToList}" />
|
||||
<Label Text="{u:I18n NoCollectionsToList}" HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
|
||||
@@ -58,7 +58,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if(!Collections.Any(c => c.Checked))
|
||||
var selectedCollectionIds = Collections?.Where(c => c.Checked).Select(c => c.Collection.Id);
|
||||
if(!selectedCollectionIds?.Any() ?? true)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
|
||||
AppResources.Ok);
|
||||
@@ -71,8 +72,7 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
_cipherDomain.CollectionIds = new HashSet<string>(
|
||||
Collections.Where(c => c.Checked).Select(c => c.Collection.Id));
|
||||
_cipherDomain.CollectionIds = new HashSet<string>(selectedCollectionIds);
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
@@ -85,7 +85,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,9 +95,10 @@
|
||||
IsVisible="{Binding ShowList}"
|
||||
ItemsSource="{Binding GroupedItems}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HasUnevenRows="true"
|
||||
HasUnevenRows="True"
|
||||
RowHeight="-1"
|
||||
RefreshCommand="{Binding RefreshCommand}"
|
||||
IsPullToRefreshEnabled="true"
|
||||
IsPullToRefreshEnabled="True"
|
||||
IsRefreshing="{Binding Refreshing}"
|
||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||
IsGroupingEnabled="True"
|
||||
@@ -110,10 +111,13 @@
|
||||
<ListView.GroupHeaderTemplate>
|
||||
<DataTemplate x:DataType="pages:GroupingsPageListGroup">
|
||||
<ViewCell>
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<BoxView StyleClass="list-section-separator"
|
||||
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
|
||||
<StackLayout StyleClass="list-row-header">
|
||||
<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"
|
||||
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" />
|
||||
@@ -121,6 +125,7 @@
|
||||
Text="{Binding ItemCount}"
|
||||
StyleClass="list-header-sub" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
|
||||
</StackLayout>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -22,8 +23,10 @@ namespace Bit.App.Pages
|
||||
private readonly GroupingsPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
|
||||
private PreviousPageInfo _previousPage;
|
||||
|
||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
||||
string collectionId = null, string pageTitle = null)
|
||||
string collectionId = null, string pageTitle = null, PreviousPageInfo previousPage = null)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
@@ -41,6 +44,7 @@ namespace Bit.App.Pages
|
||||
_vm.Type = type;
|
||||
_vm.FolderId = folderId;
|
||||
_vm.CollectionId = collectionId;
|
||||
_previousPage = previousPage;
|
||||
if(pageTitle != null)
|
||||
{
|
||||
_vm.PageTitle = pageTitle;
|
||||
@@ -78,15 +82,14 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if(message.Command == "syncCompleted")
|
||||
{
|
||||
if(!_vm.LoadedOnce)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
IsBusy = false;
|
||||
var task = _vm.LoadAsync();
|
||||
if(_vm.LoadedOnce)
|
||||
{
|
||||
var task = _vm.LoadAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -102,7 +105,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch(Exception e) when(e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
@@ -126,6 +129,7 @@ namespace Bit.App.Pages
|
||||
await _syncService.FullSyncAsync(true);
|
||||
}
|
||||
}
|
||||
await ShowPreviousPageAsync();
|
||||
}, _mainContent);
|
||||
|
||||
if(!_vm.MainPage)
|
||||
@@ -245,5 +249,22 @@ namespace Bit.App.Pages
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPreviousPageAsync()
|
||||
{
|
||||
if(_previousPage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId)));
|
||||
}
|
||||
else if(_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
|
||||
}
|
||||
_previousPage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace Bit.App.Pages
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILockService _lockService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
@@ -48,6 +50,8 @@ namespace Bit.App.Pages
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
@@ -134,6 +138,15 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var authed = await _userService.IsAuthenticatedAsync();
|
||||
if(!authed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(await _lockService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
@@ -305,9 +318,17 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
await _syncService.FullSyncAsync(false);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
try
|
||||
{
|
||||
await _syncService.FullSyncAsync(false, true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("error", null, AppResources.SyncingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
@@ -328,7 +349,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Folders = await _folderService.GetAllDecryptedAsync();
|
||||
NestedFolders = await _folderService.GetAllNestedAsync();
|
||||
HasFolders = NestedFolders.Any();
|
||||
HasFolders = NestedFolders.Any(f => f.Node?.Id != null);
|
||||
Collections = await _collectionService.GetAllDecryptedAsync();
|
||||
NestedCollections = await _collectionService.GetAllNestedAsync(Collections);
|
||||
HasCollections = NestedCollections.Any();
|
||||
@@ -366,6 +387,7 @@ namespace Bit.App.Pages
|
||||
if(collectionNode?.Node != null)
|
||||
{
|
||||
PageTitle = collectionNode.Node.Name;
|
||||
NestedCollections = (collectionNode.Children?.Count ?? 0) > 0 ? collectionNode.Children : null;
|
||||
}
|
||||
Filter = c => c.CollectionIds?.Contains(CollectionId) ?? false;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
</ViewCell>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -27,12 +27,15 @@
|
||||
|
||||
<ScrollView x:Name="_scrollView">
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding HasOrganizations, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoOrgsToList}" />
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding HasOrganizations, Converter={StaticResource inverseBool}}">
|
||||
<StackLayout StyleClass="box-row" Padding="10, 20">
|
||||
<Label Text="{u:I18n NoOrgsToList}" HorizontalTextAlignment="Center" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding HasOrganizations}">
|
||||
<StackLayout StyleClass="box-row, box-row-input, box-row-input-options-platform">
|
||||
<Label
|
||||
Text="{u:I18n Organization}"
|
||||
StyleClass="box-label" />
|
||||
@@ -49,7 +52,7 @@
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding OrganizationId, Converter={StaticResource notNull}}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Collections}"
|
||||
<Label Text="{u:I18n Collections, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -17,6 +19,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
_organizationPicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
_organizationPicker.ItemDisplayBinding = new Binding("Key");
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if(!Collections?.Any(c => c.Checked) ?? true)
|
||||
var selectedCollectionIds = Collections?.Where(c => c.Checked).Select(c => c.Collection.Id);
|
||||
if(!selectedCollectionIds?.Any() ?? true)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
|
||||
AppResources.Ok);
|
||||
@@ -102,8 +103,7 @@ namespace Bit.App.Pages
|
||||
var cipherDomain = await _cipherService.GetAsync(CipherId);
|
||||
var cipherView = await cipherDomain.DecryptAsync();
|
||||
|
||||
var checkedCollectionIds = new HashSet<string>(
|
||||
Collections.Where(c => c.Checked).Select(c => c.Collection.Id));
|
||||
var checkedCollectionIds = new HashSet<string>(selectedCollectionIds);
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
@@ -116,12 +116,19 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch(System.Exception e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Message, AppResources.Ok);
|
||||
if(e.Message != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Message, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<StackLayout Spacing="20" x:Name="_mainLayout">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n ItemInformation}"
|
||||
<Label Text="{u:I18n ItemInformation, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
@@ -88,7 +88,9 @@
|
||||
CommandParameter="LoginUsername"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
||||
@@ -120,6 +122,7 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
LineBreakMode="CharacterWrap"
|
||||
IsVisible="{Binding ShowPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
@@ -127,14 +130,18 @@
|
||||
Command="{Binding CheckPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -142,7 +149,9 @@
|
||||
CommandParameter="LoginPassword"
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
|
||||
@@ -183,7 +192,9 @@
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
||||
</StackLayout>
|
||||
@@ -226,7 +237,9 @@
|
||||
CommandParameter="CardNumber"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyNumber}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Number, Converter={StaticResource stringHasValue}}" />
|
||||
@@ -286,7 +299,9 @@
|
||||
Command="{Binding ToggleCardCodeCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -294,7 +309,9 @@
|
||||
CommandParameter="CardCode"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopySecurityCode}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Card.Code, Converter={StaticResource stringHasValue}}" />
|
||||
@@ -418,7 +435,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowUris}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n URIs}"
|
||||
<Label Text="{u:I18n URIs, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Login.Uris}">
|
||||
@@ -460,7 +477,9 @@
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding CanLaunch, Mode=OneWay}" />
|
||||
IsVisible="{Binding CanLaunch, Mode=OneWay}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Launch}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -468,7 +487,9 @@
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Copy}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -479,7 +500,7 @@
|
||||
<StackLayout StyleClass="box"
|
||||
IsVisible="{Binding Cipher.Notes, Converter={StaticResource stringHasValue}}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Notes}"
|
||||
<Label Text="{u:I18n Notes, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row">
|
||||
@@ -496,7 +517,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding Cipher.HasFields}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n CustomFields}"
|
||||
<Label Text="{u:I18n CustomFields, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Fields}">
|
||||
@@ -550,7 +571,9 @@
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -559,7 +582,9 @@
|
||||
IsVisible="{Binding ShowCopyButton}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2" />
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Copy}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@@ -569,7 +594,7 @@
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Attachments}"
|
||||
<Label Text="{u:I18n Attachments, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Cipher.Attachments}">
|
||||
@@ -592,7 +617,9 @@
|
||||
Text=""
|
||||
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center" />
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Download}" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPageViewModel ViewModel => _vm;
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
@@ -154,7 +156,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
var options = new List<string> { AppResources.Attachments };
|
||||
options.Add(_vm.Cipher.OrganizationId != null ? AppResources.Share : AppResources.Collections);
|
||||
options.Add(_vm.Cipher.OrganizationId == null ? AppResources.Share : AppResources.Collections);
|
||||
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
|
||||
AppResources.Delete, options.ToArray());
|
||||
if(selection == AppResources.Delete)
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private CipherView _cipher;
|
||||
private List<ViewPageFieldViewModel> _fields;
|
||||
private bool _canAccessPremium;
|
||||
@@ -32,6 +33,7 @@ namespace Bit.App.Pages
|
||||
private string _totpSec;
|
||||
private bool _totpLow;
|
||||
private DateTime? _totpInterval = null;
|
||||
private string _previousCipherId;
|
||||
|
||||
public ViewPageViewModel()
|
||||
{
|
||||
@@ -42,6 +44,7 @@ namespace Bit.App.Pages
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
CopyCommand = new Command<string>((id) => CopyAsync(id, null));
|
||||
CopyUriCommand = new Command<LoginUriView>(CopyUri);
|
||||
CopyFieldCommand = new Command<FieldView>(CopyField);
|
||||
@@ -217,7 +220,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
Cipher = await cipher.DecryptAsync();
|
||||
CanAccessPremium = await _userService.CanAccessPremiumAsync();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(f)).ToList();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(Cipher, f)).ToList();
|
||||
|
||||
if(Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||
@@ -236,6 +239,11 @@ namespace Bit.App.Pages
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if(_previousCipherId != CipherId)
|
||||
{
|
||||
var task = _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientViewed, CipherId);
|
||||
}
|
||||
_previousCipherId = CipherId;
|
||||
finishedLoadingAction?.Invoke();
|
||||
return true;
|
||||
}
|
||||
@@ -248,11 +256,20 @@ namespace Bit.App.Pages
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
if(ShowPassword)
|
||||
{
|
||||
var task = _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientToggledPasswordVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleCardCode()
|
||||
{
|
||||
ShowCardCode = !ShowCardCode;
|
||||
if(ShowCardCode)
|
||||
{
|
||||
var task = _eventService.CollectAsync(
|
||||
Core.Enums.EventType.Cipher_ClientToggledCardCodeVisible, CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync()
|
||||
@@ -275,13 +292,17 @@ namespace Bit.App.Pages
|
||||
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted);
|
||||
_messagingService.Send("deletedCipher");
|
||||
_messagingService.Send("deletedCipher", Cipher);
|
||||
return true;
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -434,7 +455,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
name = AppResources.URI;
|
||||
}
|
||||
else if(id == "FieldValue")
|
||||
else if(id == "FieldValue" || id == "H_FieldValue")
|
||||
{
|
||||
name = AppResources.Value;
|
||||
}
|
||||
@@ -456,6 +477,18 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name));
|
||||
}
|
||||
if(id == "LoginPassword")
|
||||
{
|
||||
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, CipherId);
|
||||
}
|
||||
else if(id == "CardCode")
|
||||
{
|
||||
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, CipherId);
|
||||
}
|
||||
else if(id == "H_FieldValue")
|
||||
{
|
||||
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedHiddenField, CipherId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +499,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void CopyField(FieldView field)
|
||||
{
|
||||
CopyAsync("FieldValue", field.Value);
|
||||
CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value);
|
||||
}
|
||||
|
||||
private void LaunchUri(LoginUriView uri)
|
||||
@@ -481,10 +514,12 @@ namespace Bit.App.Pages
|
||||
public class ViewPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private FieldView _field;
|
||||
private CipherView _cipher;
|
||||
private bool _showHiddenValue;
|
||||
|
||||
public ViewPageFieldViewModel(FieldView field)
|
||||
public ViewPageFieldViewModel(CipherView cipher, FieldView field)
|
||||
{
|
||||
_cipher = cipher;
|
||||
Field = field;
|
||||
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
|
||||
}
|
||||
@@ -526,6 +561,12 @@ namespace Bit.App.Pages
|
||||
public void ToggleHiddenValue()
|
||||
{
|
||||
ShowHiddenValue = !ShowHiddenValue;
|
||||
if(ShowHiddenValue)
|
||||
{
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
var task = eventService.CollectAsync(
|
||||
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
src/App/Resources/AppResources.Designer.cs
generated
94
src/App/Resources/AppResources.Designer.cs
generated
@@ -19,7 +19,7 @@ namespace Bit.App.Resources {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class AppResources {
|
||||
@@ -537,6 +537,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Biometrics.
|
||||
/// </summary>
|
||||
public static string Biometrics {
|
||||
get {
|
||||
return ResourceManager.GetString("Biometrics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use biometrics to verify..
|
||||
/// </summary>
|
||||
public static string BiometricsDirection {
|
||||
get {
|
||||
return ResourceManager.GetString("BiometricsDirection", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden.
|
||||
/// </summary>
|
||||
@@ -807,6 +825,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Capitalize.
|
||||
/// </summary>
|
||||
public static string Capitalize {
|
||||
get {
|
||||
return ResourceManager.GetString("Capitalize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cardholder Name.
|
||||
/// </summary>
|
||||
@@ -1239,6 +1266,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Download.
|
||||
/// </summary>
|
||||
public static string Download {
|
||||
get {
|
||||
return ResourceManager.GetString("Download", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading....
|
||||
/// </summary>
|
||||
@@ -1932,6 +1968,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Include Number.
|
||||
/// </summary>
|
||||
public static string IncludeNumber {
|
||||
get {
|
||||
return ResourceManager.GetString("IncludeNumber", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please connect to the internet before continuing..
|
||||
/// </summary>
|
||||
@@ -2247,6 +2292,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your login session has expired..
|
||||
/// </summary>
|
||||
public static string LoginExpired {
|
||||
get {
|
||||
return ResourceManager.GetString("LoginExpired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Login.
|
||||
/// </summary>
|
||||
@@ -3327,6 +3381,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shared.
|
||||
/// </summary>
|
||||
public static string Shared {
|
||||
get {
|
||||
return ResourceManager.GetString("Shared", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared..
|
||||
/// </summary>
|
||||
@@ -3498,6 +3561,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your theme changes will apply when the app is restarted..
|
||||
/// </summary>
|
||||
public static string ThemeAppliedOnRestart {
|
||||
get {
|
||||
return ResourceManager.GetString("ThemeAppliedOnRestart", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Change the application's color theme..
|
||||
/// </summary>
|
||||
@@ -3525,6 +3597,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Toggle Visibility.
|
||||
/// </summary>
|
||||
public static string ToggleVisibility {
|
||||
get {
|
||||
return ResourceManager.GetString("ToggleVisibility", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Tools.
|
||||
/// </summary>
|
||||
@@ -3553,7 +3634,7 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Try Again.
|
||||
/// Looks up a localized string similar to Try again.
|
||||
/// </summary>
|
||||
public static string TryAgain {
|
||||
get {
|
||||
@@ -3759,6 +3840,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use Biometrics To Unlock.
|
||||
/// </summary>
|
||||
public static string UseBiometricsToUnlock {
|
||||
get {
|
||||
return ResourceManager.GetString("UseBiometricsToUnlock", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use Face ID To Unlock.
|
||||
/// </summary>
|
||||
|
||||
@@ -1556,4 +1556,35 @@
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
||||
</data>
|
||||
<data name="ThemeAppliedOnRestart" xml:space="preserve">
|
||||
<value>Your theme changes will apply when the app is restarted.</value>
|
||||
</data>
|
||||
<data name="Capitalize" xml:space="preserve">
|
||||
<value>Capitalize</value>
|
||||
<comment>ex. Uppercase the first character of a word.</comment>
|
||||
</data>
|
||||
<data name="IncludeNumber" xml:space="preserve">
|
||||
<value>Include Number</value>
|
||||
</data>
|
||||
<data name="Download" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
</data>
|
||||
<data name="Shared" xml:space="preserve">
|
||||
<value>Shared</value>
|
||||
</data>
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user