1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

..

57 Commits

Author SHA1 Message Date
André Bispo
bcc260183e [PS-416] Fixed a bug where the item details page was not updating after saving. 2022-07-26 21:28:32 +01:00
André Bispo
2f2b90acee [SSG-416] PR fixes 2022-07-18 22:13:44 +01:00
André Bispo
1b82249d44 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-18 14:21:15 +01:00
André Bispo
f432a58e9f [SSG-416] run dotnet tool run dotnet-format 2022-07-13 12:53:54 +01:00
André Bispo
7e6935f21c [SSG-416] Add to remove a11y text from totp toggle because on android it places an helper text next to the switch making it invisible. Also removed from the label because it already reads the text from the label 2022-07-13 11:13:50 +01:00
André Bispo
7cf34b845e [SSG-416] Mobile PR Fixes 2022-07-12 23:00:31 +01:00
André Bispo
8ed909eb91 [SG-416] Updated scanner mode toggle text color to be inline with figma designs 2022-07-07 17:58:39 +01:00
André Bispo
a57dc50a50 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-07 17:13:04 +01:00
André Bispo
9074f533f9 Merge branch 'master' into feature/totp-tab 2022-07-07 15:33:08 +01:00
André Bispo
9499fa0fb3 [SSG-416] Updated ViewPageViewModel and code refactoring. 2022-07-07 15:27:44 +01:00
André Bispo
69e47b651d [SSG-416] removed unnecessary using. 2022-07-06 19:50:09 +01:00
André Bispo
dbe72c68a8 [SSG-416] removed unnecessary changes to the TabsPage file 2022-07-06 19:36:12 +01:00
André Bispo
3822373b98 [SSG-416] Removed the dashes from free user and just left the Premium subscription required. 2022-07-06 18:00:05 +01:00
André Bispo
a51c2efdae PS-70 changed premium required label. fixed bug when to show premium required label. 2022-06-29 11:29:29 +01:00
André Bispo
0ce01ddd84 PS-70 PR fix for AppResource vs code behind generation. 2022-06-28 12:51:45 +01:00
André Bispo
6b33266721 PS-70 removed update to premium uri launch 2022-06-24 19:50:21 +01:00
André Bispo
6f61f1f4d8 PS-70 code format 2022-06-24 19:31:43 +01:00
André Bispo
d3d935fff6 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
#	src/App/Pages/Vault/ScanPage.xaml.cs
2022-06-24 17:39:24 +01:00
André Bispo
a97abc61d5 PS-70 removed shadow for newer versions of android 2022-06-24 16:17:25 +01:00
André Bispo
6e30ec2dc7 PS-70 fixed font clipping bug on iOS 2022-06-24 12:19:04 +01:00
André Bispo
472f251714 PS-70 Added frame styling for iOS, since frame view has different base configuration for android and iOS. 2022-06-24 11:57:57 +01:00
André Bispo
a6069ef37b PS-70 Fixed android button overlapping bug by adding button styling to a Frame view and placing a label inside. Fixed Color on scanner page. 2022-06-23 17:17:59 +01:00
André Bispo
6aeec4a89a PS-70 Added label on top of button to solve UI bug. 2022-06-23 11:31:50 +01:00
André Bispo
9ddbe400f8 PS-70 changed labels on manual scanner screen 2022-06-22 19:02:16 +01:00
André Bispo
9bc8e6912d PS-70 added copy button to totp edit cipher. Added row button when totp is null. 2022-06-22 19:01:23 +01:00
André Bispo
64beb23239 PS-70 fixed totp cell title label font 2022-06-22 16:12:22 +01:00
André Bispo
94f2c5a4f8 PS-70 added vibrate permission to manifest. refactored scanpage code. added manual authentication key feature in scanner. 2022-06-22 12:26:51 +01:00
André Bispo
3522953b8c PS-70 fixed scanner animation for android devices 2022-06-22 10:57:39 +01:00
André Bispo
3a91bc7eb1 PS-70 let zxing scanner camera feed on until screen is closed. 2022-06-22 10:13:58 +01:00
André Bispo
277dd5942f PS-70 Added scanner square corner overlay. Added scanning animation. Added scan success animation. 2022-06-21 22:28:13 +01:00
André Bispo
46c73dafbe PS-70 Splited totp code to adjust spacing. 2022-06-21 17:07:45 +01:00
André Bispo
c564a34317 PS-70 added new UI to enter code manually in the QR Code scanner screen. Changed existing labels on scanner screen. 2022-06-20 18:41:50 +01:00
André Bispo
e606ff88a4 PS-70 Renamed TOTP to Totp to have consistency in naming. Removed a11y text of switch because android was overlapping text. 2022-06-20 14:42:33 +01:00
André Bispo
67fbd55ae9 PS-70 added text labels to resource files 2022-06-20 12:39:53 +01:00
André Bispo
c67229d235 PS-70 show upgrade to premium text on details to free user. 2022-06-20 12:28:00 +01:00
André Bispo
e8d31b8a22 PS-70 add copy to clipboard. 2022-06-20 12:27:11 +01:00
André Bispo
3b763e454c PS-70 removed unnecessary code 2022-06-20 11:18:15 +01:00
André Bispo
c9c3e6d98d PS-70 show toggle only if it's premium 2022-06-20 11:17:20 +01:00
André Bispo
751fdf2db6 PS-70 Added new props to custom control. 2022-06-17 10:23:25 +01:00
André Bispo
6ae7d6dd8d PS-70 Fixed grid cell width. Added red progress at 20 percent. Refactored circular progress view. 2022-06-15 20:54:00 +01:00
André Bispo
a3218aed26 PS-70 Added circular progress to the OTP count down 2022-06-13 12:57:07 +01:00
André Bispo
77c8156515 fixed formatting 2022-06-07 19:06:26 +01:00
André Bispo
1c0375ef05 removed unnecessary code on vm 2022-06-07 18:59:17 +01:00
André Bispo
b3f2730c71 PS-70 removed old authentication tab 2022-06-07 18:44:07 +01:00
André Bispo
5e2142fba7 PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource 2022-06-07 18:38:48 +01:00
André Bispo
c91277bc43 Merge branch 'beeep-totp' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml.cs
#	src/App/Pages/Authenticator/AuthenticatorPageViewModel.cs
2022-06-06 14:16:40 +01:00
Jacob Fink
a484ca633c Merge branch 'beeep-totp' of https://github.com/bitwarden/mobile into beeep-totp 2022-06-06 08:58:52 -04:00
André Bispo
31d1a2e083 enable context loading and vm init 2022-06-04 20:19:03 +01:00
André Bispo
655b51b6a5 Merge branch 'master' into beeep-totp
# Conflicts:
#	src/App/App.csproj
2022-06-04 18:05:14 +01:00
Jacob Fink
0b626cedc7 got the countdown working 2022-03-04 16:44:22 -05:00
Jacob Fink
3ac2580742 add toolbar icons 2022-02-17 11:26:14 -05:00
Jacob Fink
008ed8eb56 add authentication view cell 2022-02-16 15:55:02 -05:00
Jacob Fink
26e0e43bb4 Merge branch 'master' into beeep-totp 2022-02-16 09:25:29 -05:00
Jacob Fink
0ad992faec add tab page 2022-02-16 09:25:19 -05:00
Jacob Fink
98dd8298ea clear extra code and fix build 2022-02-15 14:15:42 -05:00
Jacob Fink
bb37bac620 Revert config files from previous commit
This reverts commit b02c58e362.
2022-02-15 13:36:32 -05:00
Carlos J. Muentes
b02c58e362 Initial commit of new TOTP page 2022-02-11 15:22:51 -05:00
278 changed files with 7991 additions and 33734 deletions

View File

@@ -22,7 +22,7 @@
## Before you submit
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
- If this change requires a **documentation update** - notify the documentation team
- If this change has particular **deployment requirements** - notify the DevOps team
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -57,7 +57,7 @@ jobs:
android:
name: Android
runs-on: windows-2022
runs-on: windows-2019
needs: setup
steps:
- name: Setup NuGet
@@ -68,26 +68,6 @@ jobs:
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
$componentsToAdd = @(
"Component.Xamarin"
)
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -eq 0)
{
Write-Host "components have been successfully added"
}
else
{
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -232,7 +212,7 @@ jobs:
f-droid:
name: F-Droid Build
runs-on: windows-2022
runs-on: windows-2019
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
@@ -242,26 +222,6 @@ jobs:
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
$componentsToAdd = @(
"Component.Xamarin"
)
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -eq 0)
{
Write-Host "components have been successfully added"
}
else
{
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -441,17 +401,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
appcenter-ios-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
with:
keyvault: "bitwarden-prod-kv"
secrets: "appcenter-ios-token"
- name: Decrypt secrets
env:
@@ -642,17 +595,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Upload Sources
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
@@ -709,18 +655,11 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
if: failure()
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
devops-alerts-slack-webhook-url
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33

View File

@@ -24,20 +24,13 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Download translations
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -53,38 +53,18 @@ jobs:
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
- name: Create GitHub deployment
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
initial-status: 'in_progress'
environment: 'production'
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
task: release
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }}
- name: Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: master
- name: Prep Bitwarden iOS release asset
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: github.event.inputs.release_type != 'Dry Run'
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
with:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
@@ -98,22 +78,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
- name: Update deployment status to Success
if: ${{ success() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ failure() }}
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
f-droid:
name: F-Droid Release
@@ -125,7 +89,6 @@ jobs:
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
@@ -133,15 +96,6 @@ jobs:
branch: ${{ needs.release.outputs.branch-name }}
name: com.x8bit.bitwarden-fdroid.apk
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
with:
workflow: build.yml
workflow_conclusion: success
branch: master
name: com.x8bit.bitwarden-fdroid.apk
- name: Set up Node
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
with:
@@ -207,5 +161,5 @@ jobs:
cd $GITHUB_WORKSPACE
- name: Deploy to gh-pages
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: github.event.inputs.release_type != 'Dry Run'
run: npm run deploy

View File

@@ -1,67 +0,0 @@
---
name: Version Auto Bump
on:
release:
types: [published]
jobs:
setup:
name: "Setup"
runs-on: ubuntu-20.04
outputs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Get version to bump
id: version
env:
RELEASE_TAG: ${{ github.event.release.tag }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
echo $CURR_VER
((CURR_VER++))
NEW_VER=$CURR_MAJOR$CURR_VER
echo $NEW_VER
echo "::set-output name=new-version::$NEW_VER"
trigger_version_bump:
name: "Trigger version bump workflow"
runs-on: ubuntu-20.04
needs:
- setup
steps:
- name: Login to Azure
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
run: |
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$SECRET::$VALUE"
- name: Call GitHub API to trigger workflow bump
env:
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
VERSION: ${{ needs.setup.outputs.version_number}}
run: |
JSON_STRING=$(printf '{"ref":"master", "inputs": { "version_number":"%s"}}' "$VERSION")
curl \
-X POST \
-i -u bitwarden-devops-bot:$TOKEN \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/bitwarden/mobile/actions/workflows/version-bump.yml/dispatches \
-d $JSON_STRING

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"schedule:monthly",
":maintainLockFilesMonthly",
":preserveSemverRanges",
":rebaseStalePrs",
":disableDependencyDashboard"
],
"enabledManagers": [
"nuget"
],
"packageRules": [
{
"matchManagers": ["nuget"],
"groupName": "Nuget updates",
"groupSlug": "nuget",
"separateMajorMinor": false
}
]
}

View File

@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
namespace Bit.Droid.Accessibility
{
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden", Exported = true)]
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")]
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]

View File

@@ -15,7 +15,7 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v12.1</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
@@ -75,24 +75,24 @@
<Version>2.1.0.4</Version>
</PackageReference>
<PackageReference Include="Portable.BouncyCastle">
<Version>1.9.0</Version>
<Version>1.8.10</Version>
</PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.9" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.11" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.10" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.3</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>123.0.8</Version>
<Version>122.0.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.4.0.4" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>118.0.1.2</Version>
<Version>117.0.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -151,7 +151,6 @@
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Utilities\IntentExtensions.cs" />
<Compile Include="Renderers\CustomPageRenderer.cs" />
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -176,7 +175,6 @@
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
<AndroidResource Include="Resources\drawable\icon.xml" />
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" />
<AndroidResource Include="Resources\drawable\ic_launcher_monochrome.xml" />
<AndroidResource Include="Resources\drawable\ic_warning.xml" />
<AndroidResource Include="Resources\drawable\id.xml" />
<AndroidResource Include="Resources\drawable\info.xml" />
@@ -214,13 +212,6 @@
<AndroidResource Include="Resources\values\colors.xml" />
<AndroidResource Include="Resources\values\manifest.xml" />
<AndroidResource Include="Resources\values-v30\manifest.xml" />
<AndroidResource Include="Resources\drawable-v26\splash_screen_round.xml" />
<AndroidResource Include="Resources\drawable\logo_rounded.xml" />
<AndroidResource Include="Resources\drawable-night-v26\splash_screen_round.xml" />
<AndroidResource Include="Resources\drawable\ic_notification.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
@@ -288,8 +279,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\values-v30\" />
<Folder Include="Resources\drawable-v26\" />
<Folder Include="Resources\drawable-night-v26\" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View File

