mirror of
https://github.com/bitwarden/mobile
synced 2025-12-30 15:13:24 +00:00
Compare commits
61 Commits
feature/to
...
v2022.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c50028d0d3 | ||
|
|
0b9a16beef | ||
|
|
9b93dbb8e3 | ||
|
|
261610b700 | ||
|
|
fd18dccce9 | ||
|
|
04daaf4e3a | ||
|
|
a08d89a002 | ||
|
|
63e1185537 | ||
|
|
1e8a6ca81f | ||
|
|
6fe7e9ce1b | ||
|
|
70ee24d82a | ||
|
|
28576bbf49 | ||
|
|
7f9dfd3dae | ||
|
|
115aee2026 | ||
|
|
86fee6f04e | ||
|
|
2deab59b35 | ||
|
|
b4737457a8 | ||
|
|
305c770c58 | ||
|
|
afa9e23707 | ||
|
|
87fb5cf2ae | ||
|
|
3f8e00985c | ||
|
|
533928a4f1 | ||
|
|
b7048de2a1 | ||
|
|
2016eadb0d | ||
|
|
68b5bc0964 | ||
|
|
119fc5812b | ||
|
|
b628c1990e | ||
|
|
183bfa0ab2 | ||
|
|
b1fb867b6e | ||
|
|
673ba9f3cc | ||
|
|
cdd9a5ff4d | ||
|
|
d204e812e1 | ||
|
|
9163b9e4de | ||
|
|
ecd4da08ee | ||
|
|
525288d804 | ||
|
|
e829279e29 | ||
|
|
3d9555d420 | ||
|
|
5f7a1e769a | ||
|
|
8b118408fa | ||
|
|
de41845e3e | ||
|
|
61597585b5 | ||
|
|
e04b250a73 | ||
|
|
4fbe1b40e3 | ||
|
|
3ef5b576ac | ||
|
|
570b56364a | ||
|
|
ae4e8e2d8e | ||
|
|
2c8406d0ad | ||
|
|
94bd5ceed3 | ||
|
|
aa6be3d691 | ||
|
|
97fe65647a | ||
|
|
ee8b8866e0 | ||
|
|
3128a4c5c8 | ||
|
|
8ec6545bbc | ||
|
|
90a6850d76 | ||
|
|
16f70dc0ce | ||
|
|
f0ebc5e644 | ||
|
|
03c5dd78c1 | ||
|
|
e2b6e99a0c | ||
|
|
263aeef030 | ||
|
|
f809170c51 | ||
|
|
c2fcc0ac52 |
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,7 +22,7 @@
|
||||
|
||||
|
||||
## Before you submit
|
||||
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
|
||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||
- 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
|
||||
|
||||
89
.github/workflows/build.yml
vendored
89
.github/workflows/build.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
android:
|
||||
name: Android
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
needs: setup
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
@@ -68,6 +68,26 @@ 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
|
||||
@@ -212,7 +232,7 @@ jobs:
|
||||
|
||||
f-droid:
|
||||
name: F-Droid Build
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||
@@ -222,6 +242,26 @@ 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
|
||||
@@ -401,10 +441,17 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "appcenter-ios-token"
|
||||
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
|
||||
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
@@ -595,10 +642,17 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "crowdin-api-token"
|
||||
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
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
||||
@@ -655,11 +709,18 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
if: failure()
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
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
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||
|
||||
17
.github/workflows/crowdin-pull.yml
vendored
17
.github/workflows/crowdin-pull.yml
vendored
@@ -24,13 +24,20 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "crowdin-api-token"
|
||||
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
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
||||
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@@ -53,18 +53,38 @@ 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,
|
||||
@@ -78,6 +98,22 @@ 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
|
||||
@@ -89,6 +125,7 @@ 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
|
||||
@@ -96,6 +133,15 @@ 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:
|
||||
@@ -161,5 +207,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
|
||||
|
||||
67
.github/workflows/version-auto-bump.yml
vendored
Normal file
67
.github/workflows/version-auto-bump.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
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
|
||||
22
renovate.json
Normal file
22
renovate.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Accessibility
|
||||
{
|
||||
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")]
|
||||
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden", Exported = true)]
|
||||
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
|
||||
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
|
||||
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v12.1</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.8.10</Version>
|
||||
<Version>1.9.0</Version>
|
||||
</PackageReference>
|
||||
<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.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.Essentials">
|
||||
<Version>1.7.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>122.0.0</Version>
|
||||
<Version>123.0.8</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.4.0.4" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
|
||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
|
||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>117.0.1</Version>
|
||||
<Version>118.0.1.2</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -151,6 +151,7 @@
|
||||
<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" />
|
||||
@@ -175,6 +176,7 @@
|
||||
<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" />
|
||||
@@ -212,6 +214,13 @@
|
||||
<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" />
|
||||
@@ -279,6 +288,8 @@
|
||||
</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>
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
{
|
||||
@@ -270,8 +271,7 @@ namespace Bit.Droid.Autofill
|
||||
return null;
|
||||
}
|
||||
intent.PutExtra("autofillFrameworkUri", uri);
|
||||
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
|
||||
PendingIntentFlags.CancelCurrent);
|
||||
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
|
||||
|
||||
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(),
|
||||
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
|
||||
}
|
||||
var slice = CreateInlinePresentationSlice(
|
||||
inlinePresentationSpec,
|
||||
|
||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")]
|
||||
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden", Exported = true)]
|
||||
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
|
||||
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
|
||||
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
|
||||
|
||||
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ 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 AndroidX.Core.Content;
|
||||
using Android.Views;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
@@ -18,7 +20,9 @@ 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
|
||||
{
|
||||
@@ -35,6 +39,7 @@ 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()}";
|
||||
@@ -45,7 +50,7 @@ namespace Bit.Droid
|
||||
{
|
||||
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
||||
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
||||
PendingIntentFlags.UpdateCurrent);
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
|
||||
|
||||
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
|
||||
StrictMode.SetThreadPolicy(policy);
|
||||
@@ -56,6 +61,7 @@ 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;
|
||||
@@ -70,7 +76,7 @@ namespace Bit.Droid
|
||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||
});
|
||||
|
||||
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
||||
_logger.InitAsync();
|
||||
|
||||
var toplayout = Window?.DecorView?.RootView;
|
||||
if (toplayout != null)
|
||||
@@ -81,8 +87,9 @@ 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) =>
|
||||
{
|
||||
@@ -273,7 +280,7 @@ namespace Bit.Droid
|
||||
{
|
||||
var intent = new Intent(this, Class);
|
||||
intent.AddFlags(ActivityFlags.SingleTop);
|
||||
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
|
||||
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(0, true));
|
||||
// register for all NDEF tags starting with http och https
|
||||
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
||||
ndef.AddDataScheme("http");
|
||||
@@ -401,5 +408,38 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ 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(
|
||||
@@ -71,8 +72,9 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||
ServiceContainer.Resolve<ILogger>("logger"));
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
ServiceContainer.Resolve<ILogger>("logger"),
|
||||
ServiceContainer.Resolve<IMessagingService>("messagingService"));
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
}
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@@ -193,5 +195,12 @@ namespace Bit.Droid
|
||||
{
|
||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||
}
|
||||
|
||||
private void InitializeAppSetup()
|
||||
{
|
||||
var appSetup = new AppSetup();
|
||||
appSetup.InitializeServicesLastChance();
|
||||
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,49 @@
|
||||
<?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>
|
||||
<?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>
|
||||
@@ -1,7 +1,9 @@
|
||||
#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;
|
||||
@@ -16,34 +18,41 @@ namespace Bit.Droid.Push
|
||||
{
|
||||
public async override void OnNewToken(string token)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
try {
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
|
||||
await stateService.SetPushRegisteredTokenAsync(token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
await stateService.SetPushRegisteredTokenAsync(token);
|
||||
await pushNotificationService.RegisterAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
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 (JsonReaderException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(ex.ToString());
|
||||
Logger.Instance.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
15
src/Android/Resources/drawable/ic_launcher_monochrome.xml
Normal file
15
src/Android/Resources/drawable/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
4
src/Android/Resources/drawable/ic_notification.xml
Normal file
4
src/Android/Resources/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
14
src/Android/Resources/drawable/logo_rounded.xml
Normal file
14
src/Android/Resources/drawable/logo_rounded.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
@@ -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_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@@ -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_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@@ -4,6 +4,7 @@
|
||||
<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">
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<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">
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||
android:notificationTimeout="100"
|
||||
android:canRetrieveWindowContent="true"/>
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:isAccessibilityTool="false"/>
|
||||
@@ -1,9 +1,14 @@
|
||||
#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
|
||||
@@ -23,6 +28,11 @@ 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();
|
||||
@@ -47,6 +57,39 @@ 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
|
||||
|
||||
@@ -5,6 +5,7 @@ using Android.Content;
|
||||
using Android.OS;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Droid.Receivers;
|
||||
using Bit.Droid.Utilities;
|
||||
using Plugin.CurrentActivity;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
@@ -23,7 +24,7 @@ namespace Bit.Droid.Services
|
||||
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
||||
0,
|
||||
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
||||
PendingIntentFlags.UpdateCurrent));
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
|
||||
}
|
||||
|
||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||
|
||||
@@ -964,5 +964,14 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using Java.Lang;
|
||||
namespace Bit.Droid.Tile
|
||||
{
|
||||
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
|
||||
Icon = "@drawable/shield")]
|
||||
Icon = "@drawable/shield", Exported = true)]
|
||||
[IntentFilter(new string[] { ActionQsTile })]
|
||||
[Register("com.x8bit.bitwarden.AutofillTileService")]
|
||||
public class AutofillTileService : TileService
|
||||
|
||||
@@ -14,7 +14,7 @@ using Java.Lang;
|
||||
|
||||
namespace Bit.Droid.Tile
|
||||
{
|
||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/PasswordGenerator",
|
||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Exported = true, Label = "@string/PasswordGenerator",
|
||||
Icon = "@drawable/generate")]
|
||||
[IntentFilter(new string[] { ActionQsTile })]
|
||||
[Register("com.x8bit.bitwarden.GeneratorTileService")]
|
||||
|
||||
@@ -15,7 +15,8 @@ using Java.Lang;
|
||||
namespace Bit.Droid.Tile
|
||||
{
|
||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
|
||||
Icon = "@drawable/shield")]
|
||||
Icon = "@drawable/shield",
|
||||
Exported = true)]
|
||||
[IntentFilter(new string[] { ActionQsTile })]
|
||||
[Register("com.x8bit.bitwarden.MyVaultTileService")]
|
||||
public class MyVaultTileService : TileService
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Bit.App.Utilities;
|
||||
|
||||
@@ -47,5 +49,22 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace Bit.Droid
|
||||
{
|
||||
[Activity(
|
||||
NoHistory = true,
|
||||
LaunchMode = LaunchMode.SingleTop)]
|
||||
LaunchMode = LaunchMode.SingleTop,
|
||||
Exported = true)]
|
||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||
DataScheme = "bitwarden")]
|
||||
|
||||
@@ -49,5 +49,6 @@ namespace Bit.App.Abstractions
|
||||
float GetSystemFontSizeScale();
|
||||
Task OnAccountSwitchCompleteAsync();
|
||||
Task SetScreenCaptureAllowedAsync();
|
||||
void OpenAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<RootNamespace>Bit.App</RootNamespace>
|
||||
<AssemblyName>BitwardenApp</AssemblyName>
|
||||
<Configurations>Debug;Release;FDroid</Configurations>
|
||||
@@ -14,13 +14,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
|
||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
|
||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
|
||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
|
||||
<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.2478" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
|
||||
<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>
|
||||
@@ -97,11 +98,11 @@
|
||||
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
||||
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
|
||||
<DependentUpon>AddEditPage.xaml</DependentUpon>
|
||||
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
|
||||
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
|
||||
<DependentUpon>ViewPage.xaml</DependentUpon>
|
||||
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
|
||||
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||
@@ -122,11 +123,20 @@
|
||||
<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\" />
|
||||
@@ -412,6 +422,12 @@
|
||||
<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\" />
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -25,13 +26,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)
|
||||
{
|
||||
@@ -47,10 +48,9 @@ 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,6 +140,10 @@ 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)
|
||||
{
|
||||
@@ -148,11 +152,55 @@ 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))
|
||||
@@ -164,6 +212,10 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
}
|
||||
}
|
||||
if (_pendingCheckPasswordlessLoginRequests)
|
||||
{
|
||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
@@ -196,6 +248,10 @@ namespace Bit.App
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||
_isResumed = true;
|
||||
if (_pendingCheckPasswordlessLoginRequests)
|
||||
{
|
||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||
}
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ResumedAsync().FireAndForget();
|
||||
@@ -330,20 +386,20 @@ namespace Bit.App
|
||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||
if (topPage is NavigationPage navPage)
|
||||
{
|
||||
if (navPage.CurrentPage is ViewPage viewPage)
|
||||
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "view",
|
||||
CipherId = viewPage.ViewModel.CipherId
|
||||
CipherId = cipherDetailsPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
||||
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
|
||||
{
|
||||
lastPageBeforeLock = new PreviousPageInfo
|
||||
{
|
||||
Page = "edit",
|
||||
CipherId = addEditPage.ViewModel.CipherId
|
||||
CipherId = cipherAddEditPage.ViewModel.CipherId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -378,7 +434,7 @@ namespace Bit.App
|
||||
Current.MainPage = new TabsPage(Options);
|
||||
break;
|
||||
case NavigationTarget.AddEditCipher:
|
||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
||||
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
||||
break;
|
||||
case NavigationTarget.AutofillCiphers:
|
||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Collections;
|
||||
using System;
|
||||
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(
|
||||
|
||||
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Effects
|
||||
{
|
||||
public class NoEmojiKeyboardEffect : RoutingEffect
|
||||
{
|
||||
public NoEmojiKeyboardEffect()
|
||||
: base("Bitwarden.NoEmojiKeyboardEffect")
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||
{
|
||||
public partial class BooleanCustomFieldItemLayout : StackLayout
|
||||
{
|
||||
public BooleanCustomFieldItemLayout()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||
{
|
||||
public partial class HiddenCustomFieldItemLayout : StackLayout
|
||||
{
|
||||
public HiddenCustomFieldItemLayout()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||
{
|
||||
public partial class LinkedCustomFieldItemLayout : StackLayout
|
||||
{
|
||||
public LinkedCustomFieldItemLayout()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||
{
|
||||
public partial class TextCustomFieldItemLayout : StackLayout
|
||||
{
|
||||
public TextCustomFieldItemLayout()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||
{
|
||||
public interface ICustomFieldItemViewModel
|
||||
{
|
||||
FieldView Field { get; set; }
|
||||
|
||||
bool ShowCopyButton { get; }
|
||||
|
||||
void TriggerFieldChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
bool IsUrlValid(string url)
|
||||
{
|
||||
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
|
||||
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
|
||||
}
|
||||
|
||||
return IsUrlValid(BaseUrl)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -24,14 +23,6 @@ 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())
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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.Forms;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -13,18 +14,26 @@ 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 Command(async () => await SubmitAsync());
|
||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||
onException: ex =>
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||
},
|
||||
allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand SubmitCommand { get; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
@@ -37,14 +46,14 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (!Email.Contains("@"))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +63,7 @@ namespace Bit.App.Pages
|
||||
await _apiService.PostPasswordHintAsync(
|
||||
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
<Entry
|
||||
x:Name="_email"
|
||||
Text="{Binding Email}"
|
||||
IsEnabled="{Binding IsEmailEnabled}"
|
||||
Keyboard="Email"
|
||||
StyleClass="box-value">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
|
||||
@@ -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,6 +15,8 @@ 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;
|
||||
@@ -30,11 +32,10 @@ namespace Bit.App.Pages
|
||||
await _accountListOverlay.HideAsync();
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
_email.IsEnabled = false;
|
||||
}
|
||||
else
|
||||
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
|
||||
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
|
||||
|
||||
if (_vm.IsEmailEnabled)
|
||||
{
|
||||
_vm.ShowCancelButton = true;
|
||||
}
|
||||
@@ -53,7 +54,7 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Add(_getPasswordHint);
|
||||
}
|
||||
|
||||
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
|
||||
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
|
||||
{
|
||||
ToolbarItems.Add(_removeAccount);
|
||||
}
|
||||
@@ -110,7 +111,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.LogInAsync(true, _email.IsEnabled);
|
||||
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,26 +140,16 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void More_Clicked(object sender, System.EventArgs e)
|
||||
private async void More_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
await _accountListOverlay.HideAsync();
|
||||
if (!DoOnce())
|
||||
try
|
||||
{
|
||||
return;
|
||||
await _accountListOverlay.HideAsync();
|
||||
_vm.MoreCommand.Execute(null);
|
||||
}
|
||||
|
||||
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)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
||||
}
|
||||
else if (selection == AppResources.RemoveAccount)
|
||||
{
|
||||
await _vm.RemoveAccountAsync();
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
@@ -8,6 +9,7 @@ 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
|
||||
@@ -28,6 +30,7 @@ namespace Bit.App.Pages
|
||||
private bool _showCancelButton;
|
||||
private string _email;
|
||||
private string _masterPassword;
|
||||
private bool _isEmailEnabled;
|
||||
|
||||
public LoginPageViewModel()
|
||||
{
|
||||
@@ -44,6 +47,7 @@ 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)
|
||||
{
|
||||
@@ -81,10 +85,19 @@ 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; }
|
||||
@@ -201,6 +214,28 @@ 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;
|
||||
|
||||
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?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>
|
||||
43
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
43
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
175
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -69,12 +69,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogIn_Clicked(object sender, EventArgs e)
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
CopyAppOptions();
|
||||
await _vm.LogInAsync();
|
||||
_vm.LogInCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
@@ -8,13 +9,15 @@ 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;
|
||||
@@ -23,6 +26,7 @@ namespace Bit.App.Pages
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private string _orgIdentifier;
|
||||
|
||||
@@ -37,9 +41,11 @@ 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 Command(async () => await LogInAsync());
|
||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string OrgIdentifier
|
||||
@@ -48,7 +54,7 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public Command LogInCommand { get; }
|
||||
public ICommand LogInCommand { get; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
@@ -65,81 +71,91 @@ 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);
|
||||
ssoToken = response.Token;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_logger.Exception(e);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
|
||||
await _platformUtilsService.ShowDialogAsync(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;
|
||||
}
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, redirectUri, OrgIdentifier);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,11 +174,11 @@ namespace Bit.App.Pages
|
||||
return code;
|
||||
}
|
||||
|
||||
private async Task LogIn(string code, string codeVerifier, string redirectUri, string orgId)
|
||||
private async Task LogIn(string code, string codeVerifier, string orgId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri, orgId);
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
IsToggled="{Binding AcceptPolicies}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="Start"
|
||||
Margin="{Binding SwitchMargin}"/>
|
||||
Margin="0, 0, 10, 0"/>
|
||||
<Label StyleClass="box-footer-label"
|
||||
HorizontalOptions="Fill">
|
||||
<Label.FormattedText>
|
||||
|
||||
@@ -61,14 +61,6 @@ 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; }
|
||||
|
||||
@@ -17,15 +17,19 @@
|
||||
<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 Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||
x:Name="_moreItem" x:Key="moreItem"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}" />
|
||||
<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 Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||
Clicked="Methods_Clicked"
|
||||
Order="Secondary"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
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;
|
||||
@@ -137,21 +136,6 @@ 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())
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -12,6 +13,7 @@ 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;
|
||||
|
||||
@@ -30,6 +32,7 @@ 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;
|
||||
@@ -51,9 +54,11 @@ 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
|
||||
@@ -111,6 +116,7 @@ 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; }
|
||||
@@ -337,6 +343,15 @@ 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();
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<?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>
|
||||
@@ -15,6 +19,8 @@
|
||||
<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}"
|
||||
@@ -41,60 +47,302 @@
|
||||
in ContentView.-->
|
||||
<ContentView>
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<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>
|
||||
<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">
|
||||
<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"
|
||||
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>
|
||||
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" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n Options, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<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 Type}"
|
||||
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>
|
||||
<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="_typePicker"
|
||||
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||
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}"
|
||||
StyleClass="box-label" />
|
||||
<Picker
|
||||
x:Name="_passwordTypePicker"
|
||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout Spacing="0"
|
||||
@@ -128,7 +376,11 @@
|
||||
Text="{Binding WordSeparator}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value" />
|
||||
StyleClass="box-value">
|
||||
<Entry.Effects>
|
||||
<effects:NoEmojiKeyboardEffect />
|
||||
</Entry.Effects>
|
||||
</Entry>
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
|
||||
@@ -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)
|
||||
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
|
||||
{
|
||||
_tabsPage = tabsPage;
|
||||
InitializeComponent();
|
||||
@@ -27,6 +27,10 @@ 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)
|
||||
{
|
||||
@@ -47,10 +51,12 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Add(_historyItem);
|
||||
}
|
||||
}
|
||||
if (isIos)
|
||||
{
|
||||
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
||||
}
|
||||
_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);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
@@ -97,16 +103,6 @@ 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())
|
||||
@@ -124,7 +120,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Select_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
_selectAction?.Invoke(_vm.Password);
|
||||
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
|
||||
}
|
||||
|
||||
private async void History_Clicked(object sender, EventArgs e)
|
||||
@@ -150,7 +146,20 @@ namespace Bit.App.Pages
|
||||
{
|
||||
await base.UpdateOnThemeChanged();
|
||||
|
||||
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
if (_vm != null)
|
||||
{
|
||||
if (_vm.IsUsername)
|
||||
{
|
||||
_vm.RedrawUsername();
|
||||
}
|
||||
else
|
||||
{
|
||||
_vm.RedrawPassword();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
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
|
||||
{
|
||||
@@ -13,11 +19,16 @@ 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;
|
||||
@@ -30,21 +41,69 @@ namespace Bit.App.Pages
|
||||
private string _wordSeparator;
|
||||
private bool _capitalize;
|
||||
private bool _includeNumber;
|
||||
private int _typeSelectedIndex;
|
||||
private string _username;
|
||||
private GeneratorType _generatorTypeSelected;
|
||||
private int _passwordTypeSelectedIndex;
|
||||
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>(
|
||||
"passwordGenerationService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
|
||||
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
|
||||
_tokenService = ServiceContainer.Resolve<ITokenService>();
|
||||
|
||||
PageTitle = AppResources.PasswordGenerator;
|
||||
TypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
|
||||
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);
|
||||
}
|
||||
|
||||
public List<string> TypeOptions { get; set; }
|
||||
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 string Password
|
||||
{
|
||||
@@ -56,7 +115,18 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
public string ColoredPassword => PasswordFormatter.FormatPassword(Password);
|
||||
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 bool IsPassword
|
||||
{
|
||||
@@ -64,6 +134,32 @@ 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;
|
||||
@@ -235,6 +331,20 @@ 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;
|
||||
@@ -247,24 +357,276 @@ namespace Bit.App.Pages
|
||||
|
||||
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
|
||||
|
||||
public int TypeSelectedIndex
|
||||
public GeneratorType GeneratorTypeSelected
|
||||
{
|
||||
get => _typeSelectedIndex;
|
||||
get => _generatorTypeSelected;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _typeSelectedIndex, value))
|
||||
if (SetProperty(ref _generatorTypeSelected, value))
|
||||
{
|
||||
IsPassword = value == 0;
|
||||
var task = SaveOptionsAsync();
|
||||
IsUsername = value == GeneratorType.Username;
|
||||
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
|
||||
SaveOptionsAsync().FireAndForget();
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int PasswordTypeSelectedIndex
|
||||
{
|
||||
get => _passwordTypeSelectedIndex;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _passwordTypeSelectedIndex, value))
|
||||
{
|
||||
IsPassword = value == 0;
|
||||
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
|
||||
SaveOptionsAsync().FireAndForget();
|
||||
SaveUsernameOptionsAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
await RegenerateAsync();
|
||||
|
||||
_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();
|
||||
|
||||
_doneIniting = true;
|
||||
}
|
||||
|
||||
@@ -274,6 +636,11 @@ namespace Bit.App.Pages
|
||||
await _passwordGenerationService.AddHistoryAsync(Password);
|
||||
}
|
||||
|
||||
public async Task RegenerateUsernameAsync()
|
||||
{
|
||||
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
|
||||
}
|
||||
|
||||
public void RedrawPassword()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_password))
|
||||
@@ -282,6 +649,14 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public void RedrawUsername()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
TriggerPropertyChanged(nameof(ColoredUsername));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveOptionsAsync(bool regenerate = true)
|
||||
{
|
||||
if (!_doneIniting)
|
||||
@@ -291,6 +666,7 @@ namespace Bit.App.Pages
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
await _passwordGenerationService.SaveOptionsAsync(_options);
|
||||
|
||||
LoadFromOptions();
|
||||
if (regenerate)
|
||||
{
|
||||
@@ -298,6 +674,25 @@ 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);
|
||||
@@ -317,15 +712,28 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(Password);
|
||||
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFromOptions()
|
||||
{
|
||||
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
|
||||
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
|
||||
IsPassword = TypeSelectedIndex == 0;
|
||||
PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
|
||||
IsPassword = PasswordTypeSelectedIndex == 0;
|
||||
MinNumber = _options.MinNumber.GetValueOrDefault();
|
||||
MinSpecial = _options.MinSpecial.GetValueOrDefault();
|
||||
Special = _options.Special.GetValueOrDefault();
|
||||
@@ -339,10 +747,30 @@ 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 = TypeSelectedIndex == 1 ? "passphrase" : "password";
|
||||
_options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
|
||||
_options.MinNumber = MinNumber;
|
||||
_options.MinSpecial = MinSpecial;
|
||||
_options.Special = Special;
|
||||
@@ -355,5 +783,51 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
<Button
|
||||
Clicked="ToggleAccessibility"
|
||||
Command="{Binding ToggleAccessibilityCommand}"
|
||||
StyleClass="box-overlay"
|
||||
RelativeLayout.XConstraint="0"
|
||||
RelativeLayout.YConstraint="0"
|
||||
|
||||
@@ -55,14 +55,6 @@ 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())
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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
|
||||
{
|
||||
@@ -12,6 +14,8 @@ 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;
|
||||
@@ -24,7 +28,11 @@ 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
|
||||
@@ -74,6 +82,8 @@ namespace Bit.App.Pages
|
||||
|
||||
#region Accessibility
|
||||
|
||||
public ICommand ToggleAccessibilityCommand { get; }
|
||||
|
||||
public string AccessibilityDescriptionLabel
|
||||
{
|
||||
get
|
||||
@@ -176,8 +186,18 @@ namespace Bit.App.Pages
|
||||
InlineAutofillToggled = !InlineAutofillToggled;
|
||||
}
|
||||
|
||||
public void ToggleAccessibility()
|
||||
public async Task ToggleAccessibilityAsync()
|
||||
{
|
||||
if (!_deviceActionService.AutofillAccessibilityServiceRunning())
|
||||
{
|
||||
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
|
||||
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
|
||||
AppResources.Decline);
|
||||
if (!accept)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
_deviceActionService.OpenAccessibilitySettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -132,20 +132,20 @@
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAndroidAutofillSettings}">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
Text="{u:I18n BlacklistedUris}"
|
||||
Text="{u:I18n AutofillBlockedUris}"
|
||||
StyleClass="box-label" />
|
||||
<Editor
|
||||
x:Name="_blacklistedUrisEditor"
|
||||
Text="{Binding AutofillBlacklistedUris}"
|
||||
x:Name="_autofillBlockedUrisEditor"
|
||||
Text="{Binding AutofillBlockedUris}"
|
||||
StyleClass="box-value"
|
||||
AutoSize="TextChanges"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
Keyboard="Url"
|
||||
Unfocused="BlacklistedUrisEditor_Unfocused" />
|
||||
Unfocused="AutofillBlockedUrisEditor_Unfocused" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n BlacklistedUrisDescription}"
|
||||
Text="{u:I18n AutofillBlockedUrisDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -45,12 +45,12 @@ namespace Bit.App.Pages
|
||||
protected async override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
await _vm.UpdateAutofillBlacklistedUris();
|
||||
await _vm.UpdateAutofillBlockedUris();
|
||||
}
|
||||
|
||||
private async void BlacklistedUrisEditor_Unfocused(object sender, FocusEventArgs e)
|
||||
private async void AutofillBlockedUrisEditor_Unfocused(object sender, FocusEventArgs e)
|
||||
{
|
||||
await _vm.UpdateAutofillBlacklistedUris();
|
||||
await _vm.UpdateAutofillBlockedUris();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Bit.App.Pages
|
||||
|
||||
|
||||
private bool _autofillSavePrompt;
|
||||
private string _autofillBlacklistedUris;
|
||||
private string _autofillBlockedUris;
|
||||
private bool _favicon;
|
||||
private bool _autoTotpCopy;
|
||||
private int _clearClipboardSelectedIndex;
|
||||
@@ -167,10 +167,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public string AutofillBlacklistedUris
|
||||
public string AutofillBlockedUris
|
||||
{
|
||||
get => _autofillBlacklistedUris;
|
||||
set => SetProperty(ref _autofillBlacklistedUris, value);
|
||||
get => _autofillBlockedUris;
|
||||
set => SetProperty(ref _autofillBlockedUris, value);
|
||||
}
|
||||
|
||||
public bool ShowAndroidAutofillSettings
|
||||
@@ -182,8 +182,8 @@ namespace Bit.App.Pages
|
||||
public async Task InitAsync()
|
||||
{
|
||||
AutofillSavePrompt = !(await _stateService.GetAutofillDisableSavePromptAsync()).GetValueOrDefault();
|
||||
var blacklistedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
AutofillBlacklistedUris = blacklistedUrisList != null ? string.Join(", ", blacklistedUrisList) : null;
|
||||
var blockedUrisList = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
AutofillBlockedUris = blockedUrisList != null ? string.Join(", ", blockedUrisList) : 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 UpdateAutofillBlacklistedUris()
|
||||
public async Task UpdateAutofillBlockedUris()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AutofillBlacklistedUris))
|
||||
if (string.IsNullOrWhiteSpace(AutofillBlockedUris))
|
||||
{
|
||||
await _stateService.SetAutofillBlacklistedUrisAsync(null);
|
||||
AutofillBlacklistedUris = null;
|
||||
AutofillBlockedUris = null;
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var csv = AutofillBlacklistedUris;
|
||||
var csv = AutofillBlockedUris;
|
||||
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);
|
||||
AutofillBlacklistedUris = string.Join(", ", urisList);
|
||||
AutofillBlockedUris = string.Join(", ", urisList);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Bit.App.Pages
|
||||
public bool UseFrame { get; set; }
|
||||
public Func<Task> ExecuteAsync { get; set; }
|
||||
|
||||
public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled;
|
||||
public bool SubLabelTextEnabled => SubLabel == AppResources.On;
|
||||
public string LineBreakMode => SubLabel == null ? "TailTruncation" : "";
|
||||
public bool ShowSubLabel => SubLabel.Length != 0;
|
||||
public bool ShowTimeInput => Time != null;
|
||||
|
||||
@@ -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,6 +42,7 @@ 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?>>
|
||||
@@ -83,6 +84,7 @@ 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;
|
||||
@@ -133,6 +135,7 @@ namespace Bit.App.Pages
|
||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||
!await _keyConnectorService.GetUsesKeyConnector();
|
||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
@@ -326,6 +329,38 @@ 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 =>
|
||||
@@ -450,7 +485,7 @@ namespace Bit.App.Pages
|
||||
autofillItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AutofillServices,
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled,
|
||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||
});
|
||||
}
|
||||
@@ -500,10 +535,16 @@ namespace Bit.App.Pages
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.UnlockWithPIN,
|
||||
SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled,
|
||||
SubLabel = _pin ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => UpdatePinAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.ApproveLoginRequests,
|
||||
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => ApproveLoginRequestsAsync()
|
||||
},
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.LockNow,
|
||||
ExecuteAsync = () => LockAsync()
|
||||
@@ -525,7 +566,7 @@ namespace Bit.App.Pages
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled,
|
||||
SubLabel = _biometric ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => UpdateBiometricAsync()
|
||||
};
|
||||
securityItems.Insert(2, item);
|
||||
@@ -554,7 +595,7 @@ namespace Bit.App.Pages
|
||||
securityItems.Add(new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.AllowScreenCapture,
|
||||
SubLabel = _screenCaptureAllowed ? AppResources.Enabled : AppResources.Disabled,
|
||||
SubLabel = _screenCaptureAllowed ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
|
||||
});
|
||||
}
|
||||
@@ -627,7 +668,7 @@ namespace Bit.App.Pages
|
||||
new SettingsPageListItem
|
||||
{
|
||||
Name = AppResources.SubmitCrashLogs,
|
||||
SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled,
|
||||
SubLabel = _reportLoggingEnabled ? AppResources.On : AppResources.Off,
|
||||
ExecuteAsync = () => LoggerReportingAsync()
|
||||
},
|
||||
#endif
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
|
||||
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitAsyncCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
|
||||
@@ -51,14 +51,6 @@ 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())
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
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;
|
||||
@@ -8,6 +10,7 @@ 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
|
||||
@@ -20,6 +23,7 @@ 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;
|
||||
@@ -35,8 +39,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -59,6 +65,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
public byte[] FileData { get; set; }
|
||||
public Command DeleteAttachmentCommand { get; set; }
|
||||
public ICommand SubmitAsyncCommand { get; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
@@ -125,6 +132,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
_logger.Exception(e);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
@@ -132,6 +140,12 @@ namespace Bit.App.Pages
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Exception(e);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,11 +137,11 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
||||
{
|
||||
var pageForOther = new AddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||
return;
|
||||
}
|
||||
var pageForLogin = new AddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
||||
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
||||
fromAutofill: true);
|
||||
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||
}
|
||||
|
||||
75
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
75
src/App/Pages/Vault/BaseCipherViewModel.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,21 @@
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AddEditPage"
|
||||
x:Class="Bit.App.Pages.CipherAddEditPage"
|
||||
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:AddEditPageViewModel"
|
||||
x:DataType="pages:CipherAddEditPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:AddEditPageViewModel />
|
||||
<pages:CipherAddEditPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
@@ -53,6 +55,25 @@
|
||||
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>
|
||||
|
||||
@@ -107,15 +128,26 @@
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row, box-row-input">
|
||||
<Label
|
||||
<Grid StyleClass="box-row, box-row-input"
|
||||
RowDefinitions="Auto,*"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<Label
|
||||
Text="{u:I18n Username}"
|
||||
StyleClass="box-label" />
|
||||
StyleClass="box-label"/>
|
||||
<Entry
|
||||
x:Name="_loginUsernameEntry"
|
||||
Text="{Binding Cipher.Login.Username}"
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
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>
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -636,101 +668,10 @@
|
||||
<Label Text="{u:I18n CustomFields, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<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>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
BindableLayout.ItemsSource="{Binding Fields}"
|
||||
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
|
||||
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
|
||||
Clicked="NewField_Clicked"></Button>
|
||||
</StackLayout>
|
||||
@@ -14,7 +14,7 @@ using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AddEditPage : BaseContentPage
|
||||
public partial class CipherAddEditPage : 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 AddEditPageViewModel _vm;
|
||||
private CipherAddEditPageViewModel _vm;
|
||||
private bool _fromAutofill;
|
||||
|
||||
public AddEditPage(
|
||||
public CipherAddEditPage(
|
||||
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,
|
||||
ViewPage viewPage = null)
|
||||
CipherDetailsPage cipherDetailsPage = 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 AddEditPageViewModel;
|
||||
_vm = BindingContext as CipherAddEditPageViewModel;
|
||||
_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.ViewPage = viewPage;
|
||||
_vm.CipherDetailsPage = cipherDetailsPage;
|
||||
_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 AddEditPageViewModel ViewModel => _vm;
|
||||
public CipherAddEditPageViewModel ViewModel => _vm;
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Lists.ItemViewModels.CustomFields;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
@@ -16,23 +16,19 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AddEditPageViewModel : BaseViewModel
|
||||
public class CipherAddEditPageViewModel : BaseCipherViewModel
|
||||
{
|
||||
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 ILogger _logger;
|
||||
private readonly ICustomFieldItemFactory _customFieldItemFactory;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
|
||||
private CipherView _cipher;
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
@@ -46,7 +42,7 @@ namespace Bit.App.Pages
|
||||
private bool _hasCollections;
|
||||
private string _previousCipherId;
|
||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||
private string[] _additionalCipherProperties = new string[]
|
||||
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||
{
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
@@ -57,6 +53,7 @@ namespace Bit.App.Pages
|
||||
nameof(ShowCollections),
|
||||
nameof(HasTotpValue)
|
||||
};
|
||||
|
||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||
new List<KeyValuePair<UriMatchType?, string>>
|
||||
{
|
||||
@@ -69,33 +66,30 @@ namespace Bit.App.Pages
|
||||
new KeyValuePair<UriMatchType?, string>(UriMatchType.Never, AppResources.Never)
|
||||
};
|
||||
|
||||
public AddEditPageViewModel()
|
||||
public CipherAddEditPageViewModel()
|
||||
{
|
||||
_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");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
|
||||
_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<AddEditPageFieldViewModel>(FieldOptions);
|
||||
FieldOptionsCommand = new Command<ICustomFieldItemViewModel>(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<AddEditPageFieldViewModel>();
|
||||
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
AllowPersonal = true;
|
||||
|
||||
@@ -151,11 +145,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; }
|
||||
@@ -170,7 +164,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<AddEditPageFieldViewModel> Fields { get; set; }
|
||||
public ExtendedObservableCollection<ICustomFieldItemViewModel> Fields { get; set; }
|
||||
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
||||
|
||||
public int TypeSelectedIndex
|
||||
@@ -239,11 +233,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties);
|
||||
}
|
||||
public bool ShowNotesSeparator
|
||||
{
|
||||
get => _showNotesSeparator;
|
||||
@@ -291,7 +280,7 @@ namespace Bit.App.Pages
|
||||
public bool ShowOwnershipOptions => !EditMode || CloneMode;
|
||||
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
|
||||
public bool CloneMode { get; set; }
|
||||
public ViewPage ViewPage { get; set; }
|
||||
public CipherDetailsPage CipherDetailsPage { get; set; }
|
||||
public bool IsLogin => Cipher?.Type == CipherType.Login;
|
||||
public bool IsIdentity => Cipher?.Type == CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == CipherType.Card;
|
||||
@@ -428,7 +417,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Cipher.Fields != null)
|
||||
{
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(Cipher, f)));
|
||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +505,7 @@ namespace Bit.App.Pages
|
||||
EditMode && !CloneMode ? AppResources.ItemUpdated : AppResources.NewItemCreated);
|
||||
_messagingService.Send(EditMode && !CloneMode ? "editedCipher" : "addedCipher", Cipher.Id);
|
||||
|
||||
if (Page is AddEditPage page && page.FromAutofillFramework)
|
||||
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
|
||||
{
|
||||
// Close and go back to app
|
||||
_deviceActionService.CloseAutofill();
|
||||
@@ -525,7 +514,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (CloneMode)
|
||||
{
|
||||
ViewPage?.UpdateCipherId(this.Cipher.Id);
|
||||
CipherDetailsPage?.UpdateCipherId(this.Cipher.Id);
|
||||
}
|
||||
// if the app is tombstoned then PopModalAsync would throw index out of bounds
|
||||
if (Page.Navigation?.ModalStack?.Count > 0)
|
||||
@@ -608,9 +597,35 @@ 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 AddEditPage).DoOnce())
|
||||
if (!(Page as CipherAddEditPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -646,9 +661,9 @@ namespace Bit.App.Pages
|
||||
Uris.Add(new LoginUriView());
|
||||
}
|
||||
|
||||
public async void FieldOptions(AddEditPageFieldViewModel field)
|
||||
public async void FieldOptions(ICustomFieldItemViewModel field)
|
||||
{
|
||||
if (!(Page as AddEditPage).DoOnce())
|
||||
if (!(Page as CipherAddEditPage).DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -708,15 +723,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (Fields == null)
|
||||
{
|
||||
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
|
||||
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
|
||||
}
|
||||
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
|
||||
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
|
||||
Fields.Add(_customFieldItemFactory.CreateCustomFieldItem(new FieldView
|
||||
{
|
||||
Type = type,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
NewField = true,
|
||||
}));
|
||||
}, true, Cipher, null, null, FieldOptionsCommand));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +793,7 @@ namespace Bit.App.Pages
|
||||
TriggerCipherChanged();
|
||||
|
||||
// Linked Custom Fields only apply to a specific item type
|
||||
foreach (var field in Fields.Where(f => f.IsLinkedType).ToList())
|
||||
foreach (var field in Fields.OfType<LinkedCustomFieldItemViewModel>().ToList())
|
||||
{
|
||||
Fields.Remove(field);
|
||||
}
|
||||
@@ -839,38 +854,14 @@ namespace Bit.App.Pages
|
||||
|
||||
private void TriggerCipherChanged()
|
||||
{
|
||||
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);
|
||||
}
|
||||
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -878,114 +869,11 @@ namespace Bit.App.Pages
|
||||
_logger.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[]
|
||||
private async void OnGenerateUsernameException(Exception ex)
|
||||
{
|
||||
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;
|
||||
}
|
||||
_logger.Exception(ex);
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,20 @@
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ViewPage"
|
||||
x:Class="Bit.App.Pages.CipherDetailsPage"
|
||||
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:ViewPageViewModel"
|
||||
x:DataType="pages:CipherDetailsPageViewModel"
|
||||
x:Name="_page"
|
||||
Title="{Binding PageTitle}">
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ViewPageViewModel />
|
||||
<pages:CipherDetailsPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
@@ -46,6 +48,25 @@
|
||||
<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">
|
||||
@@ -559,83 +580,10 @@
|
||||
<Label Text="{u:I18n CustomFields, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<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
|
||||
Spacing="0"
|
||||
BindableLayout.ItemsSource="{Binding Fields}"
|
||||
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
@@ -9,18 +9,18 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ViewPage : BaseContentPage
|
||||
public partial class CipherDetailsPage : BaseContentPage
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly ISyncService _syncService;
|
||||
private ViewPageViewModel _vm;
|
||||
private CipherDetailsPageViewModel _vm;
|
||||
|
||||
public ViewPage(string cipherId)
|
||||
public CipherDetailsPage(string cipherId)
|
||||
{
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_vm = BindingContext as ViewPageViewModel;
|
||||
_vm = BindingContext as CipherDetailsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.CipherId = cipherId;
|
||||
SetActivityIndicator(_mainContent);
|
||||
@@ -40,7 +40,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public ViewPageViewModel ViewModel => _vm;
|
||||
public CipherDetailsPageViewModel ViewModel => _vm;
|
||||
|
||||
public void UpdateCipherId(string cipherId)
|
||||
{
|
||||
@@ -55,7 +55,7 @@ namespace Bit.App.Pages
|
||||
IsBusy = true;
|
||||
}
|
||||
|
||||
_broadcasterService.Subscribe(nameof(ViewPage), async (message) =>
|
||||
_broadcasterService.Subscribe(nameof(CipherDetailsPage), async (message) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -111,8 +111,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_broadcasterService.Unsubscribe(nameof(ViewPage));
|
||||
_vm.StopCiphersTotpTick();
|
||||
_vm.StopCiphersTotpTick().FireAndForget();
|
||||
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
|
||||
}
|
||||
|
||||
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 AddEditPage(_vm.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_vm.CipherId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (selection == AppResources.Clone)
|
||||
{
|
||||
var page = new AddEditPage(_vm.CipherId, cloneMode: true, viewPage: this);
|
||||
var page = new CipherAddEditPage(_vm.CipherId, cloneMode: true, cipherDetailsPage: this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Lists.ItemViewModels.CustomFields;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
@@ -18,22 +19,20 @@ using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ViewPageViewModel : BaseViewModel
|
||||
public class CipherDetailsPageViewModel : BaseCipherViewModel, IPasswordPromptable
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly ILocalizeService _localizeService;
|
||||
private readonly ICustomFieldItemFactory _customFieldItemFactory;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CipherView _cipher;
|
||||
private List<ViewPageFieldViewModel> _fields;
|
||||
private List<ICustomFieldItemViewModel> _fields;
|
||||
private bool _canAccessPremium;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
@@ -51,19 +50,18 @@ namespace Bit.App.Pages
|
||||
private CancellationTokenSource _totpTickCancellationToken;
|
||||
private Task _totpTickTask;
|
||||
|
||||
public ViewPageViewModel()
|
||||
public CipherDetailsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
||||
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||
|
||||
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
@@ -72,8 +70,7 @@ namespace Bit.App.Pages
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
||||
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
||||
CheckPasswordCommand = new Command(CheckPasswordAsync);
|
||||
DownloadAttachmentCommand = new Command<AttachmentView>(DownloadAttachmentAsync);
|
||||
DownloadAttachmentCommand = new AsyncCommand<AttachmentView>(DownloadAttachmentAsync, allowsMultipleExecutions: false);
|
||||
|
||||
PageTitle = AppResources.ViewItem;
|
||||
}
|
||||
@@ -85,33 +82,27 @@ 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 DownloadAttachmentCommand { get; set; }
|
||||
public AsyncCommand<AttachmentView> DownloadAttachmentCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public CipherView Cipher
|
||||
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
nameof(IsCard),
|
||||
nameof(IsSecureNote),
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowTotp),
|
||||
nameof(ColoredPassword),
|
||||
nameof(UpdatedText),
|
||||
nameof(PasswordUpdatedText),
|
||||
nameof(PasswordHistoryText),
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
nameof(ShowUpgradePremiumTotpText)
|
||||
});
|
||||
}
|
||||
public List<ViewPageFieldViewModel> Fields
|
||||
nameof(IsLogin),
|
||||
nameof(IsIdentity),
|
||||
nameof(IsCard),
|
||||
nameof(IsSecureNote),
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowTotp),
|
||||
nameof(ColoredPassword),
|
||||
nameof(UpdatedText),
|
||||
nameof(PasswordUpdatedText),
|
||||
nameof(PasswordHistoryText),
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
nameof(ShowUpgradePremiumTotpText)
|
||||
};
|
||||
public List<ICustomFieldItemViewModel> Fields
|
||||
{
|
||||
get => _fields;
|
||||
set => SetProperty(ref _fields, value);
|
||||
@@ -153,7 +144,7 @@ namespace Bit.App.Pages
|
||||
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
|
||||
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
|
||||
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
|
||||
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password);
|
||||
public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
|
||||
public FormattedString UpdatedText
|
||||
{
|
||||
get
|
||||
@@ -211,7 +202,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && ShowTotp;
|
||||
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && !Cipher.OrganizationUseTotp && ShowTotp;
|
||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||
public bool ShowIdentityAddress => IsIdentity && (
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
||||
@@ -225,7 +216,7 @@ namespace Bit.App.Pages
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
get => _canAccessPremium ? _totpCodeFormatted : string.Empty;
|
||||
get => ShowUpgradePremiumTotpText ? string.Empty : _totpCodeFormatted;
|
||||
set => SetProperty(ref _totpCodeFormatted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
@@ -264,7 +255,10 @@ namespace Bit.App.Pages
|
||||
}
|
||||
Cipher = await cipher.DecryptAsync();
|
||||
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
Fields = Cipher.Fields?.Select(f => new ViewPageFieldViewModel(this, Cipher, f)).ToList();
|
||||
|
||||
Fields = Cipher.Fields?
|
||||
.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, false, Cipher, this, CopyFieldCommand, null))
|
||||
.ToList();
|
||||
|
||||
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
||||
@@ -431,86 +425,93 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
private async void CheckPasswordAsync()
|
||||
private async Task TotpUpdateCodeAsync()
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
if (Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
|
||||
{
|
||||
_totpInterval = null;
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Cipher.Login?.Password))
|
||||
_totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
|
||||
if (_totpCode != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
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")));
|
||||
if (_totpCode.Length > 4)
|
||||
{
|
||||
var half = (int)Math.Floor(_totpCode.Length / 2M);
|
||||
TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half),
|
||||
_totpCode.Substring(half));
|
||||
}
|
||||
else
|
||||
{
|
||||
TotpCodeFormatted = _totpCode;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
|
||||
TotpCodeFormatted = null;
|
||||
_totpInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void DownloadAttachmentAsync(AttachmentView attachment)
|
||||
private async Task TotpTickAsync(int intervalSeconds)
|
||||
{
|
||||
if (!(Page as BaseContentPage).DoOnce())
|
||||
var epoc = CoreHelpers.EpocUtcNow() / 1000;
|
||||
var mod = epoc % intervalSeconds;
|
||||
var totpSec = intervalSeconds - mod;
|
||||
TotpSec = totpSec.ToString();
|
||||
TotpLow = totpSec < 7;
|
||||
if (mod == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
if (attachment.FileSize >= 10485760) // 10 MB
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await TotpUpdateCodeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenFile = true;
|
||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
||||
// for any reason we want to be sure to catch it here.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
|
||||
canOpenFile = false;
|
||||
}
|
||||
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
private async Task DownloadAttachmentAsync(AttachmentView attachment)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
if (attachment.FileSize >= 10485760) // 10 MB
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No);
|
||||
if (!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var canOpenFile = true;
|
||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
||||
// for any reason we want to be sure to catch it here.
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
|
||||
canOpenFile = false;
|
||||
}
|
||||
|
||||
if (!await PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (data == null)
|
||||
@@ -537,9 +538,11 @@ namespace Bit.App.Pages
|
||||
OpenAttachment(data, attachment);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +671,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<bool> PromptPasswordAsync()
|
||||
public async Task<bool> PromptPasswordAsync()
|
||||
{
|
||||
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
|
||||
{
|
||||
@@ -678,96 +681,4 @@ namespace Bit.App.Pages
|
||||
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ViewPageFieldViewModel : ExtendedViewModel
|
||||
{
|
||||
private II18nService _i18nService;
|
||||
private ViewPageViewModel _vm;
|
||||
private FieldView _field;
|
||||
private CipherView _cipher;
|
||||
private bool _showHiddenValue;
|
||||
|
||||
public ViewPageFieldViewModel(ViewPageViewModel vm, CipherView cipher, FieldView field)
|
||||
{
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
_vm = vm;
|
||||
_cipher = cipher;
|
||||
Field = field;
|
||||
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
|
||||
}
|
||||
|
||||
public FieldView Field
|
||||
{
|
||||
get => _field;
|
||||
set => SetProperty(ref _field, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ValueText),
|
||||
nameof(IsBooleanType),
|
||||
nameof(IsHiddenType),
|
||||
nameof(IsTextType),
|
||||
nameof(ShowCopyButton),
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShowHiddenValue
|
||||
{
|
||||
get => _showHiddenValue;
|
||||
set => SetProperty(ref _showHiddenValue, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ShowHiddenValueIcon)
|
||||
});
|
||||
}
|
||||
|
||||
public string ValueText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsBooleanType)
|
||||
{
|
||||
return _field.Value == "true" ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
|
||||
}
|
||||
else if (IsLinkedType)
|
||||
{
|
||||
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
|
||||
return BitwardenIcons.Link + _i18nService.T(i18nKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _field.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
|
||||
|
||||
public Command ToggleHiddenValueCommand { get; set; }
|
||||
|
||||
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
|
||||
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
|
||||
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
|
||||
public bool IsLinkedType => _field.Type == Core.Enums.FieldType.Linked;
|
||||
public bool ShowViewHidden => IsHiddenType && _cipher.ViewPassword;
|
||||
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
|
||||
!string.IsNullOrWhiteSpace(_field.Value) &&
|
||||
!(IsHiddenType && !_cipher.ViewPassword) &&
|
||||
_field.Type != FieldType.Linked;
|
||||
|
||||
public async void ToggleHiddenValue()
|
||||
{
|
||||
if (!await _vm.PromptPasswordAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ShowHiddenValue = !ShowHiddenValue;
|
||||
if (ShowHiddenValue)
|
||||
{
|
||||
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
var task = eventService.CollectAsync(
|
||||
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -157,7 +156,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
var page = new CipherDetailsPage(cipher.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
else if (selection == AppResources.Autofill || selection == AppResources.AutofillAndSave)
|
||||
|
||||
@@ -140,30 +140,6 @@
|
||||
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"
|
||||
|
||||
@@ -22,6 +22,7 @@ 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;
|
||||
|
||||
@@ -29,7 +30,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)
|
||||
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
@@ -41,6 +42,7 @@ 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;
|
||||
@@ -48,6 +50,7 @@ namespace Bit.App.Pages
|
||||
_vm.FolderId = folderId;
|
||||
_vm.CollectionId = collectionId;
|
||||
_vm.Deleted = deleted;
|
||||
_vm.ShowTotp = showTotp;
|
||||
_previousPage = previousPage;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
@@ -69,7 +72,7 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Add(_lockItem);
|
||||
ToolbarItems.Add(_exitItem);
|
||||
}
|
||||
if (deleted)
|
||||
if (deleted || showTotp)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Remove(_addItem);
|
||||
@@ -193,7 +196,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_vm.StopCiphersTotpTick();
|
||||
_vm.StopCiphersTotpTick().FireAndForget();
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
@@ -201,42 +204,54 @@ namespace Bit.App.Pages
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
((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 GroupingsPageTOTPListItem totpItem)
|
||||
{
|
||||
await _vm.SelectCipherAsync(totpItem.Cipher);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.IsTrash)
|
||||
{
|
||||
await _vm.SelectTrashAsync();
|
||||
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);
|
||||
}
|
||||
}
|
||||
else if (item.Cipher != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
_platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +294,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (!_vm.Deleted && DoOnce())
|
||||
{
|
||||
var page = new AddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||
var page = new CipherAddEditPage(null, _vm.Type, _vm.FolderId, _vm.CollectionId, _vm.GetVaultFilterOrgId());
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
@@ -293,11 +308,11 @@ namespace Bit.App.Pages
|
||||
await _accountListOverlay.HideAsync();
|
||||
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(_previousPage.CipherId)));
|
||||
}
|
||||
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.CipherId))
|
||||
{
|
||||
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.CipherId)));
|
||||
await Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(_previousPage.CipherId)));
|
||||
}
|
||||
_previousPage = null;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
{
|
||||
@@ -38,6 +39,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_name = Collection.Name;
|
||||
}
|
||||
else if (IsTotpCode)
|
||||
{
|
||||
_name = AppResources.VerificationCodes;
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
@@ -82,6 +87,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_icon = BitwardenIcons.Collection;
|
||||
}
|
||||
else if (IsTotpCode)
|
||||
{
|
||||
_icon = BitwardenIcons.Clock;
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task CopyToClipboardAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(TotpCodeFormatted);
|
||||
await _clipboardService.CopyTextAsync(TotpCodeFormatted?.Replace(" ", string.Empty));
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
|
||||
}
|
||||
|
||||
|
||||
@@ -82,9 +82,6 @@ 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)
|
||||
{
|
||||
@@ -101,13 +98,11 @@ 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; }
|
||||
@@ -165,21 +160,15 @@ namespace Bit.App.Pages
|
||||
get => _websiteIconsEnabled;
|
||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||
}
|
||||
public bool ShowTotpFilter
|
||||
public bool ShowTotp
|
||||
{
|
||||
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()
|
||||
@@ -211,14 +200,12 @@ 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;
|
||||
@@ -243,6 +230,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (MainPage)
|
||||
{
|
||||
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
|
||||
|
||||
groupedItems.Add(new GroupingsPageListGroup(
|
||||
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
|
||||
{
|
||||
@@ -301,6 +290,11 @@ namespace Bit.App.Pages
|
||||
{
|
||||
CreateCipherGroupedItems(groupedItems);
|
||||
}
|
||||
if (ShowTotp && (!TOTPCiphers?.Any() ?? false))
|
||||
{
|
||||
Page.Navigation.PopAsync();
|
||||
return;
|
||||
}
|
||||
if (ShowNoFolderCipherGroup)
|
||||
{
|
||||
var noFolderCiphersListItems = NoFolderCiphers.Select(
|
||||
@@ -387,14 +381,30 @@ 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 (TotpFilterEnable)
|
||||
if (ShowTotp)
|
||||
{
|
||||
var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted && !string.IsNullOrEmpty(c.Login.Totp))
|
||||
.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
|
||||
var ciphersListItems = TOTPCiphers.Select(c => new GroupingsPageTOTPListItem(c, true)).ToList();
|
||||
groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items,
|
||||
ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any()));
|
||||
|
||||
@@ -438,7 +448,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SelectCipherAsync(CipherView cipher)
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
var page = new CipherDetailsPage(cipher.Id);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
@@ -485,6 +495,13 @@ 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,
|
||||
@@ -519,9 +536,11 @@ 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();
|
||||
@@ -547,6 +566,10 @@ 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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user