@@ -19,7 +19,6 @@ using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Bit.Core.Abstractions;
using SaveFlags = Android.Service.Autofill.SaveFlags;
using Bit.Droid.Utilities;
namespace Bit.Droid.Autofill
{
@@ -271,7 +270,8 @@ namespace Bit.Droid.Autofill
return null;
}
intent.PutExtra("autofillFrameworkUri", uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
PendingIntentFlags.CancelCurrent);
var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden,
@@ -324,7 +324,7 @@ namespace Bit.Droid.Autofill
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
// "my vault" presentation) so we're including an empty one here
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
}
var slice = CreateInlinePresentationSlice(
inlinePresentationSpec,

View File

@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
namespace Bit.Droid.Autofill
{
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden", Exported = true)]
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")]
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]

View File

@@ -1,24 +0,0 @@
using Android.Widget;
using Bit.Droid.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportEffect(typeof(NoEmojiKeyboardEffect), nameof(NoEmojiKeyboardEffect))]
namespace Bit.Droid.Effects
{
public class NoEmojiKeyboardEffect : PlatformEffect
{
protected override void OnAttached()
{
if (Control is EditText editText)
{
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
}
}
protected override void OnDetached()
{
}
}
}

View File

@@ -5,14 +5,12 @@ using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.Nfc;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.Core.Content;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
@@ -20,9 +18,7 @@ using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Xamarin.Essentials;
using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider;
namespace Bit.Droid
{
@@ -39,7 +35,6 @@ namespace Bit.Droid
private IStateService _stateService;
private IAppIdService _appIdService;
private IEventService _eventService;
private ILogger _logger;
private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions;
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
@@ -50,7 +45,7 @@ namespace Bit.Droid
{
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
PendingIntentFlags.UpdateCurrent);
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
StrictMode.SetThreadPolicy(policy);
@@ -61,7 +56,6 @@ namespace Bit.Droid
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
@@ -76,7 +70,7 @@ namespace Bit.Droid
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
});
_logger.InitAsync();
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
var toplayout = Window?.DecorView?.RootView;
if (toplayout != null)
@@ -87,9 +81,8 @@ namespace Bit.Droid
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState);
_appOptions = GetOptions();
CreateNotificationChannel();
LoadApplication(new App.App(_appOptions));
DisableAndroidFontScale();
_broadcasterService.Subscribe(_activityKey, (message) =>
{
@@ -280,7 +273,7 @@ namespace Bit.Droid
{
var intent = new Intent(this, Class);
intent.AddFlags(ActivityFlags.SingleTop);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(0, true));
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
// register for all NDEF tags starting with http och https
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
ndef.AddDataScheme("http");
@@ -408,38 +401,5 @@ namespace Bit.Droid
alarmManager.Cancel(_eventUploadPendingIntent);
await _eventService.UploadEventsAsync();
}
private void CreateNotificationChannel()
{
#if !FDROID
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
// Notification channels are new in API 26 (and not a part of the
// support library). There is no need to create a notification
// channel on older versions of Android.
return;
}
var channel = new NotificationChannel(Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
if(GetSystemService(NotificationService) is NotificationManager notificationManager)
{
notificationManager.CreateNotificationChannel(channel);
}
#endif
}
private void DisableAndroidFontScale()
{
try
{
//As we are using NamedSizes the xamarin will change the font size. So we are disabling the Android scaling.
Resources.Configuration.FontScale = 1f;
BaseContext.Resources.DisplayMetrics.ScaledDensity = Resources.Configuration.FontScale * (float)DeviceDisplay.MainDisplayInfo.Density;
}
catch (Exception e)
{
_logger.Exception(e);
}
}
}
}

View File

@@ -48,7 +48,6 @@ namespace Bit.Droid
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
Constants.AndroidAllClearCipherCacheKeys);
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
@@ -72,9 +71,8 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -195,12 +193,5 @@ namespace Bit.Droid
{
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
}
private void InitializeAppSetup()
{
var appSetup = new AppSetup();
appSetup.InitializeServicesLastChance();
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
}
}
}

View File

@@ -1,49 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.10.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
<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="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
</provider>
<meta-data android:name="android.max_aspect" android:value="2.1" />
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true" />
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true" />
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>
</application>
<!-- Package visibility (for Android 11+) -->
<queries>
<intent>
<action android:name="*" />
</intent>
</queries>
</manifest>
<?xml version='1.0' encoding='UTF-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.6.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
<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="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
</provider>
<meta-data android:name="android.max_aspect" android:value="2.1"/>
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions"/>
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true"/>
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true"/>
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true"/>
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/*"/>
<data android:mimeType="image/*"/>
<data android:mimeType="video/*"/>
<data android:mimeType="text/*"/>
</intent-filter>
</activity>
</application>
<!-- Package visibility (for Android 11+) -->
<queries>
<intent>
<action android:name="*"/>
</intent>
</queries>
</manifest>

View File

@@ -1,9 +1,7 @@
#if !FDROID
using System;
using Android.App;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Firebase.Messaging;
using Newtonsoft.Json;
@@ -18,41 +16,34 @@ namespace Bit.Droid.Push
{
public async override void OnNewToken(string token)
{
try {
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
}
catch (Exception ex)
{
Logger.Instance.Exception(ex);
}
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
}
public async override void OnMessageReceived(RemoteMessage message)
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
try
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
var obj = JObject.Parse(data);
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
"pushNotificationListenerService");
await listener.OnMessageAsync(obj, Device.Android);
}
catch (Exception ex)
catch (JsonReaderException ex)
{
Logger.Instance.Exception(ex);
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
}
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/darkgray"/>
<foreground android:drawable="@drawable/logo_rounded"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/logo_rounded"/>
</adaptive-icon>

View File

@@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.099"
android:scaleY="0.099"
android:translateX="24.3"
android:translateY="24.3">
<path
android:fillColor="#ffffff"
android:pathData="M481.4,102.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6L131.7,96.6c-5.1,0 -9.4,1.9 -13.1,5.6C114.9,105.9 113,110.2 113,115.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8L487.3,115.3C487,110.2 485.1,105.9 481.4,102.2zM438,341.8C438,423 300,493 300,493L300,144.6h138C438,144.6 438,260.6 438,341.8z" />
</group>
</vector>

View File

@@ -1,4 +0,0 @@
<vector android:height="24dp" android:viewportHeight="420"
android:viewportWidth="420" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M350.43,40.516C347.563,37.65 344.153,36.178 340.281,36.178L79.487,36.178C75.538,36.178 72.206,37.65 69.338,40.516C66.472,43.384 65,46.716 65,50.665L65,224.527C65,237.466 67.557,250.405 72.593,263.112C77.629,275.895 83.904,287.207 91.42,297.046C98.857,306.964 107.768,316.571 118.149,325.869C128.455,335.242 138.063,342.99 146.817,349.19C155.573,355.387 164.715,361.198 174.243,366.699C183.773,372.2 190.514,375.919 194.543,377.933C198.572,379.871 201.749,381.421 204.151,382.426C205.932,383.357 207.948,383.821 210.04,383.821C212.131,383.821 214.145,383.357 215.929,382.426C218.329,381.344 221.584,379.871 225.534,377.933C229.563,375.997 236.304,372.2 245.832,366.699C255.365,361.198 264.506,355.311 273.262,349.19C282.017,342.99 291.545,335.242 301.928,325.869C312.232,316.493 321.142,306.886 328.657,297.046C336.096,287.129 342.372,275.819 347.407,263.112C352.444,250.328 355,237.466 355,224.527L355,50.665C354.768,46.716 353.296,43.384 350.43,40.516ZM316.804,226.154C316.804,289.067 209.883,343.302 209.883,343.302L209.883,73.368L316.804,73.368C316.804,73.368 316.804,163.242 316.804,226.154Z"/>
</vector>

View File

@@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.11454546"
android:scaleY="0.11454546"
android:translateX="31.663637"
android:translateY="27.54">
<path
android:pathData="M376.4,12.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6H26.7c-5.1,0 -9.4,1.9 -13.1,5.6C9.9,15.9 8,20.2 8,25.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8V25.3C382,20.2 380.1,15.9 376.4,12.2zM333,251.8C333,333 195,403 195,403V54.6h138C333,54.6 333,170.6 333,251.8z"
android:fillColor="#FFFFFF"/>
</group>
</vector>

View File

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

View File

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

View File

@@ -4,7 +4,6 @@
<style name="LaunchTheme" parent="BaseTheme">
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
</style>
<style name="BaseTheme" parent="Theme.AppCompat">

View File

@@ -4,8 +4,6 @@
<style name="LaunchTheme" parent="BaseTheme">
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
</style>
<style name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">

View File

@@ -6,5 +6,4 @@
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:isAccessibilityTool="false"/>
android:canRetrieveWindowContent="true"/>

View File

@@ -1,14 +1,9 @@
#if !FDROID
using System;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using Bit.App.Abstractions;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Droid.Utilities;
using Xamarin.Forms;
namespace Bit.Droid.Services
@@ -28,11 +23,6 @@ namespace Bit.Droid.Services
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
public Task<bool> AreNotificationsSettingsEnabledAsync()
{
return Task.FromResult(IsRegisteredForPush);
}
public async Task<string> GetTokenAsync()
{
return await _stateService.GetPushCurrentTokenAsync();
@@ -57,39 +47,6 @@ namespace Bit.Droid.Services
// Do we ever need to unregister?
return Task.FromResult(0);
}
public void DismissLocalNotification(string notificationId)
{
if (int.TryParse(notificationId, out int intNotificationId))
{
var notificationManager = NotificationManagerCompat.From(Android.App.Application.Context);
notificationManager.Cancel(intNotificationId);
}
}
public void SendLocalNotification(string title, string message, string notificationId)
{
if (string.IsNullOrEmpty(notificationId))
{
throw new ArgumentNullException("notificationId cannot be null or empty.");
}
var context = Android.App.Application.Context;
var intent = new Intent(context, typeof(MainActivity));
var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
.SetContentIntent(pendingIntent)
.SetContentTitle(title)
.SetContentText(message)
.SetTimeoutAfter(Constants.PasswordlessNotificationTimeoutInMinutes * 60000)
.SetSmallIcon(Resource.Drawable.ic_notification)
.SetColor((int)Android.Graphics.Color.White)
.SetAutoCancel(true);
var notificationManager = NotificationManagerCompat.From(context);
notificationManager.Notify(int.Parse(notificationId), builder.Build());
}
}
}
#endif

View File

@@ -5,7 +5,6 @@ using Android.Content;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Xamarin.Essentials;
@@ -24,7 +23,7 @@ namespace Bit.Droid.Services
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
0,
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
PendingIntentFlags.UpdateCurrent));
}
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)

View File

@@ -964,14 +964,5 @@ namespace Bit.Droid.Services
}
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
}
public void OpenAppSettings()
{
var intent = new Intent(Android.Provider.Settings.ActionApplicationDetailsSettings);
intent.AddFlags(ActivityFlags.NewTask);
var uri = Android.Net.Uri.FromParts("package", Application.Context.PackageName, null);
intent.SetData(uri);
Application.Context.StartActivity(intent);
}
}
}

View File

@@ -12,7 +12,7 @@ using Java.Lang;
namespace Bit.Droid.Tile
{
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
Icon = "@drawable/shield", Exported = true)]
Icon = "@drawable/shield")]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.AutofillTileService")]
public class AutofillTileService : TileService

View File

@@ -14,7 +14,7 @@ using Java.Lang;
namespace Bit.Droid.Tile
{
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Exported = true, Label = "@string/PasswordGenerator",
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/PasswordGenerator",
Icon = "@drawable/generate")]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.GeneratorTileService")]

View File

@@ -15,8 +15,7 @@ using Java.Lang;
namespace Bit.Droid.Tile
{
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
Icon = "@drawable/shield",
Exported = true)]
Icon = "@drawable/shield")]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.MyVaultTileService")]
public class MyVaultTileService : TileService

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Bit.App.Utilities;
@@ -49,22 +47,5 @@ namespace Bit.Droid.Utilities
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
}
}
public static PendingIntentFlags AddPendingIntentMutabilityFlag(PendingIntentFlags pendingIntentFlags, bool isMutable)
{
//Mutable flag was added on API level 31
if (isMutable && Build.VERSION.SdkInt >= BuildVersionCodes.S)
{
return pendingIntentFlags | PendingIntentFlags.Mutable;
}
//Immutable flag was added on API level 23
if (!isMutable && Build.VERSION.SdkInt >= BuildVersionCodes.M)
{
return pendingIntentFlags | PendingIntentFlags.Immutable;
}
return pendingIntentFlags;
}
}
}

View File

@@ -7,8 +7,7 @@ namespace Bit.Droid
{
[Activity(
NoHistory = true,
LaunchMode = LaunchMode.SingleTop,
Exported = true)]
LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
DataScheme = "bitwarden")]

View File

@@ -49,6 +49,5 @@ namespace Bit.App.Abstractions
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
}
}

View File

@@ -1,16 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Bit.App.Abstractions
{
public interface IPushNotificationService
{
bool IsRegisteredForPush { get; }
Task<bool> AreNotificationsSettingsEnabledAsync();
Task<string> GetTokenAsync();
Task RegisterAsync();
Task UnregisterAsync();
void SendLocalNotification(string title, string message, string notificationId);
void DismissLocalNotification(string notificationId);
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Bit.App</RootNamespace>
<AssemblyName>BitwardenApp</AssemblyName>
<Configurations>Debug;Release;FDroid</Configurations>
@@ -14,14 +14,13 @@
<ItemGroup>
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
@@ -98,11 +97,11 @@
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
<DependentUpon>AddEditPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
<DependentUpon>ViewPage.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
<DependentUpon>SettingsPage.xaml</DependentUpon>
@@ -123,20 +122,11 @@
<SubType>Code</SubType>
</Compile>
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Behaviors\" />
<Folder Include="Lists\" />
<Folder Include="Lists\ItemLayouts\" />
<Folder Include="Lists\DataTemplateSelectors\" />
<Folder Include="Lists\ItemLayouts\CustomFields\" />
<Folder Include="Lists\ItemViewModels\" />
<Folder Include="Lists\ItemViewModels\CustomFields\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Controls\DateTime\" />
@@ -422,12 +412,6 @@
<ItemGroup>
<None Remove="Behaviors\" />
<None Remove="Xamarin.CommunityToolkit" />
<None Remove="Lists\" />
<None Remove="Lists\DataTemplates\" />
<None Remove="Lists\DataTemplateSelectors\" />
<None Remove="Lists\DataTemplates\CustomFields\" />
<None Remove="Lists\ItemViewModels\" />
<None Remove="Lists\ItemViewModels\CustomFields\" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Controls\DateTime\" />

View File

@@ -7,7 +7,6 @@ using Bit.App.Resources;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@@ -26,13 +25,13 @@ namespace Bit.App
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ISyncService _syncService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
private readonly IStorageService _secureStorageService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
private static bool _isResumed;
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
private static bool _pendingCheckPasswordlessLoginRequests;
public App(AppOptions appOptions)
{
@@ -48,9 +47,10 @@ namespace Bit.App
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_accountsManager.Init(() => Options, this);
@@ -140,10 +140,6 @@ namespace Bit.App
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
}
catch (Exception ex)
{
@@ -152,55 +148,11 @@ namespace Bit.App
});
}
private async Task CheckPasswordlessLoginRequestsAsync()
{
if (!_isResumed)
{
_pendingCheckPasswordlessLoginRequests = true;
return;
}
_pendingCheckPasswordlessLoginRequests = false;
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
if (notification == null)
{
return;
}
// Delay to wait for the vault page to appear
await Task.Delay(2000);
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
{
PubKey = loginRequestData.PublicKey,
Id = loginRequestData.Id,
IpAddress = loginRequestData.RequestIpAddress,
Email = await _stateService.GetEmailAsync(),
FingerprintPhrase = loginRequestData.RequestFingerprint,
RequestDate = loginRequestData.CreationDate,
DeviceType = loginRequestData.RequestDeviceType,
Origin = loginRequestData.Origin,
});
await _stateService.SetPasswordlessLoginNotificationAsync(null);
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
if (loginRequestData.CreationDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
{
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
}
}
public AppOptions Options { get; private set; }
protected async override void OnStart()
{
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
_isResumed = true;
await ClearCacheIfNeededAsync();
Prime();
if (string.IsNullOrWhiteSpace(Options.Uri))
@@ -212,10 +164,6 @@ namespace Bit.App
SyncIfNeeded();
}
}
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
@@ -248,10 +196,6 @@ namespace Bit.App
{
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
ResumedAsync().FireAndForget();
@@ -386,20 +330,20 @@ namespace Bit.App
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
if (topPage is NavigationPage navPage)
{
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
if (navPage.CurrentPage is ViewPage viewPage)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "view",
CipherId = cipherDetailsPage.ViewModel.CipherId
CipherId = viewPage.ViewModel.CipherId
};
}
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
{
lastPageBeforeLock = new PreviousPageInfo
{
Page = "edit",
CipherId = cipherAddEditPage.ViewModel.CipherId
CipherId = addEditPage.ViewModel.CipherId
};
}
}
@@ -434,7 +378,7 @@ namespace Bit.App
Current.MainPage = new TabsPage(Options);
break;
case NavigationTarget.AddEditCipher:
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
break;
case NavigationTarget.AutofillCiphers:
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));

View File

@@ -1,11 +1,9 @@
using System;
using System.Collections;
using System.Collections;
using System.Collections.Specialized;
using Xamarin.Forms;
namespace Bit.App.Controls
{
[Obsolete]
public class RepeaterView : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(

View File

@@ -1,12 +0,0 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Effects
{
public class NoEmojiKeyboardEffect : RoutingEffect
{
public NoEmojiKeyboardEffect()
: base("Bitwarden.NoEmojiKeyboardEffect")
{ }
}
}

View File

@@ -1,28 +0,0 @@
using Bit.App.Lists.ItemViewModels.CustomFields;
using Xamarin.Forms;
namespace Bit.App.Lists.DataTemplateSelectors
{
public class CustomFieldItemTemplateSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate BooleanTemplate { get; set; }
public DataTemplate LinkedTemplate { get; set; }
public DataTemplate HiddenTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
switch (item)
{
case BooleanCustomFieldItemViewModel _:
return BooleanTemplate;
case LinkedCustomFieldItemViewModel _:
return LinkedTemplate;
case HiddenCustomFieldItemViewModel _:
return HiddenTemplate;
default:
return TextTemplate;
}
}
}
}

View File

@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<StackLayout
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.BooleanCustomFieldItemLayout"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:BooleanCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsEditing}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<controls:IconLabel
Text="{Binding BooleanValue, Mode=OneWay, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Checkbox}}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
Margin="0, 5, 0, 0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Switch
IsToggled="{Binding BooleanValue}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>

View File

@@ -1,12 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class BooleanCustomFieldItemLayout : StackLayout
{
public BooleanCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.HiddenCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:HiddenCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<StackLayout
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Converter={StaticResource inverseBool}}">
<controls:MonoLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}"
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False">
<Entry.Keyboard>
<Keyboard x:FactoryMethod="Create">
<x:Arguments>
<KeyboardFlags>None</KeyboardFlags>
</x:Arguments>
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValue, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -1,12 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class HiddenCustomFieldItemLayout : StackLayout
{
public HiddenCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.LinkedCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:LinkedCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsEditing}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -1,12 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class LinkedCustomFieldItemLayout : StackLayout
{
public LinkedCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.TextCustomFieldItemLayout"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="cfvm:TextCustomFieldItemViewModel"
Spacing="0" Padding="0">
<StackLayout.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
</ResourceDictionary>
</StackLayout.Resources>
<Grid
StyleClass="box-row"
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsEditing}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyFieldCommand}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding FieldOptionsCommand}"
IsVisible="{Binding IsEditing}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
</StackLayout>

View File

@@ -1,12 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class TextCustomFieldItemLayout : StackLayout
{
public TextCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -1,49 +0,0 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel
{
protected FieldView _field;
protected bool _isEditing;
private string[] _additionalFieldProperties = new string[]
{
nameof(ValueText),
nameof(ShowCopyButton)
};
public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
{
_field = field;
_isEditing = isEditing;
FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this));
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ShowCopyButton),
});
}
public bool IsEditing => _isEditing;
public virtual bool ShowCopyButton => false;
public virtual string ValueText => _field.Value;
public ICommand FieldOptionsCommand { get; }
public void TriggerFieldChanged()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
}
}

View File

@@ -1,23 +0,0 @@
using System.Windows.Input;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
: base(field, isEditing, fieldOptionsCommand)
{
}
public bool BooleanValue
{
get => bool.TryParse(Field.Value, out var boolVal) && boolVal;
set
{
Field.Value = value.ToString().ToLower();
TriggerPropertyChanged(nameof(BooleanValue));
}
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemFactory
{
ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand);
}
public class CustomFieldItemFactory : ICustomFieldItemFactory
{
readonly II18nService _i18nService;
readonly IEventService _eventService;
public CustomFieldItemFactory(II18nService i18nService, IEventService eventService)
{
_i18nService = i18nService;
_eventService = eventService;
}
public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
bool isEditing,
CipherView cipher,
IPasswordPromptable passwordPromptable,
ICommand copyFieldCommand,
ICommand fieldOptionsCommand)
{
switch (field.Type)
{
case FieldType.Text:
return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand);
case FieldType.Boolean:
return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand);
case FieldType.Hidden:
return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand);
case FieldType.Linked:
return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService);
default:
throw new NotImplementedException("There is no custom field item for field type " + field.Type);
}
}
}
}

View File

@@ -1,70 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly IPasswordPromptable _passwordPromptable;
private readonly IEventService _eventService;
private bool _showHiddenValue;
public HiddenCustomFieldItemViewModel(FieldView field,
bool isEditing,
ICommand fieldOptionsCommand,
CipherView cipher,
IPasswordPromptable passwordPromptable,
IEventService eventService,
ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_passwordPromptable = passwordPromptable;
_eventService = eventService;
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, (Func<bool>)null, ex =>
{
#if !FDROID
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
#endif
});
}
public ICommand CopyFieldCommand { get; }
public ICommand ToggleHiddenValueCommand { get; set; }
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value);
}
public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField);
public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value);
public async Task ToggleHiddenValueAsync()
{
if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null))
{
await _eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
}
}

View File

@@ -1,13 +0,0 @@
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public interface ICustomFieldItemViewModel
{
FieldView Field { get; set; }
bool ShowCopyButton { get; }
void TriggerFieldChanged();
}
}

View File

@@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
private readonly CipherView _cipher;
private readonly II18nService _i18nService;
private int _linkedFieldOptionSelectedIndex;
public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService)
: base(field, isEditing, fieldOptionsCommand)
{
_cipher = cipher;
_i18nService = i18nService;
LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue
? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value)
: 0;
if (isEditing && Field.LinkedId is null)
{
field.LinkedId = LinkedFieldOptions[0].Value;
}
}
public override string ValueText
{
get
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}";
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@@ -1,19 +0,0 @@
using System.Windows.Input;
using Bit.Core.Models.View;
using Xamarin.Forms;
namespace Bit.App.Lists.ItemViewModels.CustomFields
{
public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel
{
public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand)
: base(field, isEditing, fieldOptionsCommand)
{
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
}
public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value);
public ICommand CopyFieldCommand { get; }
}
}

View File

@@ -70,7 +70,7 @@ namespace Bit.App.Pages
{
bool IsUrlValid(string url)
{
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
}
return IsUrlValid(BaseUrl)

View File

@@ -14,7 +14,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
</ContentPage.ToolbarItems>
<ScrollView>

View File

@@ -1,4 +1,5 @@
using Xamarin.Forms;
using System;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -23,6 +24,14 @@ namespace Bit.App.Pages
RequestFocus(_email);
}
private async void Submit_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())

View File

@@ -1,11 +1,10 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -14,26 +13,18 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
public HintPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.PasswordHint;
SubmitCommand = new AsyncCommand(SubmitAsync,
onException: ex =>
{
_logger.Exception(ex);
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
},
allowsMultipleExecutions: false);
SubmitCommand = new Command(async () => await SubmitAsync());
}
public ICommand SubmitCommand { get; }
public Command SubmitCommand { get; }
public string Email { get; set; }
public async Task SubmitAsync()
@@ -46,14 +37,14 @@ namespace Bit.App.Pages
}
if (string.IsNullOrWhiteSpace(Email))
{
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
AppResources.Ok);
return;
}
if (!Email.Contains("@"))
{
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
return;
}
@@ -63,7 +54,7 @@ namespace Bit.App.Pages
await _apiService.PostPasswordHintAsync(
new Core.Models.Request.PasswordHintRequest { Email = Email });
await _deviceActionService.HideLoadingAsync();
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
await Page.Navigation.PopModalAsync();
}
catch (ApiException e)

View File

@@ -55,7 +55,6 @@
<Entry
x:Name="_email"
Text="{Binding Email}"
IsEnabled="{Binding IsEmailEnabled}"
Keyboard="Email"
StyleClass="box-value">
<VisualStateManager.VisualStateGroups>

View File

@@ -1,8 +1,8 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -15,8 +15,6 @@ namespace Bit.App.Pages
private bool _inputFocused;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public LoginPage(string email = null, AppOptions appOptions = null)
{
_appOptions = appOptions;
@@ -32,10 +30,11 @@ namespace Bit.App.Pages
await _accountListOverlay.HideAsync();
await Navigation.PopModalAsync();
};
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
if (_vm.IsEmailEnabled)
if (!string.IsNullOrWhiteSpace(email))
{
_email.IsEnabled = false;
}
else
{
_vm.ShowCancelButton = true;
}
@@ -54,7 +53,7 @@ namespace Bit.App.Pages
ToolbarItems.Add(_getPasswordHint);
}
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
{
ToolbarItems.Add(_removeAccount);
}
@@ -111,7 +110,7 @@ namespace Bit.App.Pages
{
if (DoOnce())
{
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
await _vm.LogInAsync(true, _email.IsEnabled);
}
}
@@ -140,16 +139,26 @@ namespace Bit.App.Pages
}
}
private async void More_Clicked(object sender, EventArgs e)
private async void More_Clicked(object sender, System.EventArgs e)
{
try
await _accountListOverlay.HideAsync();
if (!DoOnce())
{
await _accountListOverlay.HideAsync();
_vm.MoreCommand.Execute(null);
return;
}
catch (Exception ex)
var buttons = _email.IsEnabled ? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await DisplayActionSheet(AppResources.Options,
AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
{
_logger.Value.Exception(ex);
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
}
else if (selection == AppResources.RemoveAccount)
{
await _vm.RemoveAccountAsync();
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
@@ -9,7 +8,6 @@ using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -30,7 +28,6 @@ namespace Bit.App.Pages
private bool _showCancelButton;
private string _email;
private string _masterPassword;
private bool _isEmailEnabled;
public LoginPageViewModel()
{
@@ -47,7 +44,6 @@ namespace Bit.App.Pages
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
LogInCommand = new Command(async () => await LogInAsync());
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -85,19 +81,10 @@ namespace Bit.App.Pages
set => SetProperty(ref _masterPassword, value);
}
public bool IsEmailEnabled
{
get => _isEmailEnabled;
set => SetProperty(ref _isEmailEnabled, value);
}
public bool IsIosExtension { get; set; }
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public ICommand MoreCommand { get; internal set; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public Action StartTwoFactorAction { get; set; }
@@ -214,28 +201,6 @@ namespace Bit.App.Pages
}
}
private async Task MoreAsync()
{
var buttons = IsEmailEnabled
? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
{
var hintNavigationPage = new NavigationPage(new HintPage());
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
}
else if (selection == AppResources.RemoveAccount)
{
await RemoveAccountAsync();
}
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;

View File

@@ -1,85 +0,0 @@
<?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.LoginPasswordlessPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPasswordlessViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:LoginPasswordlessViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout
Padding="7, 0, 7, 20">
<ScrollView
VerticalOptions="FillAndExpand">
<StackLayout>
<Label
Text="{u:I18n AreYouTryingToLogIn}"
FontSize="Title"
FontAttributes="Bold"
Margin="0,14,0,21"/>
<Label
Text="{Binding LogInAttemptByLabel}"
FontSize="Small"
Margin="0,0,0,24"/>
<Label
Text="{u:I18n FingerprintPhrase}"
FontSize="Small"
FontAttributes="Bold"/>
<controls:MonoLabel
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
FontSize="Medium"
TextColor="{DynamicResource FingerprintPhrase}"
Margin="0,0,0,27"/>
<Label
Text="{u:I18n DeviceType}"
FontSize="Small"
FontAttributes="Bold"/>
<Label
Text="{Binding LoginRequest.DeviceType}"
FontSize="Small"
Margin="0,0,0,21"/>
<Label
Text="{u:I18n IpAddress}"
IsVisible="{Binding ShowIpAddress}"
FontSize="Small"
FontAttributes="Bold"/>
<Label
Text="{Binding LoginRequest.IpAddress}"
IsVisible="{Binding ShowIpAddress}"
FontSize="Small"
Margin="0,0,0,21"/>
<Label
Text="{u:I18n Time}"
FontSize="Small"
FontAttributes="Bold"/>
<Label
Text="{Binding TimeOfRequestText}"
FontSize="Small"
Margin="0,0,0,57"/>
</StackLayout>
</ScrollView>
<Button
Text="{u:I18n ConfirmLogIn}"
Command="{Binding AcceptRequestCommand}"
Margin="0,0,0,17"
StyleClass="btn-primary"/>
<Button
Text="{u:I18n DenyLogIn}"
Command="{Binding RejectRequestCommand}"
StyleClass="btn-secundary"/>
</StackLayout>
</pages:BaseContentPage>

View File

@@ -1,43 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class LoginPasswordlessPage : BaseContentPage
{
private LoginPasswordlessViewModel _vm;
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
{
InitializeComponent();
_vm = BindingContext as LoginPasswordlessViewModel;
_vm.Page = this;
_vm.LoginRequest = loginPasswordlessDetails;
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem);
}
}
private async void Close_Clicked(object sender, System.EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.StartRequestTimeUpdater();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_vm.StopRequestTimeUpdater();
}
}
}

View File

@@ -1,175 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginPasswordlessViewModel : BaseViewModel
{
private IDeviceActionService _deviceActionService;
private IAuthService _authService;
private IPlatformUtilsService _platformUtilsService;
private ILogger _logger;
private LoginPasswordlessDetails _resquest;
private CancellationTokenSource _requestTimeCts;
private Task _requestTimeTask;
private const int REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES = 5;
public LoginPasswordlessViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
PageTitle = AppResources.LogInRequested;
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
}
public ICommand AcceptRequestCommand { get; }
public ICommand RejectRequestCommand { get; }
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
public LoginPasswordlessDetails LoginRequest
{
get => _resquest;
set
{
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
{
nameof(LogInAttemptByLabel),
nameof(TimeOfRequestText),
nameof(ShowIpAddress),
});
}
}
public void StopRequestTimeUpdater()
{
try
{
_requestTimeCts?.Cancel();
_requestTimeCts?.Dispose();
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
public void StartRequestTimeUpdater()
{
try
{
_requestTimeCts?.Cancel();
_requestTimeCts = new CancellationTokenSource();
_requestTimeTask = new TimerTask(_logger, UpdateRequestTime, _requestTimeCts).RunPeriodic(TimeSpan.FromMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES));
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
private async Task UpdateRequestTime()
{
TriggerPropertyChanged(nameof(TimeOfRequestText));
if (DateTime.UtcNow > LoginRequest?.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
{
StopRequestTimeUpdater();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
}
}
private async Task PasswordlessLoginAsync(bool approveRequest)
{
if (LoginRequest.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
{
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
await _deviceActionService.HideLoadingAsync();
await Page.Navigation.PopModalAsync();
_platformUtilsService.ShowToast("info", null, approveRequest ? AppResources.LogInAccepted : AppResources.LogInDenied);
StopRequestTimeUpdater();
}
private string CreateRequestDate(DateTime? requestDate)
{
if (!requestDate.HasValue)
{
return string.Empty;
}
if (DateTime.UtcNow < requestDate.Value.ToUniversalTime().AddMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES))
{
return AppResources.JustNow;
}
return string.Format(AppResources.XMinutesAgo, DateTime.UtcNow.Minute - requestDate.Value.ToUniversalTime().Minute);
}
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
}
public class LoginPasswordlessDetails
{
public string Id { get; set; }
public string Key { get; set; }
public string PubKey { get; set; }
public string Origin { get; set; }
public string Email { get; set; }
public string FingerprintPhrase { get; set; }
public DateTime RequestDate { get; set; }
public string DeviceType { get; set; }
public string IpAddress { get; set; }
}
}

View File

@@ -69,12 +69,12 @@ namespace Bit.App.Pages
}
}
private void LogIn_Clicked(object sender, EventArgs e)
private async void LogIn_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
CopyAppOptions();
_vm.LogInCommand.Execute(null);
await _vm.LogInAsync();
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
@@ -9,15 +8,13 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class LoginSsoPageViewModel : BaseViewModel
{
private const string REDIRECT_URI = "bitwarden://sso-callback";
private readonly IDeviceActionService _deviceActionService;
private readonly IAuthService _authService;
private readonly ISyncService _syncService;
@@ -26,7 +23,6 @@ namespace Bit.App.Pages
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService;
private readonly ILogger _logger;
private string _orgIdentifier;
@@ -41,11 +37,9 @@ namespace Bit.App.Pages
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
PageTitle = AppResources.Bitwarden;
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
LogInCommand = new Command(async () => await LogInAsync());
}
public string OrgIdentifier
@@ -54,7 +48,7 @@ namespace Bit.App.Pages
set => SetProperty(ref _orgIdentifier, value);
}
public ICommand LogInCommand { get; }
public Command LogInCommand { get; }
public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action SsoAuthSuccessAction { get; set; }
@@ -71,91 +65,81 @@ namespace Bit.App.Pages
public async Task LogInAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
string ssoToken;
try
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier))
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
AppResources.AnErrorHasOccurred,
AppResources.Ok);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
var response = await _apiService.PreValidateSso(OrgIdentifier);
if (string.IsNullOrWhiteSpace(response?.Token))
{
_logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace");
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError);
return;
}
var ssoToken = response.Token;
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
"redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" +
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
"ssoToken=" + Uri.EscapeDataString(ssoToken);
WebAuthenticatorResult authResult = null;
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(REDIRECT_URI));
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, OrgIdentifier);
}
else
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
ssoToken = response.Token;
}
catch (ApiException e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError,
await _platformUtilsService.ShowDialogAsync(
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
AppResources.AnErrorHasOccurred);
return;
}
var passwordOptions = new PasswordGenerationOptions(true);
passwordOptions.Length = 64;
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
var redirectUri = "bitwarden://sso-callback";
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
"client_id=" + _platformUtilsService.GetClientType().GetString() + "&" +
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
"response_type=code&scope=api%20offline_access&" +
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256&response_mode=query&" +
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" +
"ssoToken=" + Uri.EscapeDataString(ssoToken);
WebAuthenticatorResult authResult = null;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
new Uri(redirectUri));
}
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
catch (Exception ex)
var code = GetResultCode(authResult, state);
if (!string.IsNullOrEmpty(code))
{
await LogIn(code, codeVerifier, redirectUri, OrgIdentifier);
}
else
{
_logger.Exception(ex);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
AppResources.AnErrorHasOccurred);
}
}
@@ -174,11 +158,11 @@ namespace Bit.App.Pages
return code;
}
private async Task LogIn(string code, string codeVerifier, string orgId)
private async Task LogIn(string code, string codeVerifier, string redirectUri, string orgId)
{
try
{
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri, orgId);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
await _deviceActionService.HideLoadingAsync();

View File

@@ -132,7 +132,7 @@
IsToggled="{Binding AcceptPolicies}"
StyleClass="box-value"
HorizontalOptions="Start"
Margin="0, 0, 10, 0"/>
Margin="{Binding SwitchMargin}"/>
<Label StyleClass="box-footer-label"
HorizontalOptions="Fill">
<Label.FormattedText>

View File

@@ -61,6 +61,14 @@ namespace Bit.App.Pages
get => _acceptPolicies;
set => SetProperty(ref _acceptPolicies, value);
}
public Thickness SwitchMargin
{
get => Device.RuntimePlatform == Device.Android
? new Thickness(0, 0, 0, 0)
: new Thickness(0, 0, 10, 0);
}
public bool ShowTerms { get; set; }
public Command SubmitCommand { get; }
public Command TogglePasswordCommand { get; }

View File

@@ -17,19 +17,15 @@
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_cancelItem" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IsNullConverter x:Key="isNull" />
<ToolbarItem
x:Name="_moreItem"
x:Key="moreItem"
Icon="more_vert.png"
Order="Primary"
Command="{Binding MoreCommand}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
x:Name="_moreItem" x:Key="moreItem"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
Clicked="Methods_Clicked"
Order="Secondary"

View File

@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
@@ -136,6 +137,21 @@ namespace Bit.App.Pages
}
}
private async void More_Clicked(object sender, EventArgs e)
{
if (!DoOnce())
{
return;
}
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
if (selection == AppResources.UseAnotherTwoStepMethod)
{
await _vm.AnotherMethodAsync();
}
}
private async void ResendEmail_Clicked(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
@@ -13,7 +12,6 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Request;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -32,7 +30,6 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly II18nService _i18nService;
private readonly IAppIdService _appIdService;
private readonly ILogger _logger;
private TwoFactorProviderType? _selectedProviderType;
private string _totpInstruction;
@@ -54,11 +51,9 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_logger = ServiceContainer.Resolve<ILogger>();
PageTitle = AppResources.TwoStepLogin;
SubmitCommand = new Command(async () => await SubmitAsync());
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
}
public string TotpInstruction
@@ -116,7 +111,6 @@ namespace Bit.App.Pages
});
}
public Command SubmitCommand { get; }
public ICommand MoreCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; }
public Action StartSetPasswordAction { get; set; }
public Action CloseAction { get; set; }
@@ -343,15 +337,6 @@ namespace Bit.App.Pages
}
}
private async Task MoreAsync()
{
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
if (selection == AppResources.UseAnotherTwoStepMethod)
{
await AnotherMethodAsync();
}
}
public async Task AnotherMethodAsync()
{
var supportedProviders = _authService.GetSupportedTwoFactorProviders();

View File

@@ -1,15 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="Bit.App.Pages.GeneratorPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:enums="clr-namespace:Bit.Core.Enums;assembly=BitwardenCore"
x:DataType="pages:GeneratorPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
@@ -19,8 +15,6 @@
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Select}"
@@ -47,302 +41,60 @@
in ContentView.-->
<ContentView>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Spacing="0"
Padding="10,0">
<Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0"
Padding="10,0"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
</Grid>
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box">
<Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
</Grid>
<controls:MonoLabel
x:Name="lblPassword"
StyleClass="text-lg, text-html"
Text="{Binding ColoredPassword, Mode=OneWay}"
Margin="0, 20" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GeneratePassword}" />
</Grid>
<Grid IsVisible="{Binding IsUsername}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<controls:MonoLabel
x:Name="lblUsername"
StyleClass="text-lg, text-html"
Text="{Binding ColoredUsername, Mode=OneWay}"
Margin="0, 20"
HorizontalOptions="Start" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyUsername}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateUsernameCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
<BoxView StyleClass="box-row-separator"/>
<StackLayout StyleClass="box"
IsVisible="{Binding ShowTypePicker}"
Padding="0,10">
<Label
Text="{u:I18n WhatWouldYouLikeToGenerate}"
StyleClass="box-label" />
<Picker
x:Name="_typePicker"
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
SelectedItem="{Binding GeneratorTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
HorizontalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
LineBreakMode="CharacterWrap" />
<Button Text="{u:I18n RegeneratePassword}"
StyleClass="btn-primary"
HorizontalOptions="FillAndExpand"
Clicked="Regenerate_Clicked"></Button>
<Button Text="{u:I18n CopyPassword}"
HorizontalOptions="FillAndExpand"
Clicked="Copy_Clicked"></Button>
</StackLayout>
<Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform"
Margin="0,10,0,0"/>
<!--USERNAME OPTIONS-->
<StackLayout IsVisible="{Binding IsUsername}">
<StackLayout Orientation="Horizontal">
<Label
Text="{u:I18n UsernameType}"
StyleClass="box-label"
VerticalOptions="Center"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
Command="{Binding UsernameTypePromptHelpCommand}"
TextColor="{DynamicResource HyperlinkColor}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UsernamePromptHelpLink}"
VerticalOptions="Center"/>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<Picker
x:Name="_usernameTypePicker"
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
SelectedItem="{Binding UsernameTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label
StyleClass="box-footer-label"
Text="{Binding UsernameTypeDescriptionLabel}" />
<!--PLUS ADDRESSED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
<Label Text="{u:I18n EmailRequiredParenthesis}"
StyleClass="box-label" />
<Entry x:Name="_plusAddressedEmailEntry"
Text="{Binding PlusAddressedEmail}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_plusAddressedEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value" />
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0" />
</StackLayout>
<!--CATCH-ALL EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
<Label
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label" />
<Entry
x:Name="_catchAllEmailDomainNameEntry"
Text="{Binding CatchAllEmailDomain}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_catchallEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding CatchAllEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value"/>
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0"/>
</StackLayout>
<!--FORWARDED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
<Label
Text="{u:I18n Service}"
StyleClass="box-label" />
<Picker
x:Name="_serviceTypePicker"
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
SelectedItem="{Binding ForwardedEmailServiceSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<!--ANONADDY OPTIONS-->
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Margin="0,10,0,0"
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_anonAddyApiAccessTokenEntry"
Text="{Binding AnonAddyApiAccessToken}"
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
x:Name="_anonAddyDomainNameEntry"
Text="{Binding AnonAddyDomainName}"
StyleClass="box-value"/>
<!--FIREFOX RELAY OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_firefoxRelayApiAccessTokenEntry"
Text="{Binding FirefoxRelayApiAccessToken}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--SIMPLELOGIN OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_simpleLoginApiKeyEntry"
Text="{Binding SimpleLoginApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
</StackLayout>
<!--RANDOM WORD OPTIONS-->
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n Capitalize}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding CapitalizeRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n IncludeNumber}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding IncludeNumberRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
</StackLayout>
<!--PASSWORD OPTIONS-->
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n PasswordType}"
Text="{u:I18n Type}"
StyleClass="box-label" />
<Picker
x:Name="_passwordTypePicker"
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
x:Name="_typePicker"
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
SelectedIndex="{Binding TypeSelectedIndex}"
StyleClass="box-value" />
</StackLayout>
<StackLayout Spacing="0"
@@ -376,11 +128,7 @@
Text="{Binding WordSeparator}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
StyleClass="box-value">
<Entry.Effects>
<effects:NoEmojiKeyboardEffect />
</Entry.Effects>
</Entry>
StyleClass="box-value" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label

View File

@@ -18,7 +18,7 @@ namespace Bit.App.Pages
private readonly Action<string> _selectAction;
private readonly TabsPage _tabsPage;
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null)
{
_tabsPage = tabsPage;
InitializeComponent();
@@ -27,10 +27,6 @@ namespace Bit.App.Pages
_vm.Page = this;
_fromTabPage = fromTabPage;
_selectAction = selectAction;
_vm.ShowTypePicker = fromTabPage;
_vm.IsUsername = isUsernameGenerator;
_vm.EmailWebsite = emailWebsite;
_vm.EditMode = editMode;
var isIos = Device.RuntimePlatform == Device.iOS;
if (selectAction != null)
{
@@ -51,12 +47,10 @@ namespace Bit.App.Pages
ToolbarItems.Add(_historyItem);
}
}
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_passwordTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_usernameTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_serviceTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_plusAddressedEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_catchallEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
if (isIos)
{
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
}
}
public async Task InitAsync()
@@ -103,6 +97,16 @@ namespace Bit.App.Pages
return base.OnBackButtonPressed();
}
private async void Regenerate_Clicked(object sender, EventArgs e)
{
await _vm.RegenerateAsync();
}
private async void Copy_Clicked(object sender, EventArgs e)
{
await _vm.CopyAsync();
}
private async void More_Clicked(object sender, EventArgs e)
{
if (!DoOnce())
@@ -120,7 +124,7 @@ namespace Bit.App.Pages
private void Select_Clicked(object sender, EventArgs e)
{
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
_selectAction?.Invoke(_vm.Password);
}
private async void History_Clicked(object sender, EventArgs e)
@@ -146,20 +150,7 @@ namespace Bit.App.Pages
{
await base.UpdateOnThemeChanged();
await Device.InvokeOnMainThreadAsync(() =>
{
if (_vm != null)
{
if (_vm.IsUsername)
{
_vm.RedrawUsername();
}
else
{
_vm.RedrawPassword();
}
}
});
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
}
}
}

View File

@@ -1,16 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -19,16 +13,11 @@ namespace Bit.App.Pages
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions;
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
private string _password;
private bool _isPassword;
private bool _isUsername;
private bool _uppercase;
private bool _lowercase;
private bool _number;
@@ -41,69 +30,21 @@ namespace Bit.App.Pages
private string _wordSeparator;
private bool _capitalize;
private bool _includeNumber;
private string _username;
private GeneratorType _generatorTypeSelected;
private int _passwordTypeSelectedIndex;
private int _typeSelectedIndex;
private bool _doneIniting;
private bool _showTypePicker;
private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _editMode;
public GeneratorPageViewModel()
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
"passwordGenerationService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
GeneratorType.Password,
GeneratorType.Username
};
PasswordTypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
UsernameTypeOptions = new List<UsernameType> {
UsernameType.PlusAddressedEmail,
UsernameType.CatchAllEmail,
UsernameType.ForwardedEmailAlias,
UsernameType.RandomWord
};
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
UsernameEmailTypeOptions = new List<UsernameEmailType>
{
UsernameEmailType.Random,
UsernameEmailType.Website
};
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
PageTitle = AppResources.PasswordGenerator;
TypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
}
public List<GeneratorType> GeneratorTypeOptions { get; set; }
public List<string> PasswordTypeOptions { get; set; }
public List<UsernameType> UsernameTypeOptions { get; set; }
public List<ForwardedEmailServiceType> ForwardedEmailServiceTypeOptions { get; set; }
public List<UsernameEmailType> UsernameEmailTypeOptions { get; set; }
public Command UsernameTypePromptHelpCommand { get; set; }
public ICommand RegenerateCommand { get; set; }
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public List<string> TypeOptions { get; set; }
public string Password
{
@@ -115,18 +56,7 @@ namespace Bit.App.Pages
});
}
public string Username
{
get => _username;
set => SetProperty(ref _username, value,
additionalPropertyNames: new string[]
{
nameof(ColoredUsername)
});
}
public string ColoredPassword => GeneratedValueFormatter.Format(Password);
public string ColoredUsername => GeneratedValueFormatter.Format(Username);
public string ColoredPassword => PasswordFormatter.FormatPassword(Password);
public bool IsPassword
{
@@ -134,32 +64,6 @@ namespace Bit.App.Pages
set => SetProperty(ref _isPassword, value);
}
public bool IsUsername
{
get => _isUsername;
set => SetProperty(ref _isUsername, value);
}
public bool ShowTypePicker
{
get => _showTypePicker;
set => SetProperty(ref _showTypePicker, value);
}
public bool EditMode
{
get => _editMode;
set => SetProperty(ref _editMode, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public bool ShowUsernameEmailType
{
get => !string.IsNullOrWhiteSpace(EmailWebsite);
}
public int Length
{
get => _length;
@@ -331,20 +235,6 @@ namespace Bit.App.Pages
}
}
public string PlusAddressedEmail
{
get => _usernameOptions.PlusAddressedEmail;
set
{
if (_usernameOptions.PlusAddressedEmail != value)
{
_usernameOptions.PlusAddressedEmail = value;
TriggerPropertyChanged(nameof(PlusAddressedEmail));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
{
get => _enforcedPolicyOptions;
@@ -357,276 +247,24 @@ namespace Bit.App.Pages
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
public GeneratorType GeneratorTypeSelected
public int TypeSelectedIndex
{
get => _generatorTypeSelected;
get => _typeSelectedIndex;
set
{
if (SetProperty(ref _generatorTypeSelected, value))
{
IsUsername = value == GeneratorType.Username;
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public int PasswordTypeSelectedIndex
{
get => _passwordTypeSelectedIndex;
set
{
if (SetProperty(ref _passwordTypeSelectedIndex, value))
if (SetProperty(ref _typeSelectedIndex, value))
{
IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
var task = SaveOptionsAsync();
}
}
}
public UsernameType UsernameTypeSelected
{
get => _usernameOptions.Type;
set
{
if (_usernameOptions.Type != value)
{
_usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected
{
get => _usernameOptions.ServiceType;
set
{
if (_usernameOptions.ServiceType != value)
{
_usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string CatchAllEmailDomain
{
get => _usernameOptions.CatchAllEmailDomain;
set
{
if (_usernameOptions.CatchAllEmailDomain != value)
{
_usernameOptions.CatchAllEmailDomain = value;
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string AnonAddyApiAccessToken
{
get => _usernameOptions.AnonAddyApiAccessToken;
set
{
if (_usernameOptions.AnonAddyApiAccessToken != value)
{
_usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowAnonAddyApiAccessToken
{
get
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
set
{
if (_usernameOptions.AnonAddyDomainName != value)
{
_usernameOptions.AnonAddyDomainName = value;
TriggerPropertyChanged(nameof(AnonAddyDomainName));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string FirefoxRelayApiAccessToken
{
get => _usernameOptions.FirefoxRelayApiAccessToken;
set
{
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFirefoxRelayApiAccessToken
{
get
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
set
{
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowSimpleLoginApiKey
{
get
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool CapitalizeRandomWordUsername
{
get => _usernameOptions.CapitalizeRandomWordUsername;
set
{
if (_usernameOptions.CapitalizeRandomWordUsername != value)
{
_usernameOptions.CapitalizeRandomWordUsername = value;
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public bool IncludeNumberRandomWordUsername
{
get => _usernameOptions.IncludeNumberRandomWordUsername;
set
{
if (_usernameOptions.IncludeNumberRandomWordUsername != value)
{
_usernameOptions.IncludeNumberRandomWordUsername = value;
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType PlusAddressedEmailTypeSelected
{
get => _usernameOptions.PlusAddressedEmailType;
set
{
if (_usernameOptions.PlusAddressedEmailType != value)
{
_usernameOptions.PlusAddressedEmailType = value;
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType CatchAllEmailTypeSelected
{
get => _usernameOptions.CatchAllEmailType;
set
{
if (_usernameOptions.CatchAllEmailType != value)
{
_usernameOptions.CatchAllEmailType = value;
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public string EmailWebsite
{
get => _emailWebsite;
set => SetProperty(ref _emailWebsite, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public async Task InitAsync()
{
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
_usernameOptions.EmailWebsite = EmailWebsite;
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
if (!IsUsername)
{
await RegenerateAsync();
}
else
{
if (UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
}
TriggerUsernamePropertiesChanged();
await RegenerateAsync();
_doneIniting = true;
}
@@ -636,11 +274,6 @@ namespace Bit.App.Pages
await _passwordGenerationService.AddHistoryAsync(Password);
}
public async Task RegenerateUsernameAsync()
{
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
}
public void RedrawPassword()
{
if (!string.IsNullOrEmpty(_password))
@@ -649,14 +282,6 @@ namespace Bit.App.Pages
}
}
public void RedrawUsername()
{
if (!string.IsNullOrEmpty(_username))
{
TriggerPropertyChanged(nameof(ColoredUsername));
}
}
public async Task SaveOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
@@ -666,7 +291,6 @@ namespace Bit.App.Pages
SetOptions();
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
await _passwordGenerationService.SaveOptionsAsync(_options);
LoadFromOptions();
if (regenerate)
{
@@ -674,25 +298,6 @@ namespace Bit.App.Pages
}
}
public async Task SaveUsernameOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
{
return;
}
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
}
public async Task SliderChangedAsync()
{
await SaveOptionsAsync(false);
@@ -712,28 +317,15 @@ namespace Bit.App.Pages
public async Task CopyAsync()
{
await _clipboardService.CopyTextAsync(IsUsername ? Username : Password);
_platformUtilsService.ShowToastForCopiedValue(IsUsername ? AppResources.Username : AppResources.Password);
}
public void UsernameTypePromptHelp()
{
try
{
_platformUtilsService.LaunchUri("https://bitwarden.com/help/generator/#username-types");
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
await _clipboardService.CopyTextAsync(Password);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}
private void LoadFromOptions()
{
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = PasswordTypeSelectedIndex == 0;
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = TypeSelectedIndex == 0;
MinNumber = _options.MinNumber.GetValueOrDefault();
MinSpecial = _options.MinSpecial.GetValueOrDefault();
Special = _options.Special.GetValueOrDefault();
@@ -747,30 +339,10 @@ namespace Bit.App.Pages
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
}
private void TriggerUsernamePropertiesChanged()
{
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
}
private void SetOptions()
{
_options.AllowAmbiguousChar = AllowAmbiguousChars;
_options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password";
_options.MinNumber = MinNumber;
_options.MinSpecial = MinSpecial;
_options.Special = Special;
@@ -783,51 +355,5 @@ namespace Bit.App.Pages
_options.Capitalize = Capitalize;
_options.IncludeNumber = IncludeNumber;
}
private async void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok));
}
else
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok));
}
}
private string GetUsernameTypeLabelDescription(UsernameType value)
{
switch (value)
{
case UsernameType.PlusAddressedEmail:
return AppResources.PlusAddressedEmailDescription;
case UsernameType.CatchAllEmail:
return AppResources.CatchAllEmailDescription;
case UsernameType.ForwardedEmailAlias:
return AppResources.ForwardedEmailDescription;
default:
return string.Empty;
}
}
private async Task ToggleForwardedEmailHiddenValueAsync()
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
break;
case ForwardedEmailServiceType.FirefoxRelay:
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
break;
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
}
}
}
}

View File

@@ -83,7 +83,7 @@
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Command="{Binding ToggleAccessibilityCommand}"
Clicked="ToggleAccessibility"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"

View File

@@ -55,6 +55,14 @@ namespace Bit.App.Pages
_vm.ToggleInlineAutofill();
}
private void ToggleAccessibility(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ToggleAccessibility();
}
}
private void ToggleDrawOver(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -1,11 +1,9 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
@@ -14,8 +12,6 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly IStateService _stateService;
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private bool _autofillServiceToggled;
private bool _inlineAutofillToggled;
@@ -28,11 +24,7 @@ namespace Bit.App.Pages
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
PageTitle = AppResources.AutofillServices;
ToggleAccessibilityCommand = new AsyncCommand(ToggleAccessibilityAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
}
#region Autofill Service
@@ -82,8 +74,6 @@ namespace Bit.App.Pages
#region Accessibility
public ICommand ToggleAccessibilityCommand { get; }
public string AccessibilityDescriptionLabel
{
get
@@ -186,18 +176,8 @@ namespace Bit.App.Pages
InlineAutofillToggled = !InlineAutofillToggled;
}
public async Task ToggleAccessibilityAsync()
public void ToggleAccessibility()
{
if (!_deviceActionService.AutofillAccessibilityServiceRunning())
{
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
AppResources.Decline);
if (!accept)
{
return;
}
}
_deviceActionService.OpenAccessibilitySettings();
}

View File

@@ -132,20 +132,20 @@
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n AutofillBlockedUris}"
Text="{u:I18n BlacklistedUris}"
StyleClass="box-label" />
<Editor
x:Name="_autofillBlockedUrisEditor"
Text="{Binding AutofillBlockedUris}"
x:Name="_blacklistedUrisEditor"
Text="{Binding AutofillBlacklistedUris}"
StyleClass="box-value"
AutoSize="TextChanges"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
Keyboard="Url"
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
Unfocused="BlacklistedUrisEditor_Unfocused" />
</StackLayout>
<Label
Text="{u:I18n AutofillBlockedUrisDescription}"
Text="{u:I18n BlacklistedUrisDescription}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>

View File

@@ -45,12 +45,12 @@ namespace Bit.App.Pages
protected async override void OnDisappearing()
{
base.OnDisappearing();
await _vm.UpdateAutofillBlockedUris();
await _vm.UpdateAutofillBlacklistedUris();
}
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
private async void BlacklistedUrisEditor_Unfocused(object sender, FocusEventArgs e)
{
await _vm.UpdateAutofillBlockedUris();
await _vm.UpdateAutofillBlacklistedUris();
}
private async void Close_Clicked(object sender, System.EventArgs e)

View File

@@ -17,7 +17,7 @@ namespace Bit.App.Pages
private bool _autofillSavePrompt;
private string _autofillBlockedUris;
private string _autofillBlacklistedUris;
private bool _favicon;
private bool _autoTotpCopy;
private int _clearClipboardSelectedIndex;
@@ -167,10 +167,10 @@ namespace Bit.App.Pages
}
}
public string AutofillBlockedUris
public string AutofillBlacklistedUris
{
get => _autofillBlockedUris;
set => SetProperty(ref _autofillBlockedUris, value);
get => _autofillBlacklistedUris;
set => SetProperty(ref _autofillBlacklistedUris, value);
}
public bool ShowAndroidAutofillSettings
@@ -182,8 +182,8 @@ namespace Bit.App.Pages
public async Task InitAsync()
{
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : null;
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
AutoTotpCopy = !(await _stateService.GetDisableAutoTotpCopyAsync() ?? false);
Favicon = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
var theme = await _stateService.GetThemeAsync();
@@ -252,19 +252,19 @@ namespace Bit.App.Pages
}
}
public async Task UpdateAutofillBlockedUris()
public async Task UpdateAutofillBlacklistedUris()
{
if (_inited)
{
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
if (string.IsNullOrWhiteSpace(AutofillBlacklistedUris))
{
await _stateService.SetAutofillBlacklistedUrisAsync(null);
AutofillBlockedUris = null;
AutofillBlacklistedUris = null;
return;
}
try
{
var csv = AutofillBlockedUris;
var csv = AutofillBlacklistedUris;
var urisList = new List<string>();
foreach (var uri in csv.Split(','))
{
@@ -281,7 +281,7 @@ namespace Bit.App.Pages
urisList.Add(cleanedUri);
}
await _stateService.SetAutofillBlacklistedUrisAsync(urisList);
AutofillBlockedUris = string.Join(", ", urisList);
AutofillBlacklistedUris = string.Join(", ", urisList);
}
catch { }
}

View File

@@ -15,7 +15,7 @@ namespace Bit.App.Pages
public bool UseFrame { get; set; }
public Func<Task> ExecuteAsync { get; set; }
public bool SubLabelTextEnabled => SubLabel == AppResources.On;
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
public bool ShowSubLabel => SubLabel.Length != 0;
public bool ShowTimeInput => Time != null;

View File

@@ -30,7 +30,7 @@ namespace Bit.App.Pages
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private readonly IPushNotificationService _pushNotificationService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
@@ -42,7 +42,6 @@ namespace Bit.App.Pages
private string _vaultTimeoutActionDisplayValue;
private bool _showChangeMasterPassword;
private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests;
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
@@ -84,7 +83,6 @@ namespace Bit.App.Pages
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
PageTitle = AppResources.Settings;
@@ -135,7 +133,6 @@ namespace Bit.App.Pages
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
BuildList();
}
@@ -329,38 +326,6 @@ namespace Bit.App.Pages
BuildList();
}
public async Task ApproveLoginRequestsAsync()
{
var options = new[]
{
CreateSelectableOption(AppResources.Yes, _approvePasswordlessLoginRequests),
CreateSelectableOption(AppResources.No, !_approvePasswordlessLoginRequests),
};
var selection = await Page.DisplayActionSheet(AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Cancel, null, options);
if (selection == null || selection == AppResources.Cancel)
{
return;
}
_approvePasswordlessLoginRequests = CompareSelection(selection, AppResources.Yes);
await _stateService.SetApprovePasswordlessLoginsAsync(_approvePasswordlessLoginRequests);
BuildList();
if (!_approvePasswordlessLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
{
return;
}
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.ReceivePushNotificationsForNewLoginRequests, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks);
if (openAppSettingsResult)
{
_deviceActionService.OpenAppSettings();
}
}
public async Task VaultTimeoutActionAsync()
{
var options = _vaultTimeoutActions.Select(o =>
@@ -485,7 +450,7 @@ namespace Bit.App.Pages
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AutofillServices,
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
});
}
@@ -535,16 +500,10 @@ namespace Bit.App.Pages
new SettingsPageListItem
{
Name = AppResources.UnlockWithPIN,
SubLabel = _pin ? AppResources.On : AppResources.Off,
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => UpdatePinAsync()
},
new SettingsPageListItem
{
Name = AppResources.ApproveLoginRequests,
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
ExecuteAsync = () => ApproveLoginRequestsAsync()
},
new SettingsPageListItem
{
Name = AppResources.LockNow,
ExecuteAsync = () => LockAsync()
@@ -566,7 +525,7 @@ namespace Bit.App.Pages
var item = new SettingsPageListItem
{
Name = string.Format(AppResources.UnlockWith, biometricName),
SubLabel = _biometric ? AppResources.On : AppResources.Off,
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => UpdateBiometricAsync()
};
securityItems.Insert(2, item);
@@ -595,7 +554,7 @@ namespace Bit.App.Pages
securityItems.Add(new SettingsPageListItem
{
Name = AppResources.AllowScreenCapture,
SubLabel = _screenCaptureAllowed ? AppResources.On : AppResources.Off,
SubLabel = _screenCaptureAllowed ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
});
}
@@ -668,7 +627,7 @@ namespace Bit.App.Pages
new SettingsPageListItem
{
Name = AppResources.SubmitCrashLogs,
SubLabel = _reportLoggingEnabled ? AppResources.On : AppResources.Off,
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => LoggerReportingAsync()
},
#endif

View File

@@ -2,21 +2,19 @@
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.CipherAddEditPage"
x:Class="Bit.App.Pages.AddEditPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherAddEditPageViewModel"
x:DataType="pages:AddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:CipherAddEditPageViewModel />
<pages:AddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
@@ -55,25 +53,6 @@
IsDestructive="True"
x:Name="_deleteItem"
x:Key="deleteItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
</ResourceDictionary>
</ContentPage.Resources>
@@ -128,26 +107,15 @@
StyleClass="box-value" />
</StackLayout>
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input"
RowDefinitions="Auto,*"
ColumnDefinitions="*,Auto">
<Label
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n Username}"
StyleClass="box-label"/>
StyleClass="box-label" />
<Entry
x:Name="_loginUsernameEntry"
Text="{Binding Cipher.Login.Username}"
StyleClass="box-value"
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding GenerateUsernameCommand}"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
StyleClass="box-value" />
</StackLayout>
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -668,10 +636,101 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:AddEditPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsBooleanType, Mode=OneWay, Converter={StaticResource inverseBool}}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsBooleanType, Mode=OneWay}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsHiddenType}"
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding ShowViewHidden}"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False">
<Entry.Keyboard>
<Keyboard x:FactoryMethod="Create">
<x:Arguments>
<KeyboardFlags>None</KeyboardFlags>
</x:Arguments>
</Keyboard>
</Entry.Keyboard>
</controls:MonoEntry>
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsLinkedType}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<Switch
IsToggled="{Binding BooleanValue}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
IsVisible="{Binding IsBooleanType}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding BindingContext.FieldOptionsCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsBooleanType}" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
Clicked="NewField_Clicked"></Button>
</StackLayout>

View File

@@ -14,7 +14,7 @@ using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
namespace Bit.App.Pages
{
public partial class CipherAddEditPage : BaseContentPage
public partial class AddEditPage : BaseContentPage
{
private readonly AppOptions _appOptions;
private readonly IStateService _stateService;
@@ -22,10 +22,10 @@ namespace Bit.App.Pages
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
private CipherAddEditPageViewModel _vm;
private AddEditPageViewModel _vm;
private bool _fromAutofill;
public CipherAddEditPage(
public AddEditPage(
string cipherId = null,
CipherType? type = null,
string folderId = null,
@@ -36,7 +36,7 @@ namespace Bit.App.Pages
bool fromAutofill = false,
AppOptions appOptions = null,
bool cloneMode = false,
CipherDetailsPage cipherDetailsPage = null)
ViewPage viewPage = null)
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
@@ -47,7 +47,7 @@ namespace Bit.App.Pages
_fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
InitializeComponent();
_vm = BindingContext as CipherAddEditPageViewModel;
_vm = BindingContext as AddEditPageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
_vm.FolderId = folderId == "none" ? null : folderId;
@@ -57,7 +57,7 @@ namespace Bit.App.Pages
_vm.DefaultName = name ?? appOptions?.SaveName;
_vm.DefaultUri = uri ?? appOptions?.Uri;
_vm.CloneMode = cloneMode;
_vm.CipherDetailsPage = cipherDetailsPage;
_vm.ViewPage = viewPage;
_vm.Init();
SetActivityIndicator();
if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android)
@@ -145,7 +145,7 @@ namespace Bit.App.Pages
}
public bool FromAutofillFramework { get; set; }
public CipherAddEditPageViewModel ViewModel => _vm;
public AddEditPageViewModel ViewModel => _vm;
protected override async void OnAppearing()
{

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core;
@@ -16,19 +16,23 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public class CipherAddEditPageViewModel : BaseCipherViewModel
public class AddEditPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
private readonly IStateService _stateService;
private readonly IOrganizationService _organizationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuditService _auditService;
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly ILogger _logger;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _showNotesSeparator;
private bool _showPassword;
private bool _showCardNumber;
@@ -42,7 +46,7 @@ namespace Bit.App.Pages
private bool _hasCollections;
private string _previousCipherId;
private List<Core.Models.View.CollectionView> _writeableCollections;
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
private string[] _additionalCipherProperties = new string[]
{
nameof(IsLogin),
nameof(IsIdentity),
@@ -53,7 +57,6 @@ namespace Bit.App.Pages
nameof(ShowCollections),
nameof(HasTotpValue)
};
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
new List<KeyValuePair<UriMatchType?, string>>
{
@@ -66,30 +69,33 @@ namespace Bit.App.Pages
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
};
public CipherAddEditPageViewModel()
public AddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
CheckPasswordCommand = new Command(CheckPasswordAsync);
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<ICustomFieldItemViewModel>(FieldOptions);
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
GenerateUsernameCommand = new AsyncCommand(GenerateUsernameAsync, onException: ex => OnGenerateUsernameException(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
AllowPersonal = true;
@@ -145,11 +151,11 @@ namespace Bit.App.Pages
public Command TogglePasswordCommand { get; set; }
public Command ToggleCardNumberCommand { get; set; }
public Command ToggleCardCodeCommand { get; set; }
public Command CheckPasswordCommand { get; set; }
public Command UriOptionsCommand { get; set; }
public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; }
public AsyncCommand CopyCommand { get; set; }
public AsyncCommand GenerateUsernameCommand { get; set; }
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; }
@@ -164,7 +170,7 @@ namespace Bit.App.Pages
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<ICustomFieldItemViewModel> Fields { get; set; }
public ExtendedObservableCollection<AddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int TypeSelectedIndex
@@ -233,6 +239,11 @@ namespace Bit.App.Pages
}
}
}
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
}
public bool ShowNotesSeparator
{
get => _showNotesSeparator;
@@ -280,7 +291,7 @@ namespace Bit.App.Pages
public bool ShowOwnershipOptions => !EditMode || CloneMode;
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
public bool CloneMode { get; set; }
public CipherDetailsPage CipherDetailsPage { get; set; }
public ViewPage ViewPage { get; set; }
public bool IsLogin => Cipher?.Type == CipherType.Login;
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
public bool IsCard => Cipher?.Type == CipherType.Card;
@@ -417,7 +428,7 @@ namespace Bit.App.Pages
}
if (Cipher.Fields != null)
{
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
}
}
@@ -505,7 +516,7 @@ namespace Bit.App.Pages
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
if (Page is AddEditPage page && page.FromAutofillFramework)
{
// Close and go back to app
_deviceActionService.CloseAutofill();
@@ -514,7 +525,7 @@ namespace Bit.App.Pages
{
if (CloneMode)
{
CipherDetailsPage?.UpdateCipherId(this.Cipher.Id);
ViewPage?.UpdateCipherId(this.Cipher.Id);
}
// if the app is tombstoned then PopModalAsync would throw index out of bounds
if (Page.Navigation?.ModalStack?.Count > 0)
@@ -597,35 +608,9 @@ namespace Bit.App.Pages
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async Task GenerateUsernameAsync()
{
if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Username)
&& !await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToOverwriteTheCurrentUsername, null, AppResources.Yes, AppResources.No))
{
return;
}
var website = Cipher?.Login?.Uris?.FirstOrDefault()?.Host;
var page = new GeneratorPage(false, async (username) =>
{
try
{
Cipher.Login.Username = username;
TriggerCipherChanged();
await Page.Navigation.PopModalAsync();
}
catch (Exception ex)
{
OnGenerateUsernameException(ex);
}
}, isUsernameGenerator: true, emailWebsite: website, editMode: true);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async void UriOptions(LoginUriView uri)
{
if (!(Page as CipherAddEditPage).DoOnce())
if (!(Page as AddEditPage).DoOnce())
{
return;
}
@@ -661,9 +646,9 @@ namespace Bit.App.Pages
Uris.Add(new LoginUriView());
}
public async void FieldOptions(ICustomFieldItemViewModel field)
public async void FieldOptions(AddEditPageFieldViewModel field)
{
if (!(Page as CipherAddEditPage).DoOnce())
if (!(Page as AddEditPage).DoOnce())
{
return;
}
@@ -723,15 +708,15 @@ namespace Bit.App.Pages
}
if (Fields == null)
{
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
}
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
Fields.Add(_customFieldItemFactory.CreateCustomFieldItem(new FieldView
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
{
Type = type,
Name = string.IsNullOrWhiteSpace(name) ? null : name,
NewField = true,
}, true, Cipher, null, null, FieldOptionsCommand));
}));
}
}
@@ -793,7 +778,7 @@ namespace Bit.App.Pages
TriggerCipherChanged();
// Linked Custom Fields only apply to a specific item type
foreach (var field in Fields.OfType<LinkedCustomFieldItemViewModel>().ToList())
foreach (var field in Fields.Where(f => f.IsLinkedType).ToList())
{
Fields.Remove(field);
}
@@ -854,14 +839,38 @@ namespace Bit.App.Pages
private void TriggerCipherChanged()
{
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties);
}
private async void CheckPasswordAsync()
{
if (!(Page as BaseContentPage).DoOnce())
{
return;
}
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
if (matches > 0)
{
await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed,
matches.ToString("N0")));
}
else
{
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
}
}
private async Task CopyTotpClipboardAsync()
{
try
{
await _clipboardService.CopyTextAsync(Cipher.Login.Totp);
await _clipboardService.CopyTextAsync(_cipher.Login.Totp);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
}
catch (Exception ex)
@@ -869,11 +878,114 @@ namespace Bit.App.Pages
_logger.Exception(ex);
}
}
}
private async void OnGenerateUsernameException(Exception ex)
public class AddEditPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
private bool _booleanValue;
private int _linkedFieldOptionSelectedIndex;
private string[] _additionalFieldProperties = new string[]
{
_logger.Exception(ex);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(IsLinkedType),
};
public AddEditPageFieldViewModel(CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
BooleanValue = IsBooleanType && field.Value == "true";
LinkedFieldOptionSelectedIndex = !Field.LinkedId.HasValue ? 0 :
LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties);
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public bool BooleanValue
{
get => _booleanValue;
set
{
SetProperty(ref _booleanValue, value);
if (IsBooleanType)
{
Field.Value = value ? "true" : "false";
}
}
}
public int LinkedFieldOptionSelectedIndex
{
get => _linkedFieldOptionSelectedIndex;
set
{
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
{
LinkedFieldValueChanged();
}
}
}
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
{
get => _cipher.LinkedFieldOptions?
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
.ToList();
}
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool IsTextType => _field.Type == FieldType.Text;
public bool IsBooleanType => _field.Type == FieldType.Boolean;
public bool IsHiddenType => _field.Type == FieldType.Hidden;
public bool IsLinkedType => _field.Type == FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField);
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()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
}
}
}

View File

@@ -17,7 +17,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitAsyncCommand}" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>

View File

@@ -51,6 +51,14 @@ namespace Bit.App.Pages
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void ChooseFile_Clicked(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
@@ -10,7 +8,6 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -23,7 +20,6 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ILogger _logger;
private CipherView _cipher;
private Cipher _cipherDomain;
private bool _hasAttachments;
@@ -39,10 +35,8 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_logger = ServiceContainer.Resolve<ILogger>();
Attachments = new ExtendedObservableCollection<AttachmentView>();
DeleteAttachmentCommand = new Command<AttachmentView>(DeleteAsync);
SubmitAsyncCommand = new AsyncCommand(SubmitAsync, allowsMultipleExecutions: false);
PageTitle = AppResources.Attachments;
}
@@ -65,7 +59,6 @@ namespace Bit.App.Pages
}
public byte[] FileData { get; set; }
public Command DeleteAttachmentCommand { get; set; }
public ICommand SubmitAsyncCommand { get; }
public async Task InitAsync()
{
@@ -132,7 +125,6 @@ namespace Bit.App.Pages
}
catch (ApiException e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
@@ -140,12 +132,6 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
catch (Exception e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
}
return false;
}

View File

@@ -137,11 +137,11 @@ namespace Bit.App.Pages
}
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
{
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true);
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
return;
}
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
fromAutofill: true);
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
}

View File

@@ -1,75 +0,0 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public abstract class BaseCipherViewModel : BaseViewModel
{
private readonly IAuditService _auditService;
protected readonly IDeviceActionService _deviceActionService;
protected readonly ILogger _logger;
protected readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher;
protected abstract string[] AdditionalPropertiesToRaiseOnCipherChanged { get; }
public BaseCipherViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
CheckPasswordCommand = new AsyncCommand(CheckPasswordAsync, allowsMultipleExecutions: false);
}
public CipherView Cipher
{
get => _cipher;
set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged);
}
public AsyncCommand CheckPasswordCommand { get; }
protected async Task CheckPasswordAsync()
{
try
{
if (string.IsNullOrWhiteSpace(Cipher?.Login?.Password))
{
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword);
var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(matches > 0
? string.Format(AppResources.PasswordExposed, matches.ToString("N0"))
: AppResources.PasswordSafe);
}
catch (ApiException apiException)
{
_logger.Exception(apiException);
await _deviceActionService.HideLoadingAsync();
if (apiException?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(apiException.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
catch (Exception ex)
{
_logger.Exception(ex);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
}
}
}
}

View File

@@ -10,6 +10,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -156,7 +157,7 @@ namespace Bit.App.Pages
}
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
{
var page = new CipherDetailsPage(cipher.Id);
var page = new ViewPage(cipher.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)

View File

@@ -140,6 +140,30 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Filter}" />
</StackLayout>
<StackLayout
IsVisible="{Binding ShowTotpFilter}"
Orientation="Horizontal"
Margin="0,5,10,0">
<Label
Text="{u:I18n DisplayItemsContainingTOTP}"
LineBreakMode="TailTruncation"
Margin="10,0"
StyleClass="text-md, text-muted"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding TotpFilterEnable}"
StyleClass="box-value"
HorizontalOptions="End">
<Switch.Behaviors>
<xct:EventToCommandBehavior
EventName="Toggled"
Command="{Binding TotpFilterCommand}" />
</Switch.Behaviors>
</Switch>
</StackLayout>
<StackLayout
VerticalOptions="CenterAndExpand"
Padding="20, 0"

View File

@@ -22,7 +22,6 @@ namespace Bit.App.Pages
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ICipherService _cipherService;
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly GroupingsPageViewModel _vm;
private readonly string _pageName;
@@ -30,7 +29,7 @@ namespace Bit.App.Pages
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)
PreviousPageInfo previousPage = null, bool deleted = false)
{
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent();
@@ -42,7 +41,6 @@ namespace Bit.App.Pages
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_vm = BindingContext as GroupingsPageViewModel;
_vm.Page = this;
_vm.MainPage = mainPage;
@@ -50,7 +48,6 @@ namespace Bit.App.Pages
_vm.FolderId = folderId;
_vm.CollectionId = collectionId;
_vm.Deleted = deleted;
_vm.ShowTotp = showTotp;
_previousPage = previousPage;
if (pageTitle != null)
{
@@ -72,7 +69,7 @@ namespace Bit.App.Pages
ToolbarItems.Add(_lockItem);
ToolbarItems.Add(_exitItem);
}
if (deleted || showTotp)
if (deleted)
{
_absLayout.Children.Remove(_fab);
ToolbarItems.Remove(_addItem);
@@ -196,7 +193,7 @@ namespace Bit.App.Pages
{
base.OnDisappearing();
IsBusy = false;
_vm.StopCiphersTotpTick().FireAndForget();
_vm.StopCiphersTotpTick();
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
_accountAvatar?.OnDisappearing();
@@ -204,54 +201,42 @@ namespace Bit.App.Pages
private async void RowSelected(object sender, SelectionChangedEventArgs e)
{
try
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
{
((ExtendedCollectionView)sender).SelectedItem = null;
if (!DoOnce())
{
return;
}
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
{
await _vm.SelectCipherAsync(totpItem.Cipher);
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;
}
if (item.IsTrash)
{
await _vm.SelectTrashAsync();
}
else if (item.IsTotpCode)
{
await _vm.SelectTotpCodesAsync();
}
else if (item.Cipher != null)
{
await _vm.SelectCipherAsync(item.Cipher);
}
else if (item.Folder != null)
{
await _vm.SelectFolderAsync(item.Folder);
}
else if (item.Collection != null)
{
await _vm.SelectCollectionAsync(item.Collection);
}
else if (item.Type != null)
{
await _vm.SelectTypeAsync(item.Type.Value);
}
return;
}
catch (Exception ex)
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
_platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
await _vm.SelectCipherAsync(totpItem.Cipher);
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;
}
if (item.IsTrash)
{
await _vm.SelectTrashAsync();
}
else if (item.Cipher != null)
{
await _vm.SelectCipherAsync(item.Cipher);
}
else if (item.Folder != null)
{
await _vm.SelectFolderAsync(item.Folder);
}
else if (item.Collection != null)
{
await _vm.SelectCollectionAsync(item.Collection);
}
else if (item.Type != null)
{
await _vm.SelectTypeAsync(item.Type.Value);
}
}
@@ -294,7 +279,7 @@ namespace Bit.App.Pages
}
if (!_vm.Deleted && DoOnce())
{
var page = new CipherAddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
@@ -308,11 +293,11 @@ namespace Bit.App.Pages
await _accountListOverlay.HideAsync();
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
{
await Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(_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 CipherAddEditPage(_previousPage.CipherId)));
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
}
_previousPage = null;
}

View File

@@ -17,7 +17,6 @@ namespace Bit.App.Pages
public string ItemCount { get; set; }
public bool FuzzyAutofill { get; set; }
public bool IsTrash { get; set; }
public bool IsTotpCode { get; set; }
public string Name
{
@@ -39,10 +38,6 @@ namespace Bit.App.Pages
{
_name = Collection.Name;
}
else if (IsTotpCode)
{
_name = AppResources.VerificationCodes;
}
else if (Type != null)
{
switch (Type.Value)
@@ -87,10 +82,6 @@ namespace Bit.App.Pages
{
_icon = BitwardenIcons.Collection;
}
else if (IsTotpCode)
{
_icon = BitwardenIcons.Clock;
}
else if (Type != null)
{
switch (Type.Value)

View File

@@ -105,7 +105,7 @@ namespace Bit.App.Pages
public async Task CopyToClipboardAsync()
{
await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty));
await _clipboardService.CopyTextAsync(TotpCodeFormatted);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
}

View File

@@ -82,6 +82,9 @@ namespace Bit.App.Pages
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
TotpFilterCommand = new AsyncCommand(LoadAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -98,11 +101,13 @@ namespace Bit.App.Pages
public bool HasCiphers { get; set; }
public bool HasFolders { get; set; }
public bool HasCollections { get; set; }
public string ShowTotpCodesAccessibilityText => TotpFilterEnable ?
AppResources.AuthenticationCodesListIsVisibleActivateToShowCipherList
: AppResources.CipherListIsVisibleActivateToShowAuthenticationCodesList;
public bool ShowNoFolderCipherGroup => NoFolderCiphers != null
&& NoFolderCiphers.Count < NoFolderListSize
&& (Collections is null || !Collections.Any());
public List<CipherView> Ciphers { get; set; }
public List<CipherView> TOTPCiphers { get; set; }
public List<CipherView> FavoriteCiphers { get; set; }
public List<CipherView> NoFolderCiphers { get; set; }
public List<FolderView> Folders { get; set; }
@@ -160,15 +165,21 @@ namespace Bit.App.Pages
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowTotp
public bool ShowTotpFilter
{
get => _showTotpFilter;
set => SetProperty(ref _showTotpFilter, value);
}
public bool TotpFilterEnable
{
get => _totpFilterEnable;
set => SetProperty(ref _totpFilterEnable, value);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public ICommand TotpFilterCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -200,12 +211,14 @@ namespace Bit.App.Pages
{
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
Loading = true;
ShowList = false;
ShowAddCipherButton = !Deleted;
ShowTotpFilter = Type == CipherType.Login && canAccessPremium;
var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage;
@@ -230,8 +243,6 @@ namespace Bit.App.Pages
}
if (MainPage)
{
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
groupedItems.Add(new GroupingsPageListGroup(
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
{
@@ -290,11 +301,6 @@ namespace Bit.App.Pages
{
CreateCipherGroupedItems(groupedItems);
}
if (ShowTotp && (!TOTPCiphers?.Any() ?? false))
{
Page.Navigation.PopAsync();
return;
}
if (ShowNoFolderCipherGroup)
{
var noFolderCiphersListItems = NoFolderCiphers.Select(
@@ -381,30 +387,14 @@ namespace Bit.App.Pages
}
}
private void AddTotpGroupItem(List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
{
if (TOTPCiphers?.Any() == true)
{
groupedItems.Insert(0, new GroupingsPageListGroup(
AppResources.Totp, 1, uppercaseGroupNames, false)
{
new GroupingsPageListItem
{
IsTotpCode = true,
Type = CipherType.Login,
ItemCount = TOTPCiphers.Count().ToString("N0")
}
});
}
}
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
{
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
_totpTickCts?.Cancel();
if (ShowTotp)
if (TotpFilterEnable)
{
var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted && !string.IsNullOrEmpty(c.Login.Totp))
.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
@@ -448,7 +438,7 @@ namespace Bit.App.Pages
public async Task SelectCipherAsync(CipherView cipher)
{
var page = new CipherDetailsPage(cipher.Id);
var page = new ViewPage(cipher.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
@@ -495,13 +485,6 @@ namespace Bit.App.Pages
await Page.Navigation.PushAsync(page);
}
public async Task SelectTotpCodesAsync()
{
var page = new GroupingsPage(false, CipherType.Login, null, null, AppResources.VerificationCodes, _vaultFilterSelection, null,
false, true);
await Page.Navigation.PushAsync(page);
}
public async Task ExitAsync()
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
@@ -536,11 +519,9 @@ namespace Bit.App.Pages
private async Task LoadDataAsync()
{
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
NoDataText = AppResources.NoItems;
_allCiphers = await GetAllCiphersAsync();
HasCiphers = _allCiphers.Any();
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp) && (c.OrganizationUseTotp || canAccessPremium)).ToList();
FavoriteCiphers?.Clear();
NoFolderCiphers?.Clear();
_folderCounts.Clear();
@@ -566,10 +547,6 @@ namespace Bit.App.Pages
Filter = c => c.IsDeleted;
NoDataText = AppResources.NoItemsTrash;
}
else if (ShowTotp)
{
Filter = c => c.Type == CipherType.Login && !c.IsDeleted && !string.IsNullOrEmpty(c.Login?.Totp);
}
else if (Type != null)
{
Filter = c => c.Type == Type.Value && !c.IsDeleted;

View File

@@ -170,10 +170,27 @@ namespace Bit.App.Pages
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
{
if (!string.IsNullOrWhiteSpace(ViewModel.TotpAuthenticationKey))
var text = ViewModel.TotpAuthenticationKey;
if (!string.IsNullOrWhiteSpace(text))
{
_callback(ViewModel.TotpAuthenticationKey);
return;
if (text.StartsWith("otpauth://totp"))
{
_callback(text);
return;
}
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
!string.IsNullOrWhiteSpace(uri?.Query))
{
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
foreach (var part in queryParts)
{
if (part.StartsWith("secret="))
{
_callback(part.Substring(7)?.ToUpperInvariant());
return;
}
}
}
}
_callback(null);
}

View File

@@ -2,20 +2,18 @@
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.CipherDetailsPage"
x:Class="Bit.App.Pages.ViewPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherDetailsPageViewModel"
x:DataType="pages:ViewPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:CipherDetailsPageViewModel />
<pages:ViewPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
@@ -48,25 +46,6 @@
<ToolbarItem Text="{u:I18n Clone}" Clicked="Clone_Clicked" Order="Secondary"
x:Name="_cloneItem" x:Key="cloneItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
<ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20" x:Name="_mainLayout">
<StackLayout StyleClass="box">
@@ -580,10 +559,83 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:ViewPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{Binding Field.Name, Mode=OneWay}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsLinkedType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsBooleanType}"
Margin="0, 5, 0, 0" />
<StackLayout IsVisible="{Binding IsHiddenType}"
Grid.Row="1"
Grid.Column="0">
<controls:MonoLabel
Text="{Binding ColoredHiddenValue, Mode=OneWay}"
StyleClass="box-value, text-html"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
Command="{Binding ToggleHiddenValueCommand}"
IsVisible="{Binding ShowViewHidden}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding BindingContext.CopyFieldCommand, Source={x:Reference _page}}"
CommandParameter="{Binding Field}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
<StackLayout StyleClass="box-row-header">

View File

@@ -9,18 +9,18 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class CipherDetailsPage : BaseContentPage
public partial class ViewPage : BaseContentPage
{
private readonly IBroadcasterService _broadcasterService;
private readonly ISyncService _syncService;
private CipherDetailsPageViewModel _vm;
private ViewPageViewModel _vm;
public CipherDetailsPage(string cipherId)
public ViewPage(string cipherId)
{
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_vm = BindingContext as CipherDetailsPageViewModel;
_vm = BindingContext as ViewPageViewModel;
_vm.Page = this;
_vm.CipherId = cipherId;
SetActivityIndicator(_mainContent);
@@ -40,7 +40,7 @@ namespace Bit.App.Pages
}
}
public CipherDetailsPageViewModel ViewModel => _vm;
public ViewPageViewModel ViewModel => _vm;
public void UpdateCipherId(string cipherId)
{
@@ -55,7 +55,7 @@ namespace Bit.App.Pages
IsBusy = true;
}
_broadcasterService.Subscribe(nameof(CipherDetailsPage), async (message) =>
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
{
try
{
@@ -111,8 +111,8 @@ namespace Bit.App.Pages
{
base.OnDisappearing();
IsBusy = false;
_vm.StopCiphersTotpTick().FireAndForget();
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.StopCiphersTotpTick();
}
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
@@ -140,7 +140,7 @@ namespace Bit.App.Pages
{
return;
}
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_vm.CipherId)));
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_vm.CipherId)));
}
}
}
@@ -212,7 +212,7 @@ namespace Bit.App.Pages
{
return;
}
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
@@ -267,7 +267,7 @@ namespace Bit.App.Pages
}
else if (selection == AppResources.Clone)
{
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}

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