mirror of
https://github.com/bitwarden/mobile
synced 2025-12-11 13:53:29 +00:00
Compare commits
57 Commits
sg-804
...
feature/to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcc260183e | ||
|
|
2f2b90acee | ||
|
|
1b82249d44 | ||
|
|
f432a58e9f | ||
|
|
7e6935f21c | ||
|
|
7cf34b845e | ||
|
|
8ed909eb91 | ||
|
|
a57dc50a50 | ||
|
|
9074f533f9 | ||
|
|
9499fa0fb3 | ||
|
|
69e47b651d | ||
|
|
dbe72c68a8 | ||
|
|
3822373b98 | ||
|
|
a51c2efdae | ||
|
|
0ce01ddd84 | ||
|
|
6b33266721 | ||
|
|
6f61f1f4d8 | ||
|
|
d3d935fff6 | ||
|
|
a97abc61d5 | ||
|
|
6e30ec2dc7 | ||
|
|
472f251714 | ||
|
|
a6069ef37b | ||
|
|
6aeec4a89a | ||
|
|
9ddbe400f8 | ||
|
|
9bc8e6912d | ||
|
|
64beb23239 | ||
|
|
94f2c5a4f8 | ||
|
|
3522953b8c | ||
|
|
3a91bc7eb1 | ||
|
|
277dd5942f | ||
|
|
46c73dafbe | ||
|
|
c564a34317 | ||
|
|
e606ff88a4 | ||
|
|
67fbd55ae9 | ||
|
|
c67229d235 | ||
|
|
e8d31b8a22 | ||
|
|
3b763e454c | ||
|
|
c9c3e6d98d | ||
|
|
751fdf2db6 | ||
|
|
6ae7d6dd8d | ||
|
|
a3218aed26 | ||
|
|
77c8156515 | ||
|
|
1c0375ef05 | ||
|
|
b3f2730c71 | ||
|
|
5e2142fba7 | ||
|
|
c91277bc43 | ||
|
|
a484ca633c | ||
|
|
31d1a2e083 | ||
|
|
655b51b6a5 | ||
|
|
0b626cedc7 | ||
|
|
3ac2580742 | ||
|
|
008ed8eb56 | ||
|
|
26e0e43bb4 | ||
|
|
0ad992faec | ||
|
|
98dd8298ea | ||
|
|
bb37bac620 | ||
|
|
b02c58e362 |
@@ -7,12 +7,6 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-format"
|
"dotnet-format"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"cake.tool": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"commands": [
|
|
||||||
"dotnet-cake"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Before you submit
|
## Before you submit
|
||||||
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
|
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
|
||||||
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
|
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
||||||
- If this change requires a **documentation update** - notify the documentation team
|
- [ ] This change requires a **documentation update** (notify the documentation team)
|
||||||
- If this change has particular **deployment requirements** - notify the DevOps team
|
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
||||||
|
|||||||
22
.github/renovate.json
vendored
22
.github/renovate.json
vendored
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:base",
|
|
||||||
"schedule:monthly",
|
|
||||||
":maintainLockFilesMonthly",
|
|
||||||
":preserveSemverRanges",
|
|
||||||
":rebaseStalePrs",
|
|
||||||
":disableDependencyDashboard"
|
|
||||||
],
|
|
||||||
"enabledManagers": [
|
|
||||||
"nuget"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchManagers": ["nuget"],
|
|
||||||
"groupName": "Nuget updates",
|
|
||||||
"groupSlug": "nuget",
|
|
||||||
"separateMajorMinor": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
160
.github/workflows/build.yml
vendored
160
.github/workflows/build.yml
vendored
@@ -57,12 +57,8 @@ jobs:
|
|||||||
|
|
||||||
android:
|
android:
|
||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2022
|
runs-on: windows-2019
|
||||||
needs: setup
|
needs: setup
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
variant: ['prod', 'qa']
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
@@ -71,26 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
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
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -101,8 +78,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
@@ -113,17 +89,12 @@ jobs:
|
|||||||
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
||||||
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
|
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Decrypt secrets - Google Services
|
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
|
||||||
env:
|
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
|
||||||
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
|
||||||
shell: bash
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||||
@@ -151,35 +122,26 @@ jobs:
|
|||||||
run: dotnet test test/Core.Test/Core.Test.csproj
|
run: dotnet test test/Core.Test/Core.Test.csproj
|
||||||
|
|
||||||
- name: Build Play Store publisher
|
- name: Build Play Store publisher
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
|
||||||
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
||||||
|
|
||||||
- name: Setup Android build (${{ matrix.variant }})
|
- name: Build for Play Store
|
||||||
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
|
||||||
|
|
||||||
- name: Build Android
|
|
||||||
run: |
|
run: |
|
||||||
$configuration = "Release";
|
$configuration = "Release";
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Build $configuration Configuration"
|
Write-Output "##### Build $configuration Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
|
||||||
|
|
||||||
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Sign Android Build
|
- name: Sign for Play Store
|
||||||
env:
|
env:
|
||||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
||||||
$packageName = "com.x8bit.bitwarden";
|
|
||||||
|
|
||||||
if ("${{ matrix.variant }}" -ne "prod")
|
|
||||||
{
|
|
||||||
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
|
||||||
}
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -193,8 +155,9 @@ jobs:
|
|||||||
Write-Output "##### Copy Google Play Bundle to project root"
|
Write-Output "##### Copy Google Play Bundle to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab");
|
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
|
||||||
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
|
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab");
|
||||||
|
|
||||||
Copy-Item $signedAabPath $signedAabDestPath
|
Copy-Item $signedAabPath $signedAabDestPath
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -210,41 +173,33 @@ jobs:
|
|||||||
Write-Output "##### Copy Release APK to project root"
|
Write-Output "##### Copy Release APK to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk");
|
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
|
||||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk");
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: Upload Prod .aab artifact
|
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
- name: Upload Play Store .aab artifact
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Prod .apk artifact
|
- name: Upload Play Store .apk artifact
|
||||||
if: ${{ matrix.variant == 'prod' }}
|
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Other .apk artifact
|
|
||||||
if: ${{ matrix.variant != 'prod' }}
|
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
|
||||||
with:
|
|
||||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
|
||||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Deploy to Play Store
|
- name: Deploy to Play Store
|
||||||
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
|
if: |
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
(github.ref == 'refs/heads/master'
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|
|| github.ref == 'refs/heads/hotfix-rc'
|
||||||
run: |
|
run: |
|
||||||
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
||||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
CREDS_PATH="$HOME/secrets/play_creds.json"
|
||||||
@@ -257,7 +212,7 @@ jobs:
|
|||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Build
|
name: F-Droid Build
|
||||||
runs-on: windows-2022
|
runs-on: windows-2019
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
@@ -267,26 +222,6 @@ jobs:
|
|||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
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
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -466,17 +401,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
env:
|
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||||
KEYVAULT: bitwarden-prod-kv
|
with:
|
||||||
SECRETS: |
|
keyvault: "bitwarden-prod-kv"
|
||||||
appcenter-ios-token
|
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
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
@@ -667,17 +595,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
env:
|
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||||
KEYVAULT: bitwarden-prod-kv
|
with:
|
||||||
SECRETS: |
|
keyvault: "bitwarden-prod-kv"
|
||||||
crowdin-api-token
|
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
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
||||||
@@ -734,18 +655,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
|
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
with:
|
||||||
KEYVAULT: bitwarden-prod-kv
|
keyvault: "bitwarden-prod-kv"
|
||||||
SECRETS: |
|
secrets: "devops-alerts-slack-webhook-url"
|
||||||
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
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
|
|||||||
14
.github/workflows/crowdin-pull.yml
vendored
14
.github/workflows/crowdin-pull.yml
vendored
@@ -24,13 +24,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-prod-kv"
|
keyvault: "bitwarden-prod-kv"
|
||||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
secrets: "crowdin-api-token"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
@@ -40,12 +40,10 @@ jobs:
|
|||||||
upload_sources: false
|
upload_sources: false
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
github_user_name: "bitwarden-devops-bot"
|
github_user_name: "github-actions"
|
||||||
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
github_user_email: "<>"
|
||||||
commit_message: "Autosync the updated translations"
|
commit_message: "Autosync the updated translations"
|
||||||
localization_branch_name: crowdin-auto-sync
|
localization_branch_name: crowdin-auto-sync
|
||||||
create_pull_request: true
|
create_pull_request: true
|
||||||
pull_request_title: "Autosync Crowdin Translations"
|
pull_request_title: "Autosync Crowdin Translations"
|
||||||
pull_request_body: "Autosync the updated translations"
|
pull_request_body: "Autosync the updated translations"
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
|
||||||
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
|
||||||
|
|||||||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@@ -53,38 +53,18 @@ jobs:
|
|||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
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
|
- name: Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ steps.branch.outputs.branch-name }}
|
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
|
- name: Prep Bitwarden iOS release asset
|
||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||||
|
|
||||||
- name: Create release
|
- 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
|
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
||||||
with:
|
with:
|
||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
@@ -98,22 +78,6 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
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:
|
f-droid:
|
||||||
name: F-Droid Release
|
name: F-Droid Release
|
||||||
@@ -125,7 +89,6 @@ jobs:
|
|||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
@@ -133,15 +96,6 @@ jobs:
|
|||||||
branch: ${{ needs.release.outputs.branch-name }}
|
branch: ${{ needs.release.outputs.branch-name }}
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
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
|
- name: Set up Node
|
||||||
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
||||||
with:
|
with:
|
||||||
@@ -207,5 +161,5 @@ jobs:
|
|||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
- name: Deploy to gh-pages
|
- 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
|
run: npm run deploy
|
||||||
|
|||||||
63
.github/workflows/version-auto-bump.yml
vendored
63
.github/workflows/version-auto-bump.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
name: Version Auto Bump
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- v**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
setup:
|
|
||||||
name: "Setup"
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
outputs:
|
|
||||||
version_number: ${{ steps.version.outputs.new-version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout Branch
|
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
|
||||||
|
|
||||||
- name: Calculate bumped version
|
|
||||||
id: version
|
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ github.ref }}
|
|
||||||
run: |
|
|
||||||
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
|
|
||||||
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
|
|
||||||
echo "Current Major: $CURR_MAJOR"
|
|
||||||
echo "Current Patch: $CURR_PATCH"
|
|
||||||
|
|
||||||
NEW_PATCH=$((CURR_PATCH+1))
|
|
||||||
NEW_VER=$CURR_MAJOR.$NEW_PATCH
|
|
||||||
echo "New Version: $NEW_VER"
|
|
||||||
echo "::set-output name=new-version::$NEW_VER"
|
|
||||||
|
|
||||||
trigger_version_bump:
|
|
||||||
name: "Trigger version bump workflow"
|
|
||||||
runs-on: ubuntu-22.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
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-prod-kv"
|
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- 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
|
|
||||||
24
.github/workflows/version-bump.yml
vendored
24
.github/workflows/version-bump.yml
vendored
@@ -16,26 +16,6 @@ jobs:
|
|||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
- name: Login to Azure - Prod Subscription
|
|
||||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
|
||||||
with:
|
|
||||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-prod-kv"
|
|
||||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
|
||||||
|
|
||||||
- name: Import GPG key
|
|
||||||
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
|
|
||||||
with:
|
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
|
||||||
git_user_signingkey: true
|
|
||||||
git_commit_gpgsign: true
|
|
||||||
|
|
||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
run: |
|
run: |
|
||||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
@@ -72,8 +52,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup git
|
- name: Setup git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
git config --local user.name "github-actions[bot]"
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -208,5 +208,4 @@ FakesAssemblies/
|
|||||||
# Other
|
# Other
|
||||||
project.lock.json
|
project.lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
src/App/Css
|
src/App/Css
|
||||||
tools
|
|
||||||
345
build.cake
345
build.cake
@@ -1,345 +0,0 @@
|
|||||||
#addin nuget:?package=Cake.FileHelpers&version=5.0.0
|
|
||||||
#addin nuget:?package=Cake.AndroidAppManifest&version=1.1.2
|
|
||||||
#addin nuget:?package=Cake.Plist&version=0.7.0
|
|
||||||
#addin nuget:?package=Cake.Incubator&version=7.0.0
|
|
||||||
#tool dotnet:?package=GitVersion.Tool&version=5.10.3
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
var debugScript = Argument<bool>("debugScript", false);
|
|
||||||
var target = Argument("target", "Default");
|
|
||||||
var configuration = Argument("configuration", "Release");
|
|
||||||
var variant = Argument("variant", "dev");
|
|
||||||
|
|
||||||
abstract record VariantConfig(
|
|
||||||
string AppName,
|
|
||||||
string AndroidPackageName,
|
|
||||||
string iOSBundleId,
|
|
||||||
string ApsEnvironment
|
|
||||||
);
|
|
||||||
|
|
||||||
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
|
|
||||||
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
|
|
||||||
|
|
||||||
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development");
|
|
||||||
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development");
|
|
||||||
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production");
|
|
||||||
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production");
|
|
||||||
|
|
||||||
VariantConfig GetVariant() => variant.ToLower() switch{
|
|
||||||
"qa" => new QA(),
|
|
||||||
"beta" => new Beta(),
|
|
||||||
"prod" => new Prod(),
|
|
||||||
_ => new Dev()
|
|
||||||
};
|
|
||||||
|
|
||||||
GitVersion _gitVersion; //will be set by GetGitInfo task
|
|
||||||
var _slnPath = Path.Combine(""); //base path used to access files. If build.cake file is moved, just update this
|
|
||||||
string _androidPackageName = string.Empty; //will be set by UpdateAndroidManifest task
|
|
||||||
string CreateFeatureBranch(string prevVersionName, GitVersion git) => $"{prevVersionName}-{git.BranchName.Replace("/","-")}";
|
|
||||||
string GetVersionName(string prevVersionName, VariantConfig buildVariant, GitVersion git) => buildVariant is Prod? prevVersionName : CreateFeatureBranch(prevVersionName, git);
|
|
||||||
int CreateBuildNumber(int previousNumber) => ++previousNumber;
|
|
||||||
|
|
||||||
Task("GetGitInfo")
|
|
||||||
.Does(()=> {
|
|
||||||
_gitVersion = GitVersion(new GitVersionSettings());
|
|
||||||
|
|
||||||
if(debugScript)
|
|
||||||
{
|
|
||||||
Information($"GitVersion Dump:\n{_gitVersion.Dump()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Information("Git data Load successfully.");
|
|
||||||
});
|
|
||||||
|
|
||||||
#region Android
|
|
||||||
Task("UpdateAndroidAppIcon")
|
|
||||||
.Does(()=>{
|
|
||||||
//TODO we'll implement variant icons later
|
|
||||||
//manifest.ApplicationIcon = "@mipmap/ic_launcher";
|
|
||||||
Information($"Updated Androix App Icon with success");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Task("UpdateAndroidManifest")
|
|
||||||
.IsDependentOn("GetGitInfo")
|
|
||||||
.Does(()=>
|
|
||||||
{
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var manifestPath = Path.Combine(_slnPath, "src", "Android", "Properties", "AndroidManifest.xml");
|
|
||||||
|
|
||||||
// Cake.AndroidAppManifest doesn't currently enable us to access nested items so, quick (not ideal) fix:
|
|
||||||
var manifestText = FileReadText(manifestPath);
|
|
||||||
manifestText = manifestText.Replace("com.x8bit.bitwarden.", buildVariant.AndroidPackageName + ".");
|
|
||||||
manifestText = manifestText.Replace("android:label=\"Bitwarden\"", $"android:label=\"{buildVariant.AppName}\"");
|
|
||||||
FileWriteText(manifestPath, manifestText);
|
|
||||||
|
|
||||||
var manifest = DeserializeAppManifest(manifestPath);
|
|
||||||
|
|
||||||
var prevVersionCode = manifest.VersionCode;
|
|
||||||
var prevVersionName = manifest.VersionName;
|
|
||||||
_androidPackageName = manifest.PackageName;
|
|
||||||
|
|
||||||
//manifest.VersionCode = CreateBuildNumber(prevVersionCode);
|
|
||||||
manifest.VersionName = GetVersionName(prevVersionName, buildVariant, _gitVersion);
|
|
||||||
manifest.PackageName = buildVariant.AndroidPackageName;
|
|
||||||
manifest.ApplicationLabel = buildVariant.AppName;
|
|
||||||
|
|
||||||
//Information($"AndroidManigest.xml VersionCode from {prevVersionCode} to {manifest.VersionCode}");
|
|
||||||
Information($"AndroidManigest.xml VersionName from {prevVersionName} to {manifest.VersionName}");
|
|
||||||
Information($"AndroidManigest.xml PackageName from {_androidPackageName} to {buildVariant.AndroidPackageName}");
|
|
||||||
Information($"AndroidManigest.xml ApplicationLabel to {buildVariant.AppName}");
|
|
||||||
|
|
||||||
SerializeAppManifest(manifestPath, manifest);
|
|
||||||
|
|
||||||
Information("AndroidManifest updated with success!");
|
|
||||||
});
|
|
||||||
|
|
||||||
void ReplaceInFile(string filePath, string oldtext, string newtext)
|
|
||||||
{
|
|
||||||
var fileText = FileReadText(filePath);
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(fileText) || !fileText.Contains(oldtext))
|
|
||||||
{
|
|
||||||
throw new Exception($"Couldn't find {filePath} or it didn't contain: {oldtext}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fileText = fileText.Replace(oldtext, newtext);
|
|
||||||
|
|
||||||
FileWriteText(filePath, fileText);
|
|
||||||
Information($"{filePath} modified successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Task("UpdateAndroidCodeFiles")
|
|
||||||
.IsDependentOn("UpdateAndroidManifest")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
|
|
||||||
//We're not using _androidPackageName here because the codefile is currently slightly different string than the one in AndroidManifest.xml
|
|
||||||
var keyName = "com.8bit.bitwarden";
|
|
||||||
var fixedPackageName = buildVariant.AndroidPackageName.Replace("x8bit", "8bit");
|
|
||||||
var filePath = Path.Combine(_slnPath, "src", "Android", "Services", "BiometricService.cs");
|
|
||||||
ReplaceInFile(filePath, keyName, fixedPackageName);
|
|
||||||
|
|
||||||
var packageFileList = new string[] {
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "MainActivity.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "MainApplication.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Constants.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Accessibility", "AccessibilityService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillHelpers.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Receivers", "ClearClipboardAlarmReceiver.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Receivers", "EventUploadReceiver.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Receivers", "PackageReplacedReceiver.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Receivers", "RestrictionsChangedReceiver.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Services", "DeviceActionService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Tiles", "AutofillTileService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Tiles", "GeneratorTileService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Tiles", "MyVaultTileService.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "google-services.json"),
|
|
||||||
Path.Combine(_slnPath, "store", "google", "Publisher", "Program.cs"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach(string path in packageFileList)
|
|
||||||
{
|
|
||||||
ReplaceInFile(path, "com.x8bit.bitwarden", buildVariant.AndroidPackageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var labelFileList = new string[] {
|
|
||||||
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach(string path in labelFileList)
|
|
||||||
{
|
|
||||||
ReplaceInFile(path, "Bitwarden\"", $"{buildVariant.AppName}\"");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#endregion Android
|
|
||||||
|
|
||||||
#region iOS
|
|
||||||
enum iOSProjectType
|
|
||||||
{
|
|
||||||
Null,
|
|
||||||
MainApp,
|
|
||||||
Autofill,
|
|
||||||
Extension,
|
|
||||||
ShareExtension
|
|
||||||
}
|
|
||||||
|
|
||||||
string GetiOSBundleId(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
|
|
||||||
{
|
|
||||||
iOSProjectType.Autofill => $"{buildVariant.iOSBundleId}.autofill",
|
|
||||||
iOSProjectType.Extension => $"{buildVariant.iOSBundleId}.find-login-action-extension",
|
|
||||||
iOSProjectType.ShareExtension => $"{buildVariant.iOSBundleId}.share-extension",
|
|
||||||
_ => buildVariant.iOSBundleId
|
|
||||||
};
|
|
||||||
|
|
||||||
string GetiOSBundleName(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
|
|
||||||
{
|
|
||||||
iOSProjectType.Autofill => $"{buildVariant.AppName} Autofill",
|
|
||||||
iOSProjectType.Extension => $"{buildVariant.AppName} Extension",
|
|
||||||
iOSProjectType.ShareExtension => $"{buildVariant.AppName} Share Extension",
|
|
||||||
_ => buildVariant.AppName
|
|
||||||
};
|
|
||||||
|
|
||||||
private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, GitVersion git, iOSProjectType projectType = iOSProjectType.MainApp)
|
|
||||||
{
|
|
||||||
var plistFile = File(plistPath);
|
|
||||||
dynamic plist = DeserializePlist(plistFile);
|
|
||||||
|
|
||||||
var prevVersionName = plist["CFBundleShortVersionString"];
|
|
||||||
var prevVersionString = plist["CFBundleVersion"];
|
|
||||||
var prevVersion = int.Parse(plist["CFBundleVersion"]);
|
|
||||||
var prevBundleId = plist["CFBundleIdentifier"];
|
|
||||||
var prevBundleName = plist["CFBundleName"];
|
|
||||||
//var newVersion = CreateBuildNumber(prevVersion).ToString();
|
|
||||||
var newVersionName = GetVersionName(prevVersionName, buildVariant, git);
|
|
||||||
var newBundleId = GetiOSBundleId(buildVariant, projectType);
|
|
||||||
var newBundleName = GetiOSBundleName(buildVariant, projectType);
|
|
||||||
|
|
||||||
plist["CFBundleName"] = newBundleName;
|
|
||||||
plist["CFBundleDisplayName"] = newBundleName;
|
|
||||||
//plist["CFBundleVersion"] = newVersion;
|
|
||||||
plist["CFBundleShortVersionString"] = newVersionName;
|
|
||||||
plist["CFBundleIdentifier"] = newBundleId;
|
|
||||||
|
|
||||||
if(projectType == iOSProjectType.MainApp)
|
|
||||||
{
|
|
||||||
plist["CFBundleURLTypes"][0]["CFBundleURLName"] = $"{buildVariant.iOSBundleId}.url";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(projectType == iOSProjectType.Extension)
|
|
||||||
{
|
|
||||||
var keyText = plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"];
|
|
||||||
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
SerializePlist(plistFile, plist);
|
|
||||||
|
|
||||||
Information($"Changed app name from {prevBundleName} to {newBundleName}");
|
|
||||||
//Information($"Changed Bundle Version from {prevVersion} to {newVersion}");
|
|
||||||
Information($"Changed Bundle Short Version name from {prevVersionName} to {newVersionName}");
|
|
||||||
Information($"Changed Bundle Identifier from {prevBundleId} to {newBundleId}");
|
|
||||||
Information($"{plistPath} updated with success!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant)
|
|
||||||
{
|
|
||||||
var EntitlementlistFile = File(entitlementsPath);
|
|
||||||
dynamic Entitlements = DeserializePlist(EntitlementlistFile);
|
|
||||||
|
|
||||||
Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
|
|
||||||
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
|
|
||||||
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
|
|
||||||
|
|
||||||
Information($"Changed ApsEnvironment name to {buildVariant.ApsEnvironment}");
|
|
||||||
Information($"Changed keychain-access-groups bundleID to {buildVariant.iOSBundleId}");
|
|
||||||
|
|
||||||
SerializePlist(EntitlementlistFile, Entitlements);
|
|
||||||
|
|
||||||
Information($"{entitlementsPath} updated with success!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Task("UpdateiOSIcon")
|
|
||||||
.Does(()=>{
|
|
||||||
//TODO we'll implement variant icons later
|
|
||||||
Information($"Updating IOS App Icon");
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("UpdateiOSPlist")
|
|
||||||
.IsDependentOn("GetGitInfo")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS", "Info.plist");
|
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS", "Entitlements.plist");
|
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
|
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("UpdateiOSAutofillPlist")
|
|
||||||
.IsDependentOn("GetGitInfo")
|
|
||||||
.IsDependentOn("UpdateiOSPlist")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
|
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
|
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
|
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("UpdateiOSExtensionPlist")
|
|
||||||
.IsDependentOn("GetGitInfo")
|
|
||||||
.IsDependentOn("UpdateiOSPlist")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
|
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
|
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
|
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("UpdateiOSShareExtensionPlist")
|
|
||||||
.IsDependentOn("GetGitInfo")
|
|
||||||
.IsDependentOn("UpdateiOSPlist")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
|
|
||||||
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
|
|
||||||
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
|
|
||||||
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("UpdateiOSCodeFiles")
|
|
||||||
.IsDependentOn("UpdateiOSPlist")
|
|
||||||
.Does(()=> {
|
|
||||||
var buildVariant = GetVariant();
|
|
||||||
var fileList = new string[] {
|
|
||||||
Path.Combine(_slnPath, "src", "iOS.Core", "Utilities", "iOSCoreHelpers.cs"),
|
|
||||||
Path.Combine(_slnPath, "src", "iOS.Core", "Constants.cs"),
|
|
||||||
Path.Combine(".github", "resources", "export-options-ad-hoc.plist"),
|
|
||||||
Path.Combine(".github", "resources", "export-options-app-store.plist"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach(string path in fileList)
|
|
||||||
{
|
|
||||||
ReplaceInFile(path, "com.8bit.bitwarden", buildVariant.iOSBundleId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#endregion iOS
|
|
||||||
|
|
||||||
#region Main Tasks
|
|
||||||
Task("Android")
|
|
||||||
//.IsDependentOn("UpdateAndroidAppIcon")
|
|
||||||
.IsDependentOn("UpdateAndroidManifest")
|
|
||||||
.IsDependentOn("UpdateAndroidCodeFiles")
|
|
||||||
.Does(()=>
|
|
||||||
{
|
|
||||||
Information("Android app updated");
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("iOS")
|
|
||||||
//.IsDependentOn("UpdateiOSIcon")
|
|
||||||
.IsDependentOn("UpdateiOSPlist")
|
|
||||||
.IsDependentOn("UpdateiOSAutofillPlist")
|
|
||||||
.IsDependentOn("UpdateiOSExtensionPlist")
|
|
||||||
.IsDependentOn("UpdateiOSShareExtensionPlist")
|
|
||||||
.IsDependentOn("UpdateiOSCodeFiles")
|
|
||||||
.Does(()=>
|
|
||||||
{
|
|
||||||
Information("iOS app updated");
|
|
||||||
});
|
|
||||||
|
|
||||||
Task("Default")
|
|
||||||
.Does(() => {
|
|
||||||
var usage = @"Missing target.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
dotnet cake build.cake --target (Android | iOS) --variant (dev | qa | beta | prod)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--debugScript=<bool> Script debug mode.
|
|
||||||
";
|
|
||||||
Information(usage);
|
|
||||||
});
|
|
||||||
#endregion Main Tasks
|
|
||||||
|
|
||||||
RunTarget(target);
|
|
||||||
@@ -367,7 +367,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
|
|
||||||
public static string GetUri(AccessibilityNodeInfo root)
|
public static string GetUri(AccessibilityNodeInfo root)
|
||||||
{
|
{
|
||||||
var uri = string.Concat(Core.Constants.AndroidAppProtocol, root.PackageName);
|
var uri = string.Concat(Constants.AndroidAppProtocol, root.PackageName);
|
||||||
if (SupportedBrowsers.ContainsKey(root.PackageName))
|
if (SupportedBrowsers.ContainsKey(root.PackageName))
|
||||||
{
|
{
|
||||||
var browser = SupportedBrowsers[root.PackageName];
|
var browser = SupportedBrowsers[root.PackageName];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Droid.Accessibility
|
namespace Bit.Droid.Accessibility
|
||||||
{
|
{
|
||||||
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden", Exported = true)]
|
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")]
|
||||||
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
|
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
|
||||||
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
|
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
|
||||||
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||||
<TargetFrameworkVersion>v12.1</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
|
||||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||||
<NuGetPackageImportStamp>
|
<NuGetPackageImportStamp>
|
||||||
</NuGetPackageImportStamp>
|
</NuGetPackageImportStamp>
|
||||||
@@ -75,24 +75,24 @@
|
|||||||
<Version>2.1.0.4</Version>
|
<Version>2.1.0.4</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Portable.BouncyCastle">
|
<PackageReference Include="Portable.BouncyCastle">
|
||||||
<Version>1.9.0</Version>
|
<Version>1.8.10</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" />
|
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
|
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.9" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
|
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.11" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
|
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.10" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
|
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
|
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
|
||||||
<PackageReference Include="Xamarin.Essentials">
|
<PackageReference Include="Xamarin.Essentials">
|
||||||
<Version>1.7.3</Version>
|
<Version>1.7.3</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||||
<Version>123.0.8</Version>
|
<Version>122.0.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
|
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.4.0.4" />
|
||||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
|
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||||
<Version>118.0.1.2</Version>
|
<Version>117.0.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -151,12 +151,6 @@
|
|||||||
<Compile Include="Services\ClipboardService.cs" />
|
<Compile Include="Services\ClipboardService.cs" />
|
||||||
<Compile Include="Utilities\IntentExtensions.cs" />
|
<Compile Include="Utilities\IntentExtensions.cs" />
|
||||||
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
||||||
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
|
||||||
<Compile Include="Receivers\NotificationDismissReceiver.cs" />
|
|
||||||
<Compile Include="Services\FileService.cs" />
|
|
||||||
<Compile Include="Services\AutofillHandler.cs" />
|
|
||||||
<Compile Include="Constants.cs" />
|
|
||||||
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
@@ -181,7 +175,6 @@
|
|||||||
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
|
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\icon.xml" />
|
<AndroidResource Include="Resources\drawable\icon.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.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\ic_warning.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\id.xml" />
|
<AndroidResource Include="Resources\drawable\id.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\info.xml" />
|
<AndroidResource Include="Resources\drawable\info.xml" />
|
||||||
@@ -219,13 +212,6 @@
|
|||||||
<AndroidResource Include="Resources\values\colors.xml" />
|
<AndroidResource Include="Resources\values\colors.xml" />
|
||||||
<AndroidResource Include="Resources\values\manifest.xml" />
|
<AndroidResource Include="Resources\values\manifest.xml" />
|
||||||
<AndroidResource Include="Resources\values-v30\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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
||||||
@@ -293,8 +279,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\values-v30\" />
|
<Folder Include="Resources\values-v30\" />
|
||||||
<Folder Include="Resources\drawable-v26\" />
|
|
||||||
<Folder Include="Resources\drawable-night-v26\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
Binary file not shown.
@@ -19,7 +19,6 @@ using AndroidX.AutoFill.Inline;
|
|||||||
using AndroidX.AutoFill.Inline.V1;
|
using AndroidX.AutoFill.Inline.V1;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
||||||
using Bit.Droid.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
namespace Bit.Droid.Autofill
|
||||||
{
|
{
|
||||||
@@ -271,7 +270,8 @@ namespace Bit.Droid.Autofill
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
intent.PutExtra("autofillFrameworkUri", uri);
|
intent.PutExtra("autofillFrameworkUri", uri);
|
||||||
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
|
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
|
||||||
|
PendingIntentFlags.CancelCurrent);
|
||||||
|
|
||||||
var overlayPresentation = BuildOverlayPresentation(
|
var overlayPresentation = BuildOverlayPresentation(
|
||||||
AppResources.AutofillWithBitwarden,
|
AppResources.AutofillWithBitwarden,
|
||||||
@@ -324,7 +324,7 @@ namespace Bit.Droid.Autofill
|
|||||||
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
||||||
// "my vault" presentation) so we're including an empty one here
|
// "my vault" presentation) so we're including an empty one here
|
||||||
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
||||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
|
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
|
||||||
}
|
}
|
||||||
var slice = CreateInlinePresentationSlice(
|
var slice = CreateInlinePresentationSlice(
|
||||||
inlinePresentationSpec,
|
inlinePresentationSpec,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
namespace Bit.Droid.Autofill
|
||||||
{
|
{
|
||||||
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden", Exported = true)]
|
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")]
|
||||||
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
|
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
|
||||||
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
|
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
|
||||||
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
|
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
|
||||||
@@ -134,7 +134,7 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
case CipherType.Login:
|
case CipherType.Login:
|
||||||
intent.PutExtra("autofillFrameworkName", parser.Uri
|
intent.PutExtra("autofillFrameworkName", parser.Uri
|
||||||
.Replace(Core.Constants.AndroidAppProtocol, string.Empty)
|
.Replace(Constants.AndroidAppProtocol, string.Empty)
|
||||||
.Replace("https://", string.Empty)
|
.Replace("https://", string.Empty)
|
||||||
.Replace("http://", string.Empty));
|
.Replace("http://", string.Empty));
|
||||||
intent.PutExtra("autofillFrameworkUri", parser.Uri);
|
intent.PutExtra("autofillFrameworkUri", parser.Uri);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_uri = string.Concat(Core.Constants.AndroidAppProtocol, PackageName);
|
_uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
|
||||||
}
|
}
|
||||||
return _uri;
|
return _uri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Bit.Droid
|
|
||||||
{
|
|
||||||
public static class Constants
|
|
||||||
{
|
|
||||||
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using Android.Widget;
|
|
||||||
using Bit.Droid.Effects;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Forms.Platform.Android;
|
|
||||||
|
|
||||||
[assembly: ExportEffect(typeof(NoEmojiKeyboardEffect), nameof(NoEmojiKeyboardEffect))]
|
|
||||||
namespace Bit.Droid.Effects
|
|
||||||
{
|
|
||||||
public class NoEmojiKeyboardEffect : PlatformEffect
|
|
||||||
{
|
|
||||||
protected override void OnAttached()
|
|
||||||
{
|
|
||||||
if (Control is EditText editText)
|
|
||||||
{
|
|
||||||
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetached()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using Android.Widget;
|
|
||||||
using Bit.Droid.Effects;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Forms.Platform.Android;
|
|
||||||
|
|
||||||
[assembly: ExportEffect(typeof(RemoveFontPaddingEffect), nameof(RemoveFontPaddingEffect))]
|
|
||||||
namespace Bit.Droid.Effects
|
|
||||||
{
|
|
||||||
public class RemoveFontPaddingEffect : PlatformEffect
|
|
||||||
{
|
|
||||||
protected override void OnAttached()
|
|
||||||
{
|
|
||||||
if (Control is TextView textView)
|
|
||||||
{
|
|
||||||
textView.SetIncludeFontPadding(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetached()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,12 @@ using System.Threading.Tasks;
|
|||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Content.Res;
|
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Runtime;
|
using Android.Runtime;
|
||||||
using Android.Views;
|
using AndroidX.Core.Content;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
@@ -20,11 +18,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Droid.Receivers;
|
using Bit.Droid.Receivers;
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Xamarin.Essentials;
|
|
||||||
using ZXing.Net.Mobile.Android;
|
using ZXing.Net.Mobile.Android;
|
||||||
using FileProvider = AndroidX.Core.Content.FileProvider;
|
|
||||||
|
|
||||||
namespace Bit.Droid
|
namespace Bit.Droid
|
||||||
{
|
{
|
||||||
@@ -36,14 +30,11 @@ namespace Bit.Droid
|
|||||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||||
{
|
{
|
||||||
private IDeviceActionService _deviceActionService;
|
private IDeviceActionService _deviceActionService;
|
||||||
private IFileService _fileService;
|
|
||||||
private IMessagingService _messagingService;
|
private IMessagingService _messagingService;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
private IStateService _stateService;
|
private IStateService _stateService;
|
||||||
private IAppIdService _appIdService;
|
private IAppIdService _appIdService;
|
||||||
private IEventService _eventService;
|
private IEventService _eventService;
|
||||||
private IPushNotificationListenerService _pushNotificationListenerService;
|
|
||||||
private ILogger _logger;
|
|
||||||
private PendingIntent _eventUploadPendingIntent;
|
private PendingIntent _eventUploadPendingIntent;
|
||||||
private AppOptions _appOptions;
|
private AppOptions _appOptions;
|
||||||
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
||||||
@@ -54,20 +45,17 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
||||||
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
||||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
|
PendingIntentFlags.UpdateCurrent);
|
||||||
|
|
||||||
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
|
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
|
||||||
StrictMode.SetThreadPolicy(policy);
|
StrictMode.SetThreadPolicy(policy);
|
||||||
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||||
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
|
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
||||||
|
|
||||||
TabLayoutResource = Resource.Layout.Tabbar;
|
TabLayoutResource = Resource.Layout.Tabbar;
|
||||||
ToolbarResource = Resource.Layout.Toolbar;
|
ToolbarResource = Resource.Layout.Toolbar;
|
||||||
@@ -82,7 +70,7 @@ namespace Bit.Droid
|
|||||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.InitAsync();
|
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
||||||
|
|
||||||
var toplayout = Window?.DecorView?.RootView;
|
var toplayout = Window?.DecorView?.RootView;
|
||||||
if (toplayout != null)
|
if (toplayout != null)
|
||||||
@@ -93,9 +81,8 @@ namespace Bit.Droid
|
|||||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||||
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
||||||
_appOptions = GetOptions();
|
_appOptions = GetOptions();
|
||||||
CreateNotificationChannel();
|
|
||||||
LoadApplication(new App.App(_appOptions));
|
LoadApplication(new App.App(_appOptions));
|
||||||
DisableAndroidFontScale();
|
|
||||||
|
|
||||||
_broadcasterService.Subscribe(_activityKey, (message) =>
|
_broadcasterService.Subscribe(_activityKey, (message) =>
|
||||||
{
|
{
|
||||||
@@ -151,15 +138,6 @@ namespace Bit.Droid
|
|||||||
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
|
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
|
|
||||||
if (Intent?.GetStringExtra(Core.Constants.NotificationData) is string notificationDataJson)
|
|
||||||
{
|
|
||||||
var notificationType = JToken.Parse(notificationDataJson).SelectToken(Core.Constants.NotificationDataType);
|
|
||||||
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
|
|
||||||
{
|
|
||||||
_pushNotificationListenerService.OnNotificationTapped(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNewIntent(Intent intent)
|
protected override void OnNewIntent(Intent intent)
|
||||||
@@ -213,13 +191,13 @@ namespace Bit.Droid
|
|||||||
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
||||||
[GeneratedEnum] Permission[] grantResults)
|
[GeneratedEnum] Permission[] grantResults)
|
||||||
{
|
{
|
||||||
if (requestCode == Core.Constants.SelectFilePermissionRequestCode)
|
if (requestCode == Constants.SelectFilePermissionRequestCode)
|
||||||
{
|
{
|
||||||
if (grantResults.Any(r => r != Permission.Granted))
|
if (grantResults.Any(r => r != Permission.Granted))
|
||||||
{
|
{
|
||||||
_messagingService.Send("selectFileCameraPermissionDenied");
|
_messagingService.Send("selectFileCameraPermissionDenied");
|
||||||
}
|
}
|
||||||
await _fileService.SelectFileAsync();
|
await _deviceActionService.SelectFileAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -232,7 +210,7 @@ namespace Bit.Droid
|
|||||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||||
{
|
{
|
||||||
if (resultCode == Result.Ok &&
|
if (resultCode == Result.Ok &&
|
||||||
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
|
(requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode))
|
||||||
{
|
{
|
||||||
Android.Net.Uri uri = null;
|
Android.Net.Uri uri = null;
|
||||||
string fileName = null;
|
string fileName = null;
|
||||||
@@ -254,7 +232,7 @@ namespace Bit.Droid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestCode == Core.Constants.SaveFileRequestCode)
|
if (requestCode == Constants.SaveFileRequestCode)
|
||||||
{
|
{
|
||||||
_messagingService.Send("selectSaveFileResult",
|
_messagingService.Send("selectSaveFileResult",
|
||||||
new Tuple<string, string>(uri.ToString(), fileName));
|
new Tuple<string, string>(uri.ToString(), fileName));
|
||||||
@@ -295,7 +273,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
var intent = new Intent(this, Class);
|
var intent = new Intent(this, Class);
|
||||||
intent.AddFlags(ActivityFlags.SingleTop);
|
intent.AddFlags(ActivityFlags.SingleTop);
|
||||||
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(0, true));
|
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
|
||||||
// register for all NDEF tags starting with http och https
|
// register for all NDEF tags starting with http och https
|
||||||
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
||||||
ndef.AddDataScheme("http");
|
ndef.AddDataScheme("http");
|
||||||
@@ -423,38 +401,5 @@ namespace Bit.Droid
|
|||||||
alarmManager.Cancel(_eventUploadPendingIntent);
|
alarmManager.Cancel(_eventUploadPendingIntent);
|
||||||
await _eventService.UploadEventsAsync();
|
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(Core.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,8 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
RegisterLocalServices();
|
RegisterLocalServices();
|
||||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey,
|
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
|
||||||
Core.Constants.AndroidAllClearCipherCacheKeys);
|
Constants.AndroidAllClearCipherCacheKeys);
|
||||||
InitializeAppSetup();
|
|
||||||
|
|
||||||
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
|
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
|
||||||
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
|
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
|
||||||
@@ -72,9 +71,8 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||||
ServiceContainer.Resolve<ILogger>("logger"),
|
ServiceContainer.Resolve<ILogger>("logger"));
|
||||||
ServiceContainer.Resolve<IMessagingService>("messagingService"));
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||||
@@ -139,9 +137,8 @@ namespace Bit.Droid
|
|||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
|
||||||
var fileService = new FileService(stateService, broadcasterService);
|
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||||
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, new LazyResolve<IEventService>());
|
|
||||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||||
messagingService, broadcasterService);
|
messagingService, broadcasterService);
|
||||||
var biometricService = new BiometricService();
|
var biometricService = new BiometricService();
|
||||||
@@ -160,8 +157,6 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||||
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
||||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||||
ServiceContainer.Register<IFileService>(fileService);
|
|
||||||
ServiceContainer.Register<IAutofillHandler>(autofillHandler);
|
|
||||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||||
@@ -198,12 +193,5 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeAppSetup()
|
|
||||||
{
|
|
||||||
var appSetup = new AppSetup();
|
|
||||||
appSetup.InitializeServicesLastChance();
|
|
||||||
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,57 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.6.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.NFC"/>
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<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">
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
|
|
||||||
</provider>
|
<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">
|
||||||
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
||||||
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
|
||||||
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
</provider>
|
||||||
<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" />
|
<meta-data android:name="android.max_aspect" android:value="2.1"/>
|
||||||
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions"/>
|
||||||
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
|
|
||||||
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
||||||
<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">
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true"/>
|
||||||
<intent-filter>
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true"/>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
||||||
</intent-filter>
|
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true"/>
|
||||||
<intent-filter>
|
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
||||||
<action android:name="android.intent.action.SEND" />
|
<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">
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<intent-filter>
|
||||||
<data android:mimeType="application/*" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<data android:mimeType="image/*" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
<data android:mimeType="video/*" />
|
</intent-filter>
|
||||||
<data android:mimeType="text/*" />
|
<intent-filter>
|
||||||
</intent-filter>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
</activity>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</application>
|
<data android:mimeType="application/*"/>
|
||||||
<!-- Package visibility (for Android 11+) -->
|
<data android:mimeType="image/*"/>
|
||||||
<queries>
|
<data android:mimeType="video/*"/>
|
||||||
<intent>
|
<data android:mimeType="text/*"/>
|
||||||
<action android:name="*" />
|
</intent-filter>
|
||||||
</intent>
|
</activity>
|
||||||
</queries>
|
</application>
|
||||||
</manifest>
|
|
||||||
|
<!-- Package visibility (for Android 11+) -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="*"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Firebase.Messaging;
|
using Firebase.Messaging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -18,41 +16,34 @@ namespace Bit.Droid.Push
|
|||||||
{
|
{
|
||||||
public async override void OnNewToken(string token)
|
public async override void OnNewToken(string token)
|
||||||
{
|
{
|
||||||
try {
|
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
|
||||||
|
|
||||||
await stateService.SetPushRegisteredTokenAsync(token);
|
await stateService.SetPushRegisteredTokenAsync(token);
|
||||||
await pushNotificationService.RegisterAsync();
|
await pushNotificationService.RegisterAsync();
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Instance.Exception(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override void OnMessageReceived(RemoteMessage message)
|
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
|
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 obj = JObject.Parse(data);
|
||||||
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
||||||
"pushNotificationListenerService");
|
"pushNotificationListenerService");
|
||||||
await listener.OnMessageAsync(obj, Device.Android);
|
await listener.OnMessageAsync(obj, Device.Android);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (JsonReaderException ex)
|
||||||
{
|
{
|
||||||
Logger.Instance.Exception(ex);
|
System.Diagnostics.Debug.WriteLine(ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
using Android.Content;
|
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Models;
|
|
||||||
using Bit.App.Services;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using CoreConstants = Bit.Core.Constants;
|
|
||||||
|
|
||||||
namespace Bit.Droid.Receivers
|
|
||||||
{
|
|
||||||
[BroadcastReceiver(Name = Constants.PACKAGE_NAME + "." + nameof(NotificationDismissReceiver), Exported = false)]
|
|
||||||
public class NotificationDismissReceiver : BroadcastReceiver
|
|
||||||
{
|
|
||||||
private readonly LazyResolve<IPushNotificationListenerService> _pushNotificationListenerService = new LazyResolve<IPushNotificationListenerService>();
|
|
||||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
|
||||||
|
|
||||||
public override void OnReceive(Context context, Intent intent)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (intent?.GetStringExtra(CoreConstants.NotificationData) is string notificationDataJson)
|
|
||||||
{
|
|
||||||
var notificationType = JToken.Parse(notificationDataJson).SelectToken(CoreConstants.NotificationDataType);
|
|
||||||
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
|
|
||||||
{
|
|
||||||
_pushNotificationListenerService.Value.OnNotificationDismissed(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Value.Exception(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/darkgray"/>
|
|
||||||
<foreground android:drawable="@drawable/logo_rounded"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/logo_rounded"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<group
|
|
||||||
android:scaleX="0.099"
|
|
||||||
android:scaleY="0.099"
|
|
||||||
android:translateX="24.3"
|
|
||||||
android:translateY="24.3">
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="M481.4,102.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6L131.7,96.6c-5.1,0 -9.4,1.9 -13.1,5.6C114.9,105.9 113,110.2 113,115.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8L487.3,115.3C487,110.2 485.1,105.9 481.4,102.2zM438,341.8C438,423 300,493 300,493L300,144.6h138C438,144.6 438,260.6 438,341.8z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<vector android:height="24dp" android:viewportHeight="420"
|
|
||||||
android:viewportWidth="420" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M350.43,40.516C347.563,37.65 344.153,36.178 340.281,36.178L79.487,36.178C75.538,36.178 72.206,37.65 69.338,40.516C66.472,43.384 65,46.716 65,50.665L65,224.527C65,237.466 67.557,250.405 72.593,263.112C77.629,275.895 83.904,287.207 91.42,297.046C98.857,306.964 107.768,316.571 118.149,325.869C128.455,335.242 138.063,342.99 146.817,349.19C155.573,355.387 164.715,361.198 174.243,366.699C183.773,372.2 190.514,375.919 194.543,377.933C198.572,379.871 201.749,381.421 204.151,382.426C205.932,383.357 207.948,383.821 210.04,383.821C212.131,383.821 214.145,383.357 215.929,382.426C218.329,381.344 221.584,379.871 225.534,377.933C229.563,375.997 236.304,372.2 245.832,366.699C255.365,361.198 264.506,355.311 273.262,349.19C282.017,342.99 291.545,335.242 301.928,325.869C312.232,316.493 321.142,306.886 328.657,297.046C336.096,287.129 342.372,275.819 347.407,263.112C352.444,250.328 355,237.466 355,224.527L355,50.665C354.768,46.716 353.296,43.384 350.43,40.516ZM316.804,226.154C316.804,289.067 209.883,343.302 209.883,343.302L209.883,73.368L316.804,73.368C316.804,73.368 316.804,163.242 316.804,226.154Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<group android:scaleX="0.11454546"
|
|
||||||
android:scaleY="0.11454546"
|
|
||||||
android:translateX="31.663637"
|
|
||||||
android:translateY="27.54">
|
|
||||||
<path
|
|
||||||
android:pathData="M376.4,12.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6H26.7c-5.1,0 -9.4,1.9 -13.1,5.6C9.9,15.9 8,20.2 8,25.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8V25.3C382,20.2 380.1,15.9 376.4,12.2zM333,251.8C333,333 195,403 195,403V54.6h138C333,54.6 333,170.6 333,251.8z"
|
|
||||||
android:fillColor="#FFFFFF"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<style name="LaunchTheme" parent="BaseTheme">
|
<style name="LaunchTheme" parent="BaseTheme">
|
||||||
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
|
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="BaseTheme" parent="Theme.AppCompat">
|
<style name="BaseTheme" parent="Theme.AppCompat">
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
<style name="LaunchTheme" parent="BaseTheme">
|
<style name="LaunchTheme" parent="BaseTheme">
|
||||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||||
<item name="android:windowNoTitle">true</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>
|
||||||
|
|
||||||
<style name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
|||||||
@@ -6,5 +6,4 @@
|
|||||||
android:accessibilityFeedbackType="feedbackGeneric"
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||||
android:notificationTimeout="100"
|
android:notificationTimeout="100"
|
||||||
android:canRetrieveWindowContent="true"
|
android:canRetrieveWindowContent="true"/>
|
||||||
android:isAccessibilityTool="false"/>
|
|
||||||
@@ -1,21 +1,10 @@
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
|
||||||
using Android.OS;
|
|
||||||
using AndroidX.Core.App;
|
using AndroidX.Core.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Droid.Receivers;
|
|
||||||
using Bit.Droid.Utilities;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using static Xamarin.Essentials.Platform;
|
|
||||||
using Intent = Android.Content.Intent;
|
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
@@ -34,11 +23,6 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
||||||
|
|
||||||
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
|
||||||
{
|
|
||||||
return Task.FromResult(IsRegisteredForPush);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GetTokenAsync()
|
public async Task<string> GetTokenAsync()
|
||||||
{
|
{
|
||||||
return await _stateService.GetPushCurrentTokenAsync();
|
return await _stateService.GetPushCurrentTokenAsync();
|
||||||
@@ -63,50 +47,6 @@ namespace Bit.Droid.Services
|
|||||||
// Do we ever need to unregister?
|
// Do we ever need to unregister?
|
||||||
return Task.FromResult(0);
|
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, BaseNotificationData data)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(data.Id))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = Android.App.Application.Context;
|
|
||||||
var intent = new Intent(context, typeof(MainActivity));
|
|
||||||
intent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
|
||||||
var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
|
|
||||||
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
|
|
||||||
|
|
||||||
var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
|
|
||||||
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
|
||||||
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
|
|
||||||
|
|
||||||
var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId)
|
|
||||||
.SetContentIntent(pendingIntent)
|
|
||||||
.SetContentTitle(title)
|
|
||||||
.SetContentText(message)
|
|
||||||
.SetSmallIcon(Resource.Drawable.ic_notification)
|
|
||||||
.SetColor((int)Android.Graphics.Color.White)
|
|
||||||
.SetDeleteIntent(deletePendingIntent)
|
|
||||||
.SetAutoCancel(true);
|
|
||||||
|
|
||||||
if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0)
|
|
||||||
{
|
|
||||||
builder.SetTimeoutAfter(passwordlessNotificationData.TimeoutInMinutes * 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
var notificationManager = NotificationManagerCompat.From(context);
|
|
||||||
notificationManager.Notify(int.Parse(data.Id), builder.Build());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Android.App;
|
|
||||||
using Android.App.Assist;
|
|
||||||
using Android.Content;
|
|
||||||
using Android.OS;
|
|
||||||
using Android.Provider;
|
|
||||||
using Android.Views.Autofill;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Droid.Autofill;
|
|
||||||
using Plugin.CurrentActivity;
|
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
|
||||||
{
|
|
||||||
public class AutofillHandler : IAutofillHandler
|
|
||||||
{
|
|
||||||
private readonly IStateService _stateService;
|
|
||||||
private readonly IMessagingService _messagingService;
|
|
||||||
private readonly IClipboardService _clipboardService;
|
|
||||||
private readonly LazyResolve<IEventService> _eventService;
|
|
||||||
|
|
||||||
public AutofillHandler(IStateService stateService,
|
|
||||||
IMessagingService messagingService,
|
|
||||||
IClipboardService clipboardService,
|
|
||||||
LazyResolve<IEventService> eventService)
|
|
||||||
{
|
|
||||||
_stateService = stateService;
|
|
||||||
_messagingService = messagingService;
|
|
||||||
_clipboardService = clipboardService;
|
|
||||||
_eventService = eventService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServiceEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var afm = (AutofillManager)activity.GetSystemService(
|
|
||||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
|
||||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SupportsAutofillService()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
return manager.IsAutofillSupported;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Autofill(CipherView cipher)
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
if (activity == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
|
||||||
{
|
|
||||||
if (cipher == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var structure = activity.Intent.GetParcelableExtra(
|
|
||||||
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
|
||||||
if (structure == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var parser = new Parser(structure, activity.ApplicationContext);
|
|
||||||
parser.Parse();
|
|
||||||
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
|
|
||||||
var replyIntent = new Intent();
|
|
||||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
|
||||||
activity.SetResult(Result.Ok, replyIntent);
|
|
||||||
activity.Finish();
|
|
||||||
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var data = new Intent();
|
|
||||||
if (cipher?.Login == null)
|
|
||||||
{
|
|
||||||
data.PutExtra("canceled", "true");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
data.PutExtra("uri", cipher.Login.Uri);
|
|
||||||
data.PutExtra("username", cipher.Login.Username);
|
|
||||||
data.PutExtra("password", cipher.Login.Password);
|
|
||||||
}
|
|
||||||
if (activity.Parent == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.Parent.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
activity.Finish();
|
|
||||||
_messagingService.Send("finishMainActivity");
|
|
||||||
if (cipher != null)
|
|
||||||
{
|
|
||||||
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseAutofill()
|
|
||||||
{
|
|
||||||
Autofill(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityServiceRunning()
|
|
||||||
{
|
|
||||||
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
|
||||||
Settings.Secure.EnabledAccessibilityServices);
|
|
||||||
return Application.Context.PackageName != null &&
|
|
||||||
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityOverlayPermitted()
|
|
||||||
{
|
|
||||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void DisableAutofillService()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
manager.DisableAutofillServices();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServicesEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
|
||||||
{
|
|
||||||
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
|
||||||
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
|
||||||
{
|
|
||||||
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
|
||||||
return AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
// Android 8+: Either autofill or accessibility is required
|
|
||||||
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CopyTotpAsync(CipherView cipher)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
|
||||||
{
|
|
||||||
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
|
||||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
|
||||||
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
|
||||||
{
|
|
||||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
||||||
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
|
||||||
if (totp != null)
|
|
||||||
{
|
|
||||||
await _clipboardService.CopyTextAsync(totp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ using Android.Content;
|
|||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Droid.Receivers;
|
using Bit.Droid.Receivers;
|
||||||
using Bit.Droid.Utilities;
|
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ namespace Bit.Droid.Services
|
|||||||
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
||||||
0,
|
0,
|
||||||
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
||||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
|
PendingIntentFlags.UpdateCurrent));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Android;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
|
using Android.App.Assist;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
@@ -9,13 +14,20 @@ using Android.Provider;
|
|||||||
using Android.Text;
|
using Android.Text;
|
||||||
using Android.Text.Method;
|
using Android.Text.Method;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
|
using Android.Views.Autofill;
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
|
using Android.Webkit;
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
|
using AndroidX.Core.App;
|
||||||
|
using AndroidX.Core.Content;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Droid.Autofill;
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
@@ -23,20 +35,38 @@ namespace Bit.Droid.Services
|
|||||||
{
|
{
|
||||||
public class DeviceActionService : IDeviceActionService
|
public class DeviceActionService : IDeviceActionService
|
||||||
{
|
{
|
||||||
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
|
private readonly Func<IEventService> _eventServiceFunc;
|
||||||
private AlertDialog _progressDialog;
|
private AlertDialog _progressDialog;
|
||||||
object _progressDialogLock = new object();
|
object _progressDialogLock = new object();
|
||||||
|
|
||||||
|
private bool _cameraPermissionsDenied;
|
||||||
private Toast _toast;
|
private Toast _toast;
|
||||||
private string _userAgent;
|
private string _userAgent;
|
||||||
|
|
||||||
public DeviceActionService(
|
public DeviceActionService(
|
||||||
|
IClipboardService clipboardService,
|
||||||
IStateService stateService,
|
IStateService stateService,
|
||||||
IMessagingService messagingService)
|
IMessagingService messagingService,
|
||||||
|
IBroadcasterService broadcasterService,
|
||||||
|
Func<IEventService> eventServiceFunc)
|
||||||
{
|
{
|
||||||
|
_clipboardService = clipboardService;
|
||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
_messagingService = messagingService;
|
_messagingService = messagingService;
|
||||||
|
_broadcasterService = broadcasterService;
|
||||||
|
_eventServiceFunc = eventServiceFunc;
|
||||||
|
|
||||||
|
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
||||||
|
{
|
||||||
|
if (message.Command == "selectFileCameraPermissionDenied")
|
||||||
|
{
|
||||||
|
_cameraPermissionsDenied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DeviceUserAgent
|
public string DeviceUserAgent
|
||||||
@@ -182,6 +212,184 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
activity.StartActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanOpenFile(string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||||
|
PackageInfoFlags.MatchDefaultOnly);
|
||||||
|
return (activities?.Count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||||
|
{
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var cachePath = activity.CacheDir;
|
||||||
|
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||||
|
File.WriteAllBytes(filePath, fileData);
|
||||||
|
var file = new Java.IO.File(cachePath, fileName);
|
||||||
|
if (!file.IsFile)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var intent = new Intent(Intent.ActionView);
|
||||||
|
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||||
|
"com.x8bit.bitwarden.fileprovider", file);
|
||||||
|
intent.SetDataAndType(uri, mimeType);
|
||||||
|
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
|
||||||
|
if (contentUri != null)
|
||||||
|
{
|
||||||
|
var uri = Android.Net.Uri.Parse(contentUri);
|
||||||
|
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
||||||
|
// Using java bufferedOutputStream due to this issue:
|
||||||
|
// https://github.com/xamarin/xamarin-android/issues/3498
|
||||||
|
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
||||||
|
javaStream.Write(fileData);
|
||||||
|
javaStream.Flush();
|
||||||
|
javaStream.Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for location to save file
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
// Unable to identify so fall back to generic "any" type
|
||||||
|
mimeType = "*/*";
|
||||||
|
}
|
||||||
|
|
||||||
|
var intent = new Intent(Intent.ActionCreateDocument);
|
||||||
|
intent.SetType(mimeType);
|
||||||
|
intent.AddCategory(Intent.CategoryOpenable);
|
||||||
|
intent.PutExtra(Intent.ExtraTitle, fileName);
|
||||||
|
|
||||||
|
activity.StartActivityForResult(intent, Constants.SaveFileRequestCode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearCacheAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
||||||
|
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SelectFileAsync()
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
||||||
|
HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||||
|
var additionalIntents = new List<IParcelable>();
|
||||||
|
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
||||||
|
{
|
||||||
|
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
|
||||||
|
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
|
||||||
|
{
|
||||||
|
AskPermission(Manifest.Permission.WriteExternalStorage);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
if (!_cameraPermissionsDenied && !hasCameraPermission)
|
||||||
|
{
|
||||||
|
AskPermission(Manifest.Permission.Camera);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||||
|
if (!file.Exists())
|
||||||
|
{
|
||||||
|
file.ParentFile.Mkdirs();
|
||||||
|
file.CreateNewFile();
|
||||||
|
}
|
||||||
|
var outputFileUri = FileProvider.GetUriForFile(activity,
|
||||||
|
"com.x8bit.bitwarden.fileprovider", file);
|
||||||
|
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
||||||
|
}
|
||||||
|
catch (Java.IO.IOException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var docIntent = new Intent(Intent.ActionOpenDocument);
|
||||||
|
docIntent.AddCategory(Intent.CategoryOpenable);
|
||||||
|
docIntent.SetType("*/*");
|
||||||
|
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
|
||||||
|
if (additionalIntents.Count > 0)
|
||||||
|
{
|
||||||
|
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
|
||||||
|
}
|
||||||
|
activity.StartActivityForResult(chooserIntent, Constants.SelectFileRequestCode);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<string> DisplayPromptAync(string title = null, string description = null,
|
public Task<string> DisplayPromptAync(string title = null, string description = null,
|
||||||
string text = null, string okButtonText = null, string cancelButtonText = null,
|
string text = null, string okButtonText = null, string cancelButtonText = null,
|
||||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||||
@@ -259,6 +467,34 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DisableAutofillService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
manager.DisableAutofillServices();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillServicesEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
||||||
|
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
||||||
|
{
|
||||||
|
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
||||||
|
return AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
// Android 8+: Either autofill or accessibility is required
|
||||||
|
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
|
||||||
public string GetBuildNumber()
|
public string GetBuildNumber()
|
||||||
{
|
{
|
||||||
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
||||||
@@ -290,6 +526,25 @@ namespace Bit.Droid.Services
|
|||||||
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SupportsAutofillService()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
return manager.IsAutofillSupported;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int SystemMajorVersion()
|
public int SystemMajorVersion()
|
||||||
{
|
{
|
||||||
return (int)Build.VERSION.SdkInt;
|
return (int)Build.VERSION.SdkInt;
|
||||||
@@ -380,6 +635,112 @@ namespace Bit.Droid.Services
|
|||||||
title, cancel, destruction, buttons);
|
title, cancel, destruction, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Autofill(CipherView cipher)
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
||||||
|
{
|
||||||
|
if (cipher == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var structure = activity.Intent.GetParcelableExtra(
|
||||||
|
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
||||||
|
if (structure == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parser = new Parser(structure, activity.ApplicationContext);
|
||||||
|
parser.Parse();
|
||||||
|
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
|
||||||
|
var replyIntent = new Intent();
|
||||||
|
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
||||||
|
activity.SetResult(Result.Ok, replyIntent);
|
||||||
|
activity.Finish();
|
||||||
|
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var data = new Intent();
|
||||||
|
if (cipher?.Login == null)
|
||||||
|
{
|
||||||
|
data.PutExtra("canceled", "true");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
data.PutExtra("uri", cipher.Login.Uri);
|
||||||
|
data.PutExtra("username", cipher.Login.Username);
|
||||||
|
data.PutExtra("password", cipher.Login.Password);
|
||||||
|
}
|
||||||
|
if (activity.Parent == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activity.Parent.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
activity.Finish();
|
||||||
|
_messagingService.Send("finishMainActivity");
|
||||||
|
if (cipher != null)
|
||||||
|
{
|
||||||
|
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAutofill()
|
||||||
|
{
|
||||||
|
Autofill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Background()
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activity.MoveTaskToBack(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityServiceRunning()
|
||||||
|
{
|
||||||
|
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
||||||
|
Settings.Secure.EnabledAccessibilityServices);
|
||||||
|
return Application.Context.PackageName != null &&
|
||||||
|
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityOverlayPermitted()
|
||||||
|
{
|
||||||
|
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAutofillService()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void OpenAccessibilityOverlayPermissionSettings()
|
public void OpenAccessibilityOverlayPermissionSettings()
|
||||||
{
|
{
|
||||||
@@ -410,6 +771,25 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AutofillServiceEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var afm = (AutofillManager)activity.GetSystemService(
|
||||||
|
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||||
|
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void OpenAccessibilitySettings()
|
public void OpenAccessibilitySettings()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -468,6 +848,61 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool DeleteDir(Java.IO.File dir)
|
||||||
|
{
|
||||||
|
if (dir != null && dir.IsDirectory)
|
||||||
|
{
|
||||||
|
var children = dir.List();
|
||||||
|
for (int i = 0; i < children.Length; i++)
|
||||||
|
{
|
||||||
|
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
else if (dir != null && dir.IsFile)
|
||||||
|
{
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasPermission(string permission)
|
||||||
|
{
|
||||||
|
return ContextCompat.CheckSelfPermission(
|
||||||
|
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AskPermission(string permission)
|
||||||
|
{
|
||||||
|
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
|
||||||
|
Constants.SelectFilePermissionRequestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
|
||||||
|
{
|
||||||
|
var intents = new List<IParcelable>();
|
||||||
|
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
||||||
|
var captureIntent = new Intent(MediaStore.ActionImageCapture);
|
||||||
|
var listCam = pm.QueryIntentActivities(captureIntent, 0);
|
||||||
|
foreach (var res in listCam)
|
||||||
|
{
|
||||||
|
var packageName = res.ActivityInfo.PackageName;
|
||||||
|
var intent = new Intent(captureIntent);
|
||||||
|
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
|
||||||
|
intent.SetPackage(packageName);
|
||||||
|
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
||||||
|
intents.Add(intent);
|
||||||
|
}
|
||||||
|
return intents;
|
||||||
|
}
|
||||||
|
|
||||||
private Intent RateIntentForUrl(string url, Activity activity)
|
private Intent RateIntentForUrl(string url, Activity activity)
|
||||||
{
|
{
|
||||||
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
||||||
@@ -485,6 +920,24 @@ namespace Bit.Droid.Services
|
|||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CopyTotpAsync(CipherView cipher)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
||||||
|
{
|
||||||
|
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
||||||
|
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
|
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||||
|
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
||||||
|
if (totp != null)
|
||||||
|
{
|
||||||
|
await _clipboardService.CopyTextAsync(totp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public float GetSystemFontSizeScale()
|
public float GetSystemFontSizeScale()
|
||||||
{
|
{
|
||||||
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
||||||
@@ -511,20 +964,5 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseExtensionPopUp()
|
|
||||||
{
|
|
||||||
// only used by iOS
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Android;
|
|
||||||
using Android.Content;
|
|
||||||
using Android.Content.PM;
|
|
||||||
using Android.OS;
|
|
||||||
using Android.Provider;
|
|
||||||
using Android.Webkit;
|
|
||||||
using AndroidX.Core.App;
|
|
||||||
using AndroidX.Core.Content;
|
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Plugin.CurrentActivity;
|
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
|
||||||
{
|
|
||||||
public class FileService : IFileService
|
|
||||||
{
|
|
||||||
private readonly IStateService _stateService;
|
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
|
||||||
|
|
||||||
private bool _cameraPermissionsDenied;
|
|
||||||
|
|
||||||
public FileService(IStateService stateService, IBroadcasterService broadcasterService)
|
|
||||||
{
|
|
||||||
_stateService = stateService;
|
|
||||||
_broadcasterService = broadcasterService;
|
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(FileService), (message) =>
|
|
||||||
{
|
|
||||||
if (message.Command == "selectFileCameraPermissionDenied")
|
|
||||||
{
|
|
||||||
_cameraPermissionsDenied = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
activity.StartActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanOpenFile(string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
|
||||||
PackageInfoFlags.MatchDefaultOnly);
|
|
||||||
return (activities?.Count ?? 0) > 0;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
|
||||||
{
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var cachePath = activity.CacheDir;
|
|
||||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
|
||||||
File.WriteAllBytes(filePath, fileData);
|
|
||||||
var file = new Java.IO.File(cachePath, fileName);
|
|
||||||
if (!file.IsFile)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var intent = new Intent(Intent.ActionView);
|
|
||||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
|
||||||
"com.x8bit.bitwarden.fileprovider", file);
|
|
||||||
intent.SetDataAndType(uri, mimeType);
|
|
||||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
|
|
||||||
if (contentUri != null)
|
|
||||||
{
|
|
||||||
var uri = Android.Net.Uri.Parse(contentUri);
|
|
||||||
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
|
||||||
// Using java bufferedOutputStream due to this issue:
|
|
||||||
// https://github.com/xamarin/xamarin-android/issues/3498
|
|
||||||
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
|
||||||
javaStream.Write(fileData);
|
|
||||||
javaStream.Flush();
|
|
||||||
javaStream.Close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for location to save file
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
// Unable to identify so fall back to generic "any" type
|
|
||||||
mimeType = "*/*";
|
|
||||||
}
|
|
||||||
|
|
||||||
var intent = new Intent(Intent.ActionCreateDocument);
|
|
||||||
intent.SetType(mimeType);
|
|
||||||
intent.AddCategory(Intent.CategoryOpenable);
|
|
||||||
intent.PutExtra(Intent.ExtraTitle, fileName);
|
|
||||||
|
|
||||||
activity.StartActivityForResult(intent, Core.Constants.SaveFileRequestCode);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearCacheAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
|
||||||
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
|
||||||
}
|
|
||||||
catch (Exception) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SelectFileAsync()
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
|
||||||
HasPermission(Manifest.Permission.WriteExternalStorage);
|
|
||||||
var additionalIntents = new List<IParcelable>();
|
|
||||||
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
|
||||||
{
|
|
||||||
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
|
|
||||||
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
|
|
||||||
{
|
|
||||||
AskPermission(Manifest.Permission.WriteExternalStorage);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
if (!_cameraPermissionsDenied && !hasCameraPermission)
|
|
||||||
{
|
|
||||||
AskPermission(Manifest.Permission.Camera);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
|
||||||
if (!file.Exists())
|
|
||||||
{
|
|
||||||
file.ParentFile.Mkdirs();
|
|
||||||
file.CreateNewFile();
|
|
||||||
}
|
|
||||||
var outputFileUri = FileProvider.GetUriForFile(activity,
|
|
||||||
"com.x8bit.bitwarden.fileprovider", file);
|
|
||||||
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
|
||||||
}
|
|
||||||
catch (Java.IO.IOException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var docIntent = new Intent(Intent.ActionOpenDocument);
|
|
||||||
docIntent.AddCategory(Intent.CategoryOpenable);
|
|
||||||
docIntent.SetType("*/*");
|
|
||||||
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
|
|
||||||
if (additionalIntents.Count > 0)
|
|
||||||
{
|
|
||||||
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
|
|
||||||
}
|
|
||||||
activity.StartActivityForResult(chooserIntent, Core.Constants.SelectFileRequestCode);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DeleteDir(Java.IO.File dir)
|
|
||||||
{
|
|
||||||
if (dir is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir.IsDirectory)
|
|
||||||
{
|
|
||||||
var children = dir.List();
|
|
||||||
for (int i = 0; i < children.Length; i++)
|
|
||||||
{
|
|
||||||
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dir.Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dir.IsFile)
|
|
||||||
{
|
|
||||||
return dir.Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasPermission(string permission)
|
|
||||||
{
|
|
||||||
return ContextCompat.CheckSelfPermission(
|
|
||||||
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AskPermission(string permission)
|
|
||||||
{
|
|
||||||
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
|
|
||||||
Core.Constants.SelectFilePermissionRequestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
|
|
||||||
{
|
|
||||||
var intents = new List<IParcelable>();
|
|
||||||
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
|
||||||
var captureIntent = new Intent(MediaStore.ActionImageCapture);
|
|
||||||
var listCam = pm.QueryIntentActivities(captureIntent, 0);
|
|
||||||
foreach (var res in listCam)
|
|
||||||
{
|
|
||||||
var packageName = res.ActivityInfo.PackageName;
|
|
||||||
var intent = new Intent(captureIntent);
|
|
||||||
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
|
|
||||||
intent.SetPackage(packageName);
|
|
||||||
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
|
||||||
intents.Add(intent);
|
|
||||||
}
|
|
||||||
return intents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ using Java.Lang;
|
|||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
|
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
|
||||||
Icon = "@drawable/shield", Exported = true)]
|
Icon = "@drawable/shield")]
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.AutofillTileService")]
|
[Register("com.x8bit.bitwarden.AutofillTileService")]
|
||||||
public class AutofillTileService : TileService
|
public class AutofillTileService : TileService
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using Java.Lang;
|
|||||||
|
|
||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Exported = true, Label = "@string/PasswordGenerator",
|
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/PasswordGenerator",
|
||||||
Icon = "@drawable/generate")]
|
Icon = "@drawable/generate")]
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.GeneratorTileService")]
|
[Register("com.x8bit.bitwarden.GeneratorTileService")]
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ using Java.Lang;
|
|||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
|
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
|
||||||
Icon = "@drawable/shield",
|
Icon = "@drawable/shield")]
|
||||||
Exported = true)]
|
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.MyVaultTileService")]
|
[Register("com.x8bit.bitwarden.MyVaultTileService")]
|
||||||
public class MyVaultTileService : TileService
|
public class MyVaultTileService : TileService
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
|
||||||
@@ -49,22 +47,5 @@ namespace Bit.Droid.Utilities
|
|||||||
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
|
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,8 +7,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
[Activity(
|
[Activity(
|
||||||
NoHistory = true,
|
NoHistory = true,
|
||||||
LaunchMode = LaunchMode.SingleTop,
|
LaunchMode = LaunchMode.SingleTop)]
|
||||||
Exported = true)]
|
|
||||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||||
DataScheme = "bitwarden")]
|
DataScheme = "bitwarden")]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -7,36 +8,46 @@ namespace Bit.App.Abstractions
|
|||||||
{
|
{
|
||||||
string DeviceUserAgent { get; }
|
string DeviceUserAgent { get; }
|
||||||
DeviceType DeviceType { get; }
|
DeviceType DeviceType { get; }
|
||||||
int SystemMajorVersion();
|
|
||||||
string SystemModel();
|
|
||||||
string GetBuildNumber();
|
|
||||||
|
|
||||||
void Toast(string text, bool longDuration = false);
|
void Toast(string text, bool longDuration = false);
|
||||||
|
bool LaunchApp(string appName);
|
||||||
Task ShowLoadingAsync(string text);
|
Task ShowLoadingAsync(string text);
|
||||||
Task HideLoadingAsync();
|
Task HideLoadingAsync();
|
||||||
|
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||||
|
bool SaveFile(byte[] fileData, string id, string fileName, string contentUri);
|
||||||
|
bool CanOpenFile(string fileName);
|
||||||
|
Task ClearCacheAsync();
|
||||||
|
Task SelectFileAsync();
|
||||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||||
bool autofocus = true, bool password = false);
|
bool autofocus = true, bool password = false);
|
||||||
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
void RateApp();
|
||||||
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
|
||||||
|
|
||||||
bool SupportsFaceBiometric();
|
bool SupportsFaceBiometric();
|
||||||
Task<bool> SupportsFaceBiometricAsync();
|
Task<bool> SupportsFaceBiometricAsync();
|
||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsFido2();
|
bool SupportsAutofillService();
|
||||||
|
int SystemMajorVersion();
|
||||||
bool LaunchApp(string appName);
|
string SystemModel();
|
||||||
void RateApp();
|
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
||||||
|
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
||||||
|
void Autofill(CipherView cipher);
|
||||||
|
void CloseAutofill();
|
||||||
|
void Background();
|
||||||
|
bool AutofillAccessibilityServiceRunning();
|
||||||
|
bool AutofillAccessibilityOverlayPermitted();
|
||||||
|
bool HasAutofillService();
|
||||||
|
bool AutofillServiceEnabled();
|
||||||
|
void DisableAutofillService();
|
||||||
|
bool AutofillServicesEnabled();
|
||||||
|
string GetBuildNumber();
|
||||||
void OpenAccessibilitySettings();
|
void OpenAccessibilitySettings();
|
||||||
void OpenAccessibilityOverlayPermissionSettings();
|
void OpenAccessibilityOverlayPermissionSettings();
|
||||||
void OpenAutofillSettings();
|
void OpenAutofillSettings();
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
void CloseMainApp();
|
void CloseMainApp();
|
||||||
|
bool SupportsFido2();
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
Task SetScreenCaptureAllowedAsync();
|
Task SetScreenCaptureAllowedAsync();
|
||||||
void OpenAppSettings();
|
|
||||||
void CloseExtensionPopUp();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
@@ -10,8 +9,6 @@ namespace Bit.App.Abstractions
|
|||||||
Task OnRegisteredAsync(string token, string device);
|
Task OnRegisteredAsync(string token, string device);
|
||||||
void OnUnregistered(string device);
|
void OnUnregistered(string device);
|
||||||
void OnError(string message, string device);
|
void OnError(string message, string device);
|
||||||
Task OnNotificationTapped(BaseNotificationData data);
|
|
||||||
Task OnNotificationDismissed(BaseNotificationData data);
|
|
||||||
bool ShouldShowNotification();
|
bool ShouldShowNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.App.Models;
|
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
public interface IPushNotificationService
|
public interface IPushNotificationService
|
||||||
{
|
{
|
||||||
bool IsRegisteredForPush { get; }
|
bool IsRegisteredForPush { get; }
|
||||||
Task<bool> AreNotificationsSettingsEnabledAsync();
|
|
||||||
Task<string> GetTokenAsync();
|
Task<string> GetTokenAsync();
|
||||||
Task RegisterAsync();
|
Task RegisterAsync();
|
||||||
Task UnregisterAsync();
|
Task UnregisterAsync();
|
||||||
void SendLocalNotification(string title, string message, BaseNotificationData data);
|
|
||||||
void DismissLocalNotification(string notificationId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<RootNamespace>Bit.App</RootNamespace>
|
<RootNamespace>Bit.App</RootNamespace>
|
||||||
<AssemblyName>BitwardenApp</AssemblyName>
|
<AssemblyName>BitwardenApp</AssemblyName>
|
||||||
<Configurations>Debug;Release;FDroid</Configurations>
|
<Configurations>Debug;Release;FDroid</Configurations>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
||||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
|
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
|
||||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
|
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
||||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
|
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -97,11 +97,11 @@
|
|||||||
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
||||||
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
|
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
|
||||||
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
|
<DependentUpon>AddEditPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
|
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
|
||||||
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
|
<DependentUpon>ViewPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
||||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||||
@@ -122,24 +122,14 @@
|
|||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
||||||
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
|
||||||
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Behaviors\" />
|
<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="Controls\AccountSwitchingOverlay\" />
|
||||||
<Folder Include="Utilities\AccountManagement\" />
|
<Folder Include="Utilities\AccountManagement\" />
|
||||||
<Folder Include="Controls\DateTime\" />
|
<Folder Include="Controls\DateTime\" />
|
||||||
<Folder Include="Controls\IconLabelButton\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -422,15 +412,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Behaviors\" />
|
<None Remove="Behaviors\" />
|
||||||
<None Remove="Xamarin.CommunityToolkit" />
|
<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="Controls\AccountSwitchingOverlay\" />
|
||||||
<None Remove="Utilities\AccountManagement\" />
|
<None Remove="Utilities\AccountManagement\" />
|
||||||
<None Remove="Controls\DateTime\" />
|
<None Remove="Controls\DateTime\" />
|
||||||
<None Remove="Controls\IconLabelButton\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
@@ -8,11 +7,9 @@ using Bit.App.Resources;
|
|||||||
using Bit.App.Services;
|
using Bit.App.Services;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Response;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -28,15 +25,13 @@ namespace Bit.App
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
|
private readonly IStorageService _secureStorageService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IFileService _fileService;
|
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
|
||||||
private static bool _pendingCheckPasswordlessLoginRequests;
|
|
||||||
private static object _processingLoginRequestLock = new object();
|
|
||||||
|
|
||||||
public App(AppOptions appOptions)
|
public App(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
@@ -52,10 +47,10 @@ namespace Bit.App
|
|||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_fileService = ServiceContainer.Resolve<IFileService>();
|
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
|
||||||
|
|
||||||
_accountsManager.Init(() => Options, this);
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
@@ -145,14 +140,6 @@ namespace Bit.App
|
|||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (message.Command == Constants.PasswordlessLoginRequestKey || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
|
||||||
{
|
|
||||||
lock (_processingLoginRequestLock)
|
|
||||||
{
|
|
||||||
// lock doesn't allow for async execution
|
|
||||||
CheckPasswordlessLoginRequestsAsync().Wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -161,91 +148,11 @@ namespace Bit.App
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckPasswordlessLoginRequestsAsync()
|
|
||||||
{
|
|
||||||
if (!_isResumed)
|
|
||||||
{
|
|
||||||
_pendingCheckPasswordlessLoginRequests = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_pendingCheckPasswordlessLoginRequests = false;
|
|
||||||
if (await _vaultTimeoutService.IsLockedAsync())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
|
||||||
if (notification == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await CheckShouldSwitchActiveUserAsync(notification))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay to wait for the vault page to appear
|
|
||||||
await Task.Delay(2000);
|
|
||||||
// if there is a request modal opened ignore all incoming requests
|
|
||||||
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> CheckShouldSwitchActiveUserAsync(PasswordlessRequestNotification notification)
|
|
||||||
{
|
|
||||||
var activeUserId = await _stateService.GetActiveUserIdAsync();
|
|
||||||
if (notification.UserId == activeUserId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
|
|
||||||
if (result == AppResources.Ok)
|
|
||||||
{
|
|
||||||
await _stateService.SetActiveUserAsync(notification.UserId);
|
|
||||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AppOptions Options { get; private set; }
|
public AppOptions Options { get; private set; }
|
||||||
|
|
||||||
protected async override void OnStart()
|
protected async override void OnStart()
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||||
_isResumed = true;
|
|
||||||
await ClearCacheIfNeededAsync();
|
await ClearCacheIfNeededAsync();
|
||||||
Prime();
|
Prime();
|
||||||
if (string.IsNullOrWhiteSpace(Options.Uri))
|
if (string.IsNullOrWhiteSpace(Options.Uri))
|
||||||
@@ -257,10 +164,6 @@ namespace Bit.App
|
|||||||
SyncIfNeeded();
|
SyncIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_pendingCheckPasswordlessLoginRequests)
|
|
||||||
{
|
|
||||||
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
|
||||||
}
|
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
@@ -293,10 +196,6 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||||
_isResumed = true;
|
_isResumed = true;
|
||||||
if (_pendingCheckPasswordlessLoginRequests)
|
|
||||||
{
|
|
||||||
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
|
||||||
}
|
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
ResumedAsync().FireAndForget();
|
||||||
@@ -346,7 +245,7 @@ namespace Bit.App
|
|||||||
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
||||||
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
||||||
{
|
{
|
||||||
var task = Task.Run(() => _fileService.ClearCacheAsync());
|
var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,20 +330,20 @@ namespace Bit.App
|
|||||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||||
if (topPage is NavigationPage navPage)
|
if (topPage is NavigationPage navPage)
|
||||||
{
|
{
|
||||||
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
|
if (navPage.CurrentPage is ViewPage viewPage)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "view",
|
Page = "view",
|
||||||
CipherId = cipherDetailsPage.ViewModel.CipherId
|
CipherId = viewPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
|
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "edit",
|
Page = "edit",
|
||||||
CipherId = cipherAddEditPage.ViewModel.CipherId
|
CipherId = addEditPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,7 +378,7 @@ namespace Bit.App
|
|||||||
Current.MainPage = new TabsPage(Options);
|
Current.MainPage = new TabsPage(Options);
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AddEditCipher:
|
case NavigationTarget.AddEditCipher:
|
||||||
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AutofillCiphers:
|
case NavigationTarget.AutofillCiphers:
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
AccountView = accountView;
|
AccountView = accountView;
|
||||||
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||||
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email);
|
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountView AccountView
|
public AccountView AccountView
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -11,8 +10,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public class AvatarImageSource : StreamImageSource
|
public class AvatarImageSource : StreamImageSource
|
||||||
{
|
{
|
||||||
private readonly string _text;
|
private string _data;
|
||||||
private readonly string _id;
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
@@ -23,21 +21,20 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
if (obj is AvatarImageSource avatar)
|
if (obj is AvatarImageSource avatar)
|
||||||
{
|
{
|
||||||
return avatar._id == _id && avatar._text == _text;
|
return avatar._data == _data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.Equals(obj);
|
return base.Equals(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
|
public override int GetHashCode() => _data?.GetHashCode() ?? -1;
|
||||||
|
|
||||||
public AvatarImageSource(string userId = null, string name = null, string email = null)
|
public AvatarImageSource(string name = null, string email = null)
|
||||||
{
|
{
|
||||||
_id = userId;
|
_data = name;
|
||||||
_text = name;
|
if (string.IsNullOrWhiteSpace(_data))
|
||||||
if (string.IsNullOrWhiteSpace(_text))
|
|
||||||
{
|
{
|
||||||
_text = email;
|
_data = email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,24 +52,24 @@ namespace Bit.App.Controls
|
|||||||
private Stream Draw()
|
private Stream Draw()
|
||||||
{
|
{
|
||||||
string chars;
|
string chars;
|
||||||
string upperCaseText = null;
|
string upperData = null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_text))
|
if (string.IsNullOrEmpty(_data))
|
||||||
{
|
{
|
||||||
chars = "..";
|
chars = "..";
|
||||||
}
|
}
|
||||||
else if (_text?.Length > 1)
|
else if (_data?.Length > 1)
|
||||||
{
|
{
|
||||||
upperCaseText = _text.ToUpper();
|
upperData = _data.ToUpper();
|
||||||
chars = GetFirstLetters(upperCaseText, 2);
|
chars = GetFirstLetters(upperData, 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
chars = upperCaseText = _text.ToUpper();
|
chars = upperData = _data.ToUpper();
|
||||||
}
|
}
|
||||||
|
|
||||||
var bgColor = CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
|
var bgColor = StringToColor(upperData);
|
||||||
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
|
var textColor = Color.White;
|
||||||
var size = 50;
|
var size = 50;
|
||||||
|
|
||||||
using (var bitmap = new SKBitmap(size * 2,
|
using (var bitmap = new SKBitmap(size * 2,
|
||||||
@@ -88,7 +85,7 @@ namespace Bit.App.Controls
|
|||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Color = SKColor.Parse(bgColor)
|
Color = SKColor.Parse(bgColor.ToHex())
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||||
@@ -100,7 +97,7 @@ namespace Bit.App.Controls
|
|||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Color = SKColor.Parse(bgColor)
|
Color = SKColor.Parse(bgColor.ToHex())
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||||
@@ -111,7 +108,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
Color = SKColor.Parse(textColor),
|
Color = SKColor.Parse(textColor.ToHex()),
|
||||||
TextSize = textSize,
|
TextSize = textSize,
|
||||||
TextAlign = SKTextAlign.Center,
|
TextAlign = SKTextAlign.Center,
|
||||||
Typeface = typeface
|
Typeface = typeface
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public interface IAvatarImageSourcePool
|
public interface IAvatarImageSourcePool
|
||||||
{
|
{
|
||||||
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email);
|
AvatarImageSource GetOrCreateAvatar(string name, string email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
||||||
|
|
||||||
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email)
|
public AvatarImageSource GetOrCreateAvatar(string name, string email)
|
||||||
{
|
{
|
||||||
var key = $"{userId}{name}{email}";
|
var key = $"{name}{email}";
|
||||||
if (!_cache.TryGetValue(key, out var avatar))
|
if (!_cache.TryGetValue(key, out var avatar))
|
||||||
{
|
{
|
||||||
avatar = new AvatarImageSource(userId, name, email);
|
avatar = new AvatarImageSource(name, email);
|
||||||
if (!_cache.TryAdd(key, avatar)
|
if (!_cache.TryAdd(key, avatar)
|
||||||
&&
|
&&
|
||||||
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Bit.App.Effects;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -17,8 +16,6 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Effects.Add(new RemoveFontPaddingEffect());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Bit.App.Effects;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -18,8 +17,6 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Effects.Add(new RemoveFontPaddingEffect());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Controls.IconLabelButton"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
x:Name="_iconLabelButton"
|
|
||||||
HeightRequest="45"
|
|
||||||
Padding="1"
|
|
||||||
StyleClass="btn-icon-secondary"
|
|
||||||
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
|
|
||||||
HasShadow="False">
|
|
||||||
<Frame.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
|
|
||||||
</Frame.GestureRecognizers>
|
|
||||||
<Frame
|
|
||||||
Margin="0"
|
|
||||||
Padding="0"
|
|
||||||
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
|
|
||||||
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
|
|
||||||
IsClippedToBounds="True"
|
|
||||||
HasShadow="False">
|
|
||||||
<StackLayout
|
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalOptions="Center">
|
|
||||||
<controls:IconLabel
|
|
||||||
VerticalOptions="Center"
|
|
||||||
HorizontalTextAlignment="Center"
|
|
||||||
FontSize="Large"
|
|
||||||
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
|
||||||
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
|
|
||||||
</controls:IconLabel>
|
|
||||||
<Label
|
|
||||||
VerticalOptions="Center"
|
|
||||||
HorizontalTextAlignment="Center"
|
|
||||||
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
|
||||||
FontSize="Medium"
|
|
||||||
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
|
|
||||||
</StackLayout>
|
|
||||||
</Frame>
|
|
||||||
</Frame>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.Core.Models.Domain;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Forms.Xaml;
|
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
|
||||||
{
|
|
||||||
public partial class IconLabelButton : Frame
|
|
||||||
{
|
|
||||||
public static readonly BindableProperty IconProperty = BindableProperty.Create(
|
|
||||||
nameof(Icon), typeof(string), typeof(IconLabelButton));
|
|
||||||
|
|
||||||
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
|
|
||||||
nameof(Label), typeof(string), typeof(IconLabelButton));
|
|
||||||
|
|
||||||
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
|
||||||
nameof(ButtonCommand), typeof(Command), typeof(IconLabelButton));
|
|
||||||
|
|
||||||
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
|
|
||||||
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
|
||||||
|
|
||||||
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
|
|
||||||
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
|
||||||
|
|
||||||
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
|
|
||||||
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
|
||||||
|
|
||||||
public IconLabelButton()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Icon
|
|
||||||
{
|
|
||||||
get => GetValue(IconProperty) as string;
|
|
||||||
set => SetValue(IconProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Label
|
|
||||||
{
|
|
||||||
get => GetValue(LabelProperty) as string;
|
|
||||||
set => SetValue(LabelProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICommand ButtonCommand
|
|
||||||
{
|
|
||||||
get => GetValue(ButtonCommandProperty) as ICommand;
|
|
||||||
set => SetValue(ButtonCommandProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color IconLabelColor
|
|
||||||
{
|
|
||||||
get { return (Color)GetValue(IconLabelColorProperty); }
|
|
||||||
set { SetValue(IconLabelColorProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color IconLabelBackgroundColor
|
|
||||||
{
|
|
||||||
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
|
|
||||||
set { SetValue(IconLabelBackgroundColorProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color IconLabelBorderColor
|
|
||||||
{
|
|
||||||
get { return (Color)GetValue(IconLabelBorderColorProperty); }
|
|
||||||
set { SetValue(IconLabelBorderColorProperty, value); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
using System;
|
using System.Collections;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
[Obsolete]
|
|
||||||
public class RepeaterView : StackLayout
|
public class RepeaterView : StackLayout
|
||||||
{
|
{
|
||||||
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
|
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Effects
|
|
||||||
{
|
|
||||||
public class NoEmojiKeyboardEffect : RoutingEffect
|
|
||||||
{
|
|
||||||
public NoEmojiKeyboardEffect()
|
|
||||||
: base("Bitwarden.NoEmojiKeyboardEffect")
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Effects
|
|
||||||
{
|
|
||||||
public class RemoveFontPaddingEffect : RoutingEffect
|
|
||||||
{
|
|
||||||
public RemoveFontPaddingEffect()
|
|
||||||
: base("Bitwarden.RemoveFontPaddingEffect")
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using Bit.App.Lists.ItemViewModels.CustomFields;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.DataTemplateSelectors
|
|
||||||
{
|
|
||||||
public class CustomFieldItemTemplateSelector : DataTemplateSelector
|
|
||||||
{
|
|
||||||
public DataTemplate TextTemplate { get; set; }
|
|
||||||
public DataTemplate BooleanTemplate { get; set; }
|
|
||||||
public DataTemplate LinkedTemplate { get; set; }
|
|
||||||
public DataTemplate HiddenTemplate { get; set; }
|
|
||||||
|
|
||||||
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
|
||||||
{
|
|
||||||
switch (item)
|
|
||||||
{
|
|
||||||
case BooleanCustomFieldItemViewModel _:
|
|
||||||
return BooleanTemplate;
|
|
||||||
case LinkedCustomFieldItemViewModel _:
|
|
||||||
return LinkedTemplate;
|
|
||||||
case HiddenCustomFieldItemViewModel _:
|
|
||||||
return HiddenTemplate;
|
|
||||||
default:
|
|
||||||
return TextTemplate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<StackLayout
|
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.BooleanCustomFieldItemLayout"
|
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
|
||||||
x:DataType="cfvm:BooleanCustomFieldItemViewModel"
|
|
||||||
Spacing="0" Padding="0">
|
|
||||||
<StackLayout.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
|
||||||
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
|
||||||
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</StackLayout.Resources>
|
|
||||||
<Grid
|
|
||||||
StyleClass="box-row"
|
|
||||||
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Label
|
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
<Label
|
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
VerticalOptions="FillAndExpand"
|
|
||||||
VerticalTextAlignment="Center"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.RowSpan="2" />
|
|
||||||
<controls:IconLabel
|
|
||||||
Text="{Binding BooleanValue, Mode=OneWay, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Checkbox}}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="0, 5, 0, 0"
|
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
<Switch
|
|
||||||
IsToggled="{Binding BooleanValue}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.RowSpan="2" />
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
|
||||||
Command="{Binding FieldOptionsCommand}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="2"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView StyleClass="box-row-separator" />
|
|
||||||
</StackLayout>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
|
||||||
{
|
|
||||||
public partial class BooleanCustomFieldItemLayout : StackLayout
|
|
||||||
{
|
|
||||||
public BooleanCustomFieldItemLayout()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.HiddenCustomFieldItemLayout"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
|
||||||
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
|
||||||
x:DataType="cfvm:HiddenCustomFieldItemViewModel"
|
|
||||||
Spacing="0" Padding="0">
|
|
||||||
<StackLayout.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
|
||||||
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
|
||||||
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</StackLayout.Resources>
|
|
||||||
<Grid
|
|
||||||
StyleClass="box-row"
|
|
||||||
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Label
|
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0" />
|
|
||||||
<StackLayout
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing, Converter={StaticResource inverseBool}}">
|
|
||||||
<controls:MonoLabel
|
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
IsVisible="{Binding ShowHiddenValue}" />
|
|
||||||
<controls:MonoLabel
|
|
||||||
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
|
||||||
</StackLayout>
|
|
||||||
<controls:MonoEntry
|
|
||||||
Text="{Binding Field.Value}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
|
||||||
IsEnabled="{Binding ShowViewHidden}"
|
|
||||||
IsSpellCheckEnabled="False"
|
|
||||||
IsTextPredictionEnabled="False">
|
|
||||||
<Entry.Keyboard>
|
|
||||||
<Keyboard x:FactoryMethod="Create">
|
|
||||||
<x:Arguments>
|
|
||||||
<KeyboardFlags>None</KeyboardFlags>
|
|
||||||
</x:Arguments>
|
|
||||||
</Keyboard>
|
|
||||||
</Entry.Keyboard>
|
|
||||||
</controls:MonoEntry>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowHiddenValue, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
|
||||||
Command="{Binding ToggleHiddenValueCommand}"
|
|
||||||
IsVisible="{Binding ShowViewHidden}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
|
||||||
Command="{Binding CopyFieldCommand}"
|
|
||||||
IsVisible="{Binding ShowCopyButton}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="2"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Copy}" />
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
|
||||||
Command="{Binding FieldOptionsCommand}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="2"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
</StackLayout>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
|
||||||
{
|
|
||||||
public partial class HiddenCustomFieldItemLayout : StackLayout
|
|
||||||
{
|
|
||||||
public HiddenCustomFieldItemLayout()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.LinkedCustomFieldItemLayout"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
|
||||||
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
|
||||||
x:DataType="cfvm:LinkedCustomFieldItemViewModel"
|
|
||||||
Spacing="0" Padding="0">
|
|
||||||
<StackLayout.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
|
||||||
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</StackLayout.Resources>
|
|
||||||
<Grid
|
|
||||||
StyleClass="box-row"
|
|
||||||
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Label
|
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0" />
|
|
||||||
<controls:IconLabel
|
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
<StackLayout
|
|
||||||
StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding IsEditing}">
|
|
||||||
<Picker
|
|
||||||
x:Name="_linkedFieldOptionPicker"
|
|
||||||
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
|
||||||
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
|
||||||
ItemDisplayBinding="{Binding Key}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
</StackLayout>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
|
||||||
Command="{Binding FieldOptionsCommand}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
</StackLayout>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
|
||||||
{
|
|
||||||
public partial class LinkedCustomFieldItemLayout : StackLayout
|
|
||||||
{
|
|
||||||
public LinkedCustomFieldItemLayout()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.TextCustomFieldItemLayout"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
|
||||||
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
|
||||||
x:DataType="cfvm:TextCustomFieldItemViewModel"
|
|
||||||
Spacing="0" Padding="0">
|
|
||||||
<StackLayout.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
|
||||||
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</StackLayout.Resources>
|
|
||||||
<Grid
|
|
||||||
StyleClass="box-row"
|
|
||||||
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Label
|
|
||||||
Text="{Binding Field.Name, Mode=OneWay}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="0" />
|
|
||||||
<Label
|
|
||||||
Text="{Binding ValueText, Mode=OneWay}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
<Entry
|
|
||||||
Text="{Binding Field.Value}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
IsVisible="{Binding IsEditing}" />
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
|
||||||
Command="{Binding CopyFieldCommand}"
|
|
||||||
IsVisible="{Binding ShowCopyButton}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Copy}" />
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
|
||||||
Command="{Binding FieldOptionsCommand}"
|
|
||||||
IsVisible="{Binding IsEditing}"
|
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.RowSpan="2"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
|
||||||
</StackLayout>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
|
||||||
{
|
|
||||||
public partial class TextCustomFieldItemLayout : StackLayout
|
|
||||||
{
|
|
||||||
public TextCustomFieldItemLayout()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using System.Windows.Input;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
protected FieldView _field;
|
|
||||||
protected bool _isEditing;
|
|
||||||
private string[] _additionalFieldProperties = new string[]
|
|
||||||
{
|
|
||||||
nameof(ValueText),
|
|
||||||
nameof(ShowCopyButton)
|
|
||||||
};
|
|
||||||
|
|
||||||
public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
_field = field;
|
|
||||||
_isEditing = isEditing;
|
|
||||||
FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
public FieldView Field
|
|
||||||
{
|
|
||||||
get => _field;
|
|
||||||
set => SetProperty(ref _field, value,
|
|
||||||
additionalPropertyNames: new string[]
|
|
||||||
{
|
|
||||||
nameof(ValueText),
|
|
||||||
nameof(ShowCopyButton),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEditing => _isEditing;
|
|
||||||
|
|
||||||
public virtual bool ShowCopyButton => false;
|
|
||||||
|
|
||||||
public virtual string ValueText => _field.Value;
|
|
||||||
|
|
||||||
public ICommand FieldOptionsCommand { get; }
|
|
||||||
|
|
||||||
public void TriggerFieldChanged()
|
|
||||||
{
|
|
||||||
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Windows.Input;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
|
|
||||||
: base(field, isEditing, fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool BooleanValue
|
|
||||||
{
|
|
||||||
get => bool.TryParse(Field.Value, out var boolVal) && boolVal;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
Field.Value = value.ToString().ToLower();
|
|
||||||
TriggerPropertyChanged(nameof(BooleanValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public interface ICustomFieldItemFactory
|
|
||||||
{
|
|
||||||
ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
|
|
||||||
bool isEditing,
|
|
||||||
CipherView cipher,
|
|
||||||
IPasswordPromptable passwordPromptable,
|
|
||||||
ICommand copyFieldCommand,
|
|
||||||
ICommand fieldOptionsCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CustomFieldItemFactory : ICustomFieldItemFactory
|
|
||||||
{
|
|
||||||
readonly II18nService _i18nService;
|
|
||||||
readonly IEventService _eventService;
|
|
||||||
|
|
||||||
public CustomFieldItemFactory(II18nService i18nService, IEventService eventService)
|
|
||||||
{
|
|
||||||
_i18nService = i18nService;
|
|
||||||
_eventService = eventService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
|
|
||||||
bool isEditing,
|
|
||||||
CipherView cipher,
|
|
||||||
IPasswordPromptable passwordPromptable,
|
|
||||||
ICommand copyFieldCommand,
|
|
||||||
ICommand fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
switch (field.Type)
|
|
||||||
{
|
|
||||||
case FieldType.Text:
|
|
||||||
return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand);
|
|
||||||
case FieldType.Boolean:
|
|
||||||
return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand);
|
|
||||||
case FieldType.Hidden:
|
|
||||||
return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand);
|
|
||||||
case FieldType.Linked:
|
|
||||||
return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService);
|
|
||||||
default:
|
|
||||||
throw new NotImplementedException("There is no custom field item for field type " + field.Type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
private readonly CipherView _cipher;
|
|
||||||
private readonly IPasswordPromptable _passwordPromptable;
|
|
||||||
private readonly IEventService _eventService;
|
|
||||||
private bool _showHiddenValue;
|
|
||||||
|
|
||||||
public HiddenCustomFieldItemViewModel(FieldView field,
|
|
||||||
bool isEditing,
|
|
||||||
ICommand fieldOptionsCommand,
|
|
||||||
CipherView cipher,
|
|
||||||
IPasswordPromptable passwordPromptable,
|
|
||||||
IEventService eventService,
|
|
||||||
ICommand copyFieldCommand)
|
|
||||||
: base(field, isEditing, fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
_cipher = cipher;
|
|
||||||
_passwordPromptable = passwordPromptable;
|
|
||||||
_eventService = eventService;
|
|
||||||
|
|
||||||
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
|
|
||||||
ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, (Func<bool>)null, ex =>
|
|
||||||
{
|
|
||||||
#if !FDROID
|
|
||||||
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
|
||||||
#endif
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICommand CopyFieldCommand { get; }
|
|
||||||
|
|
||||||
public ICommand ToggleHiddenValueCommand { get; set; }
|
|
||||||
|
|
||||||
public bool ShowHiddenValue
|
|
||||||
{
|
|
||||||
get => _showHiddenValue;
|
|
||||||
set => SetProperty(ref _showHiddenValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField);
|
|
||||||
|
|
||||||
public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value);
|
|
||||||
|
|
||||||
public async Task ToggleHiddenValueAsync()
|
|
||||||
{
|
|
||||||
if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowHiddenValue = !ShowHiddenValue;
|
|
||||||
if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null))
|
|
||||||
{
|
|
||||||
await _eventService.CollectAsync(
|
|
||||||
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public interface ICustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
FieldView Field { get; set; }
|
|
||||||
|
|
||||||
bool ShowCopyButton { get; }
|
|
||||||
|
|
||||||
void TriggerFieldChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
private readonly CipherView _cipher;
|
|
||||||
private readonly II18nService _i18nService;
|
|
||||||
private int _linkedFieldOptionSelectedIndex;
|
|
||||||
|
|
||||||
public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService)
|
|
||||||
: base(field, isEditing, fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
_cipher = cipher;
|
|
||||||
_i18nService = i18nService;
|
|
||||||
|
|
||||||
LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue
|
|
||||||
? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (isEditing && Field.LinkedId is null)
|
|
||||||
{
|
|
||||||
field.LinkedId = LinkedFieldOptions[0].Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ValueText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
|
|
||||||
return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int LinkedFieldOptionSelectedIndex
|
|
||||||
{
|
|
||||||
get => _linkedFieldOptionSelectedIndex;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
|
|
||||||
{
|
|
||||||
LinkedFieldValueChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
|
|
||||||
{
|
|
||||||
get => _cipher.LinkedFieldOptions
|
|
||||||
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkedFieldValueChanged()
|
|
||||||
{
|
|
||||||
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
|
|
||||||
{
|
|
||||||
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
|
|
||||||
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System.Windows.Input;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
|
||||||
{
|
|
||||||
public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
|
||||||
{
|
|
||||||
public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand)
|
|
||||||
: base(field, isEditing, fieldOptionsCommand)
|
|
||||||
{
|
|
||||||
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value);
|
|
||||||
|
|
||||||
public ICommand CopyFieldCommand { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
namespace Bit.App.Models
|
|
||||||
{
|
|
||||||
public abstract class BaseNotificationData
|
|
||||||
{
|
|
||||||
public abstract string Type { get; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PasswordlessNotificationData : BaseNotificationData
|
|
||||||
{
|
|
||||||
public const string TYPE = "passwordlessNotificationData";
|
|
||||||
|
|
||||||
public override string Type => TYPE;
|
|
||||||
|
|
||||||
public int TimeoutInMinutes { get; set; }
|
|
||||||
|
|
||||||
public string UserEmail { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
bool IsUrlValid(string url)
|
bool IsUrlValid(string url)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
|
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsUrlValid(BaseUrl)
|
return IsUrlValid(BaseUrl)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
|
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Xamarin.Forms;
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -6,12 +7,11 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
private HintPageViewModel _vm;
|
private HintPageViewModel _vm;
|
||||||
|
|
||||||
public HintPage(string email = null)
|
public HintPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HintPageViewModel;
|
_vm = BindingContext as HintPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.Email = email;
|
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ToolbarItems.RemoveAt(0);
|
ToolbarItems.RemoveAt(0);
|
||||||
@@ -24,6 +24,14 @@ namespace Bit.App.Pages
|
|||||||
RequestFocus(_email);
|
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)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -14,33 +13,19 @@ namespace Bit.App.Pages
|
|||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
private readonly ILogger _logger;
|
|
||||||
private string _email;
|
|
||||||
|
|
||||||
public HintPageViewModel()
|
public HintPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>();
|
|
||||||
|
|
||||||
PageTitle = AppResources.PasswordHint;
|
PageTitle = AppResources.PasswordHint;
|
||||||
SubmitCommand = new AsyncCommand(SubmitAsync,
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
onException: ex =>
|
|
||||||
{
|
|
||||||
_logger.Exception(ex);
|
|
||||||
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
|
||||||
},
|
|
||||||
allowsMultipleExecutions: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
|
public string Email { get; set; }
|
||||||
public string Email
|
|
||||||
{
|
|
||||||
get => _email;
|
|
||||||
set => SetProperty(ref _email, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
@@ -52,14 +37,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
{
|
{
|
||||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Email.Contains("@"))
|
if (!Email.Contains("@"))
|
||||||
{
|
{
|
||||||
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
await _apiService.PostPasswordHintAsync(
|
await _apiService.PostPasswordHintAsync(
|
||||||
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||||
await Page.Navigation.PopModalAsync();
|
await Page.Navigation.PopModalAsync();
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}" />
|
||||||
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
|
||||||
<ToolbarItem
|
<ToolbarItem
|
||||||
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
@@ -33,66 +32,30 @@
|
|||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="0" Padding="10, 5">
|
||||||
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
|
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
|
||||||
<Image
|
<Image
|
||||||
x:Name="_logo"
|
x:Name="_logo"
|
||||||
Source="logo.png"
|
Source="logo.png"
|
||||||
VerticalOptions="Center" />
|
VerticalOptions="Center" />
|
||||||
<Label Text="{u:I18n LoginOrCreateNewAccount}"
|
<Label Text="{u:I18n LoginOrCreateNewAccount}"
|
||||||
StyleClass="text-lg"
|
StyleClass="text-lg"
|
||||||
HorizontalTextAlignment="Center"/>
|
HorizontalTextAlignment="Center">
|
||||||
<StackLayout
|
</Label>
|
||||||
StyleClass="box-row">
|
<StackLayout Spacing="5">
|
||||||
<Label
|
<Button Text="{u:I18n LogIn}"
|
||||||
Text="{u:I18n EmailAddress}"
|
StyleClass="btn-primary"
|
||||||
StyleClass="box-label" />
|
Clicked="LogIn_Clicked" />
|
||||||
<Entry
|
<Button Text="{u:I18n CreateAccount}"
|
||||||
x:Name="_email"
|
Clicked="Register_Clicked" />
|
||||||
Text="{Binding Email}"
|
<Button Text="{u:I18n LogInSso}"
|
||||||
Keyboard="Email"
|
Clicked="LogInSso_Clicked" />
|
||||||
StyleClass="box-value">
|
<Button Text="{u:I18n Cancel}"
|
||||||
<VisualStateManager.VisualStateGroups>
|
IsVisible="{Binding ShowCancelButton}"
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
Margin="0,10,0,0"
|
||||||
<VisualState x:Name="Disabled">
|
Clicked="Cancel_Clicked" />
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateManager.VisualStateGroups>
|
|
||||||
</Entry>
|
|
||||||
<StackLayout
|
|
||||||
Orientation="Horizontal"
|
|
||||||
Margin="0, 16, 0 ,0">
|
|
||||||
<StackLayout.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer
|
|
||||||
Command="{Binding RememberEmailCommand}" />
|
|
||||||
</StackLayout.GestureRecognizers>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n RememberMe}"
|
|
||||||
StyleClass="text-sm"
|
|
||||||
HorizontalOptions="FillAndExpand"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
VerticalTextAlignment="Center"/>
|
|
||||||
<Switch
|
|
||||||
Scale="0.8"
|
|
||||||
IsToggled="{Binding RememberEmail}"
|
|
||||||
VerticalOptions="Center"/>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Button Text="{u:I18n Continue}"
|
|
||||||
StyleClass="btn-primary"
|
|
||||||
IsEnabled="{Binding CanContinue}"
|
|
||||||
Command="{Binding ContinueCommand}" />
|
|
||||||
|
|
||||||
<Label FormattedText="{Binding CreateAccountText}"
|
|
||||||
Margin="0, 10"
|
|
||||||
StyleClass="box-footer-label">
|
|
||||||
<Label.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
|
||||||
</Label.GestureRecognizers>
|
|
||||||
</Label>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ContentPage.Resources>
|
</ContentPage.Resources>
|
||||||
|
|||||||
@@ -10,35 +10,28 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
public partial class HomePage : BaseContentPage
|
public partial class HomePage : BaseContentPage
|
||||||
{
|
{
|
||||||
private bool _checkRememberedEmail;
|
|
||||||
private readonly HomeViewModel _vm;
|
private readonly HomeViewModel _vm;
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
public HomePage(AppOptions appOptions = null, bool checkRememberedEmail = true)
|
public HomePage(AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HomeViewModel;
|
_vm = BindingContext as HomeViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.CheckHasRememberedEmail = checkRememberedEmail;
|
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
|
||||||
_vm.ShowCancelButton = _appOptions?.IosExtension ?? false;
|
|
||||||
_vm.StartLoginAction = async () => await StartLoginAsync();
|
|
||||||
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
||||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||||
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
||||||
_vm.CloseAction = async () =>
|
|
||||||
{
|
|
||||||
await _accountListOverlay.HideAsync();
|
|
||||||
await Navigation.PopModalAsync();
|
|
||||||
};
|
|
||||||
UpdateLogo();
|
UpdateLogo();
|
||||||
|
|
||||||
if (!_vm.ShowCancelButton)
|
if (_appOptions?.IosExtension ?? false)
|
||||||
{
|
{
|
||||||
ToolbarItems.Remove(_closeButton);
|
_vm.ShowCancelButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_appOptions?.HideAccountSwitcher ?? false)
|
if (_appOptions?.HideAccountSwitcher ?? false)
|
||||||
{
|
{
|
||||||
ToolbarItems.Remove(_accountAvatar);
|
ToolbarItems.Remove(_accountAvatar);
|
||||||
@@ -71,8 +64,6 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_vm.CheckNavigateLoginStep();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
@@ -105,12 +96,28 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogIn_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
_vm.StartLoginAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task StartLoginAsync()
|
private async Task StartLoginAsync()
|
||||||
{
|
{
|
||||||
var page = new LoginPage(_vm.Email, _appOptions);
|
var page = new LoginPage(null, _appOptions);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Register_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
_vm.StartRegisterAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task StartRegisterAsync()
|
private async Task StartRegisterAsync()
|
||||||
{
|
{
|
||||||
var page = new RegisterPage(this);
|
var page = new RegisterPage(this);
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Essentials;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -18,33 +12,19 @@ namespace Bit.App.Pages
|
|||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
private bool _rememberEmail;
|
|
||||||
private string _email;
|
|
||||||
private bool _isEmailEnabled;
|
|
||||||
private bool _canLogin;
|
|
||||||
private IPlatformUtilsService _platformUtilsService;
|
|
||||||
private ILogger _logger;
|
|
||||||
|
|
||||||
public HomeViewModel()
|
public HomeViewModel()
|
||||||
{
|
{
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
var logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, logger)
|
||||||
{
|
{
|
||||||
AllowActiveAccountSelection = true
|
AllowActiveAccountSelection = true
|
||||||
};
|
};
|
||||||
RememberEmailCommand = new Command(() => RememberEmail = !RememberEmail);
|
|
||||||
ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false);
|
|
||||||
CreateAccountCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(StartRegisterAction),
|
|
||||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
|
||||||
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
|
|
||||||
onException: _logger.Exception, allowsMultipleExecutions: false);
|
|
||||||
InitAsync().FireAndForget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShowCancelButton
|
public bool ShowCancelButton
|
||||||
@@ -53,92 +33,11 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _showCancelButton, value);
|
set => SetProperty(ref _showCancelButton, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RememberEmail
|
|
||||||
{
|
|
||||||
get => _rememberEmail;
|
|
||||||
set => SetProperty(ref _rememberEmail, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Email
|
|
||||||
{
|
|
||||||
get => _email;
|
|
||||||
set => SetProperty(ref _email, value,
|
|
||||||
additionalPropertyNames: new[] { nameof(CanContinue) });
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanContinue => !string.IsNullOrEmpty(Email);
|
|
||||||
|
|
||||||
public bool CheckHasRememberedEmail { get; set; }
|
|
||||||
|
|
||||||
public FormattedString CreateAccountText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var fs = new FormattedString();
|
|
||||||
fs.Spans.Add(new Span
|
|
||||||
{
|
|
||||||
Text = $"{AppResources.NewAroundHere} "
|
|
||||||
});
|
|
||||||
fs.Spans.Add(new Span
|
|
||||||
{
|
|
||||||
Text = AppResources.CreateAccount,
|
|
||||||
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
|
|
||||||
});
|
|
||||||
return fs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
public Action StartLoginAction { get; set; }
|
public Action StartLoginAction { get; set; }
|
||||||
public Action StartRegisterAction { get; set; }
|
public Action StartRegisterAction { get; set; }
|
||||||
public Action StartSsoLoginAction { get; set; }
|
public Action StartSsoLoginAction { get; set; }
|
||||||
public Action StartEnvironmentAction { get; set; }
|
public Action StartEnvironmentAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
public Command RememberEmailCommand { get; set; }
|
|
||||||
public AsyncCommand ContinueCommand { get; }
|
|
||||||
public AsyncCommand CloseCommand { get; }
|
|
||||||
public AsyncCommand CreateAccountCommand { get; }
|
|
||||||
|
|
||||||
public async Task InitAsync()
|
|
||||||
{
|
|
||||||
Email = await _stateService.GetRememberedEmailAsync();
|
|
||||||
RememberEmail = !string.IsNullOrEmpty(Email);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CheckNavigateLoginStep()
|
|
||||||
{
|
|
||||||
if (CheckHasRememberedEmail && RememberEmail)
|
|
||||||
{
|
|
||||||
StartLoginAction();
|
|
||||||
}
|
|
||||||
CheckHasRememberedEmail = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ContinueToLoginStepAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(
|
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
|
||||||
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Email.Contains("@"))
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
|
||||||
AppResources.Ok);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _stateService.SetRememberedEmailAsync(RememberEmail ? Email : null);
|
|
||||||
StartLoginAction();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Exception(ex);
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.LoginPage"
|
x:Class="Bit.App.Pages.LoginPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
x:DataType="pages:LoginPageViewModel"
|
x:DataType="pages:LoginPageViewModel"
|
||||||
@@ -47,13 +46,32 @@
|
|||||||
Order="Secondary" />
|
Order="Secondary" />
|
||||||
|
|
||||||
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
||||||
<StackLayout Spacing="0">
|
<StackLayout Spacing="20">
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
|
<StackLayout StyleClass="box-row">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n EmailAddress}"
|
||||||
|
StyleClass="box-label" />
|
||||||
|
<Entry
|
||||||
|
x:Name="_email"
|
||||||
|
Text="{Binding Email}"
|
||||||
|
Keyboard="Email"
|
||||||
|
StyleClass="box-value">
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Entry>
|
||||||
|
</StackLayout>
|
||||||
<Grid StyleClass="box-row">
|
<Grid StyleClass="box-row">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -62,7 +80,6 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPassword}"
|
Text="{u:I18n MasterPassword}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
Padding="0, 10, 0, 0"
|
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:MonoEntry
|
<controls:MonoEntry
|
||||||
@@ -80,60 +97,21 @@
|
|||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
Command="{Binding TogglePasswordCommand}"
|
Command="{Binding TogglePasswordCommand}"
|
||||||
Grid.Row="1"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="1"
|
Grid.RowSpan="2"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||||
<Label
|
|
||||||
Text="{u:I18n GetMasterPasswordwordHint}"
|
|
||||||
StyleClass="box-footer-label"
|
|
||||||
TextColor="{DynamicResource HyperlinkColor}"
|
|
||||||
Padding="0,5,0,0"
|
|
||||||
Grid.Row="2"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.ColumnSpan="2">
|
|
||||||
<Label.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
|
||||||
</Label.GestureRecognizers>
|
|
||||||
</Label>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Padding="10, 10">
|
<StackLayout Padding="10, 0">
|
||||||
<Button x:Name="_loginWithMasterPassword"
|
<Button Text="{u:I18n LogIn}"
|
||||||
Text="{u:I18n LogInWithMasterPassword}"
|
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
Clicked="LogIn_Clicked" />
|
Clicked="LogIn_Clicked" />
|
||||||
<controls:IconLabelButton
|
<Button Text="{u:I18n Cancel}"
|
||||||
HorizontalOptions="Fill"
|
IsVisible="{Binding ShowCancelButton}"
|
||||||
VerticalOptions="CenterAndExpand"
|
Clicked="Cancel_Clicked" />
|
||||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
|
||||||
Label="{u:I18n LogInWithAnotherDevice}"
|
|
||||||
ButtonCommand="{Binding LogInCommand}"
|
|
||||||
IsVisible="False"/>
|
|
||||||
<controls:IconLabelButton
|
|
||||||
HorizontalOptions="Fill"
|
|
||||||
VerticalOptions="CenterAndExpand"
|
|
||||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
|
||||||
Label="{u:I18n LogInSso}">
|
|
||||||
<controls:IconLabelButton.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
|
||||||
</controls:IconLabelButton.GestureRecognizers>
|
|
||||||
</controls:IconLabelButton>
|
|
||||||
<Label
|
|
||||||
Text="{Binding LoggingInAsText}"
|
|
||||||
StyleClass="text-sm"
|
|
||||||
Margin="0,40,0,0"/>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n NotYou}"
|
|
||||||
StyleClass="text-md"
|
|
||||||
HorizontalOptions="Start"
|
|
||||||
TextColor="{DynamicResource HyperlinkColor}">
|
|
||||||
<Label.GestureRecognizers>
|
|
||||||
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
|
||||||
</Label.GestureRecognizers>
|
|
||||||
</Label>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -16,8 +15,6 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private bool _inputFocused;
|
private bool _inputFocused;
|
||||||
|
|
||||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
|
||||||
|
|
||||||
public LoginPage(string email = null, AppOptions appOptions = null)
|
public LoginPage(string email = null, AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
@@ -26,7 +23,6 @@ namespace Bit.App.Pages
|
|||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
|
||||||
_vm.UpdateTempPasswordAction =
|
_vm.UpdateTempPasswordAction =
|
||||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
_vm.CloseAction = async () =>
|
_vm.CloseAction = async () =>
|
||||||
@@ -34,16 +30,20 @@ namespace Bit.App.Pages
|
|||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
};
|
};
|
||||||
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
|
{
|
||||||
|
_email.IsEnabled = false;
|
||||||
if (_vm.IsEmailEnabled)
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_vm.ShowCancelButton = true;
|
_vm.ShowCancelButton = true;
|
||||||
}
|
}
|
||||||
_vm.Email = email;
|
_vm.Email = email;
|
||||||
MasterPasswordEntry = _masterPassword;
|
MasterPasswordEntry = _masterPassword;
|
||||||
|
|
||||||
|
_email.ReturnType = ReturnType.Next;
|
||||||
|
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_moreItem);
|
ToolbarItems.Add(_moreItem);
|
||||||
@@ -53,7 +53,7 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_getPasswordHint);
|
ToolbarItems.Add(_getPasswordHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
|
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_removeAccount);
|
ToolbarItems.Add(_removeAccount);
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ namespace Bit.App.Pages
|
|||||||
await _vm.InitAsync();
|
await _vm.InitAsync();
|
||||||
if (!_inputFocused)
|
if (!_inputFocused)
|
||||||
{
|
{
|
||||||
RequestFocus(_masterPassword);
|
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
|
||||||
_inputFocused = true;
|
_inputFocused = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,29 +110,15 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
|
await _vm.LogInAsync(true, _email.IsEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogInSSO_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
_vm.StartSsoLoginAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartSsoLoginAsync()
|
|
||||||
{
|
|
||||||
var page = new LoginSsoPage(_appOptions);
|
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Hint_Clicked(object sender, EventArgs e)
|
private void Hint_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
_vm.ShowMasterPasswordHintAsync().FireAndForget();
|
Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,16 +139,26 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void More_Clicked(object sender, EventArgs e)
|
private async void More_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
try
|
await _accountListOverlay.HideAsync();
|
||||||
|
if (!DoOnce())
|
||||||
{
|
{
|
||||||
await _accountListOverlay.HideAsync();
|
return;
|
||||||
_vm.MoreCommand.Execute(null);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
var buttons = _email.IsEnabled ? new[] { AppResources.GetPasswordHint }
|
||||||
|
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
||||||
|
var selection = await DisplayActionSheet(AppResources.Options,
|
||||||
|
AppResources.Cancel, null, buttons);
|
||||||
|
|
||||||
|
if (selection == AppResources.GetPasswordHint)
|
||||||
{
|
{
|
||||||
_logger.Value.Exception(ex);
|
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.RemoveAccount)
|
||||||
|
{
|
||||||
|
await _vm.RemoveAccountAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Models;
|
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -27,14 +23,11 @@ namespace Bit.App.Pages
|
|||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IApiService _apiService;
|
|
||||||
private readonly IAppIdService _appIdService;
|
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
private bool _isEmailEnabled;
|
|
||||||
private bool _isKnownDevice;
|
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -47,13 +40,10 @@ namespace Bit.App.Pages
|
|||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>();
|
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>();
|
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
LogInCommand = new Command(async () => await LogInAsync());
|
LogInCommand = new Command(async () => await LogInAsync());
|
||||||
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
{
|
{
|
||||||
@@ -82,11 +72,7 @@ namespace Bit.App.Pages
|
|||||||
public string Email
|
public string Email
|
||||||
{
|
{
|
||||||
get => _email;
|
get => _email;
|
||||||
set => SetProperty(ref _email, value,
|
set => SetProperty(ref _email, value);
|
||||||
additionalPropertyNames: new string[]
|
|
||||||
{
|
|
||||||
nameof(LoggingInAsText)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MasterPassword
|
public string MasterPassword
|
||||||
@@ -95,32 +81,15 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _masterPassword, value);
|
set => SetProperty(ref _masterPassword, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEmailEnabled
|
|
||||||
{
|
|
||||||
get => _isEmailEnabled;
|
|
||||||
set => SetProperty(ref _isEmailEnabled, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsKnownDevice
|
|
||||||
{
|
|
||||||
get => _isKnownDevice;
|
|
||||||
set => SetProperty(ref _isKnownDevice, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsIosExtension { get; set; }
|
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
public ICommand MoreCommand { get; internal set; }
|
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action LogInSuccessAction { get; set; }
|
public Action LogInSuccessAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
public Action StartSsoLoginAction { get; set; }
|
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
|
||||||
protected override II18nService i18nService => _i18nService;
|
protected override II18nService i18nService => _i18nService;
|
||||||
@@ -130,14 +99,10 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
{
|
{
|
||||||
Email = await _stateService.GetRememberedEmailAsync();
|
Email = await _stateService.GetRememberedEmailAsync();
|
||||||
}
|
}
|
||||||
var deviceIdentifier = await _appIdService.GetAppIdAsync();
|
|
||||||
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, deviceIdentifier);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
|
public async Task LogInAsync(bool showLoading = true, bool checkForExistingAccount = false)
|
||||||
@@ -192,6 +157,7 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
||||||
|
await _stateService.SetRememberedEmailAsync(Email);
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
|
||||||
if (response.CaptchaNeeded)
|
if (response.CaptchaNeeded)
|
||||||
@@ -235,33 +201,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MoreAsync()
|
|
||||||
{
|
|
||||||
var buttons = IsEmailEnabled
|
|
||||||
? new[] { AppResources.GetPasswordHint }
|
|
||||||
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
|
||||||
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
|
||||||
|
|
||||||
if (selection == AppResources.GetPasswordHint)
|
|
||||||
{
|
|
||||||
await ShowMasterPasswordHintAsync();
|
|
||||||
}
|
|
||||||
else if (selection == AppResources.RemoveAccount)
|
|
||||||
{
|
|
||||||
await RemoveAccountAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ShowMasterPasswordHintAsync()
|
|
||||||
{
|
|
||||||
var hintNavigationPage = new NavigationPage(new HintPage(Email));
|
|
||||||
if (IsIosExtension)
|
|
||||||
{
|
|
||||||
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
|
||||||
}
|
|
||||||
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<pages:BaseContentPage
|
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Bit.App.Pages.LoginPasswordlessPage"
|
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
|
||||||
x:DataType="pages:LoginPasswordlessViewModel"
|
|
||||||
Title="{Binding PageTitle}">
|
|
||||||
|
|
||||||
<ContentPage.BindingContext>
|
|
||||||
<pages:LoginPasswordlessViewModel />
|
|
||||||
</ContentPage.BindingContext>
|
|
||||||
|
|
||||||
<ContentPage.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
|
||||||
x:Name="_closeItem" x:Key="closeItem" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</ContentPage.Resources>
|
|
||||||
<StackLayout
|
|
||||||
Padding="7, 0, 7, 20">
|
|
||||||
<ScrollView
|
|
||||||
VerticalOptions="FillAndExpand">
|
|
||||||
<StackLayout>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n AreYouTryingToLogIn}"
|
|
||||||
FontSize="Title"
|
|
||||||
FontAttributes="Bold"
|
|
||||||
Margin="0,14,0,21"/>
|
|
||||||
<Label
|
|
||||||
Text="{Binding LogInAttemptByLabel}"
|
|
||||||
FontSize="Small"
|
|
||||||
Margin="0,0,0,24"/>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n FingerprintPhrase}"
|
|
||||||
FontSize="Small"
|
|
||||||
FontAttributes="Bold"/>
|
|
||||||
<controls:MonoLabel
|
|
||||||
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
|
||||||
FontSize="Medium"
|
|
||||||
TextColor="{DynamicResource FingerprintPhrase}"
|
|
||||||
Margin="0,0,0,27"/>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n DeviceType}"
|
|
||||||
FontSize="Small"
|
|
||||||
FontAttributes="Bold"/>
|
|
||||||
<Label
|
|
||||||
Text="{Binding LoginRequest.DeviceType}"
|
|
||||||
FontSize="Small"
|
|
||||||
Margin="0,0,0,21"/>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n IpAddress}"
|
|
||||||
IsVisible="{Binding ShowIpAddress}"
|
|
||||||
FontSize="Small"
|
|
||||||
FontAttributes="Bold"/>
|
|
||||||
<Label
|
|
||||||
Text="{Binding LoginRequest.IpAddress}"
|
|
||||||
IsVisible="{Binding ShowIpAddress}"
|
|
||||||
FontSize="Small"
|
|
||||||
Margin="0,0,0,21"/>
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n Time}"
|
|
||||||
FontSize="Small"
|
|
||||||
FontAttributes="Bold"/>
|
|
||||||
<Label
|
|
||||||
Text="{Binding TimeOfRequestText}"
|
|
||||||
FontSize="Small"
|
|
||||||
Margin="0,0,0,57"/>
|
|
||||||
</StackLayout>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
Text="{u:I18n ConfirmLogIn}"
|
|
||||||
Command="{Binding AcceptRequestCommand}"
|
|
||||||
Margin="0,0,0,17"
|
|
||||||
StyleClass="btn-primary"/>
|
|
||||||
<Button
|
|
||||||
Text="{u:I18n DenyLogIn}"
|
|
||||||
Command="{Binding RejectRequestCommand}"
|
|
||||||
StyleClass="btn-secundary"/>
|
|
||||||
|
|
||||||
</StackLayout>
|
|
||||||
</pages:BaseContentPage>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
|
||||||
{
|
|
||||||
public partial class LoginPasswordlessPage : BaseContentPage
|
|
||||||
{
|
|
||||||
private LoginPasswordlessViewModel _vm;
|
|
||||||
|
|
||||||
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
_vm = BindingContext as LoginPasswordlessViewModel;
|
|
||||||
_vm.Page = this;
|
|
||||||
|
|
||||||
_vm.LoginRequest = loginPasswordlessDetails;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
|
||||||
{
|
|
||||||
public class LoginPasswordlessViewModel : BaseViewModel
|
|
||||||
{
|
|
||||||
private IDeviceActionService _deviceActionService;
|
|
||||||
private IAuthService _authService;
|
|
||||||
private IPlatformUtilsService _platformUtilsService;
|
|
||||||
private ILogger _logger;
|
|
||||||
private LoginPasswordlessDetails _resquest;
|
|
||||||
private CancellationTokenSource _requestTimeCts;
|
|
||||||
private Task _requestTimeTask;
|
|
||||||
|
|
||||||
private const int REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES = 5;
|
|
||||||
|
|
||||||
public LoginPasswordlessViewModel()
|
|
||||||
{
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
||||||
|
|
||||||
PageTitle = AppResources.LogInRequested;
|
|
||||||
|
|
||||||
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
|
|
||||||
onException: ex => HandleException(ex),
|
|
||||||
allowsMultipleExecutions: false);
|
|
||||||
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
|
|
||||||
onException: ex => HandleException(ex),
|
|
||||||
allowsMultipleExecutions: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICommand AcceptRequestCommand { get; }
|
|
||||||
|
|
||||||
public ICommand RejectRequestCommand { get; }
|
|
||||||
|
|
||||||
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
|
|
||||||
|
|
||||||
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
|
|
||||||
|
|
||||||
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
|
|
||||||
|
|
||||||
public LoginPasswordlessDetails LoginRequest
|
|
||||||
{
|
|
||||||
get => _resquest;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
|
|
||||||
{
|
|
||||||
nameof(LogInAttemptByLabel),
|
|
||||||
nameof(TimeOfRequestText),
|
|
||||||
nameof(ShowIpAddress),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopRequestTimeUpdater()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_requestTimeCts?.Cancel();
|
|
||||||
_requestTimeCts?.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Exception(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartRequestTimeUpdater()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_requestTimeCts?.Cancel();
|
|
||||||
_requestTimeCts = new CancellationTokenSource();
|
|
||||||
_requestTimeTask = new TimerTask(_logger, UpdateRequestTime, _requestTimeCts).RunPeriodic(TimeSpan.FromMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Exception(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateRequestTime()
|
|
||||||
{
|
|
||||||
TriggerPropertyChanged(nameof(TimeOfRequestText));
|
|
||||||
if (DateTime.UtcNow > LoginRequest?.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
|
|
||||||
{
|
|
||||||
StopRequestTimeUpdater();
|
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
|
||||||
await Page.Navigation.PopModalAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PasswordlessLoginAsync(bool approveRequest)
|
|
||||||
{
|
|
||||||
if (LoginRequest.RequestDate.ToUniversalTime().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 void LogIn_Clicked(object sender, EventArgs e)
|
private async void LogIn_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
CopyAppOptions();
|
CopyAppOptions();
|
||||||
_vm.LogInCommand.Execute(null);
|
await _vm.LogInAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -9,15 +8,13 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class LoginSsoPageViewModel : BaseViewModel
|
public class LoginSsoPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private const string REDIRECT_URI = "bitwarden://sso-callback";
|
|
||||||
|
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
@@ -26,7 +23,6 @@ namespace Bit.App.Pages
|
|||||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
private string _orgIdentifier;
|
private string _orgIdentifier;
|
||||||
|
|
||||||
@@ -41,11 +37,9 @@ namespace Bit.App.Pages
|
|||||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
||||||
|
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false);
|
LogInCommand = new Command(async () => await LogInAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string OrgIdentifier
|
public string OrgIdentifier
|
||||||
@@ -54,7 +48,7 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _orgIdentifier, value);
|
set => SetProperty(ref _orgIdentifier, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action SsoAuthSuccessAction { get; set; }
|
public Action SsoAuthSuccessAction { get; set; }
|
||||||
@@ -71,91 +65,81 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task LogInAsync()
|
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
|
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);
|
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)
|
catch (ApiException e)
|
||||||
{
|
{
|
||||||
_logger.Exception(e);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError,
|
await _platformUtilsService.ShowDialogAsync(
|
||||||
|
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
|
||||||
AppResources.AnErrorHasOccurred);
|
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)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
// user canceled
|
// user canceled
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
var code = GetResultCode(authResult, state);
|
||||||
|
if (!string.IsNullOrEmpty(code))
|
||||||
|
{
|
||||||
|
await LogIn(code, codeVerifier, redirectUri, OrgIdentifier);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_logger.Exception(ex);
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||||
|
AppResources.AnErrorHasOccurred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,11 +158,11 @@ namespace Bit.App.Pages
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogIn(string code, string codeVerifier, string orgId)
|
private async Task LogIn(string code, string codeVerifier, string redirectUri, string orgId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId);
|
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri, orgId);
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
IsToggled="{Binding AcceptPolicies}"
|
IsToggled="{Binding AcceptPolicies}"
|
||||||
StyleClass="box-value"
|
StyleClass="box-value"
|
||||||
HorizontalOptions="Start"
|
HorizontalOptions="Start"
|
||||||
Margin="0, 0, 10, 0"/>
|
Margin="{Binding SwitchMargin}"/>
|
||||||
<Label StyleClass="box-footer-label"
|
<Label StyleClass="box-footer-label"
|
||||||
HorizontalOptions="Fill">
|
HorizontalOptions="Fill">
|
||||||
<Label.FormattedText>
|
<Label.FormattedText>
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ namespace Bit.App.Pages
|
|||||||
get => _acceptPolicies;
|
get => _acceptPolicies;
|
||||||
set => SetProperty(ref _acceptPolicies, value);
|
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 bool ShowTerms { get; set; }
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
|||||||
@@ -17,19 +17,15 @@
|
|||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
x:Name="_cancelItem" />
|
x:Name="_cancelItem" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
<u:IsNullConverter x:Key="isNull" />
|
<u:IsNullConverter x:Key="isNull" />
|
||||||
<ToolbarItem
|
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary"
|
||||||
x:Name="_moreItem"
|
x:Name="_moreItem" x:Key="moreItem"
|
||||||
x:Key="moreItem"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
Icon="more_vert.png"
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
Order="Primary"
|
|
||||||
Command="{Binding MoreCommand}"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Options}" />
|
|
||||||
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
<ToolbarItem Text="{u:I18n UseAnotherTwoStepMethod}"
|
||||||
Clicked="Methods_Clicked"
|
Clicked="Methods_Clicked"
|
||||||
Order="Secondary"
|
Order="Secondary"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -136,6 +137,21 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void More_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!DoOnce())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
||||||
|
|
||||||
|
if (selection == AppResources.UseAnotherTwoStepMethod)
|
||||||
|
{
|
||||||
|
await _vm.AnotherMethodAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
private async void ResendEmail_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -13,7 +12,6 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -32,7 +30,6 @@ namespace Bit.App.Pages
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IAppIdService _appIdService;
|
private readonly IAppIdService _appIdService;
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
private TwoFactorProviderType? _selectedProviderType;
|
private TwoFactorProviderType? _selectedProviderType;
|
||||||
private string _totpInstruction;
|
private string _totpInstruction;
|
||||||
@@ -54,11 +51,9 @@ namespace Bit.App.Pages
|
|||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>();
|
|
||||||
|
|
||||||
PageTitle = AppResources.TwoStepLogin;
|
PageTitle = AppResources.TwoStepLogin;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||||
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TotpInstruction
|
public string TotpInstruction
|
||||||
@@ -116,7 +111,6 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
public Command SubmitCommand { get; }
|
public Command SubmitCommand { get; }
|
||||||
public ICommand MoreCommand { get; }
|
|
||||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||||
public Action StartSetPasswordAction { get; set; }
|
public Action StartSetPasswordAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
@@ -343,15 +337,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MoreAsync()
|
|
||||||
{
|
|
||||||
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, AppResources.UseAnotherTwoStepMethod);
|
|
||||||
if (selection == AppResources.UseAnotherTwoStepMethod)
|
|
||||||
{
|
|
||||||
await AnotherMethodAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AnotherMethodAsync()
|
public async Task AnotherMethodAsync()
|
||||||
{
|
{
|
||||||
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
|
var supportedProviders = _authService.GetSupportedTwoFactorProviders();
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (useCurrentActiveAccount)
|
if (useCurrentActiveAccount)
|
||||||
{
|
{
|
||||||
return new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
|
return new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
|
||||||
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
|
|
||||||
}
|
}
|
||||||
return new AvatarImageSource();
|
return new AvatarImageSource();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<pages:BaseContentPage
|
<pages:BaseContentPage
|
||||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
|
||||||
x:Class="Bit.App.Pages.GeneratorPage"
|
x:Class="Bit.App.Pages.GeneratorPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
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"
|
x:DataType="pages:GeneratorPageViewModel"
|
||||||
Title="{Binding PageTitle}">
|
Title="{Binding PageTitle}">
|
||||||
<ContentPage.BindingContext>
|
<ContentPage.BindingContext>
|
||||||
@@ -19,9 +15,7 @@
|
|||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
<u:LocalizableEnumConverter x:Key="localizableEnum" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
<xct:EnumToBoolConverter x:Key="enumToBool"/>
|
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"
|
|
||||||
x:Name="_closeItem" x:Key="closeItem" />
|
x:Name="_closeItem" x:Key="closeItem" />
|
||||||
<ToolbarItem Text="{u:I18n Select}"
|
<ToolbarItem Text="{u:I18n Select}"
|
||||||
Clicked="Select_Clicked"
|
Clicked="Select_Clicked"
|
||||||
@@ -47,302 +41,60 @@
|
|||||||
in ContentView.-->
|
in ContentView.-->
|
||||||
<ContentView>
|
<ContentView>
|
||||||
<ScrollView Padding="0, 0, 0, 20">
|
<ScrollView Padding="0, 0, 0, 20">
|
||||||
<StackLayout Spacing="0"
|
<StackLayout Spacing="0" Padding="0">
|
||||||
Padding="10,0">
|
<StackLayout StyleClass="box">
|
||||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||||
Margin="0, 12, 0, 0"
|
Margin="0, 12, 0, 0"
|
||||||
Padding="10,0"
|
RowSpacing="0"
|
||||||
RowSpacing="0"
|
ColumnSpacing="0">
|
||||||
ColumnSpacing="0">
|
<Grid.RowDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="*" />
|
</Grid.RowDefinitions>
|
||||||
</Grid.RowDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
</Grid.ColumnDefinitions>
|
||||||
</Grid.ColumnDefinitions>
|
<Frame Padding="10"
|
||||||
<Frame Padding="10"
|
Margin="0"
|
||||||
Margin="0"
|
HasShadow="False"
|
||||||
HasShadow="False"
|
BackgroundColor="Transparent"
|
||||||
BackgroundColor="Transparent"
|
BorderColor="Accent">
|
||||||
BorderColor="Accent">
|
<Label
|
||||||
<Label
|
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
||||||
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
StyleClass="text-muted, text-sm, text-bold"
|
HorizontalTextAlignment="Center" />
|
||||||
HorizontalTextAlignment="Center" />
|
</Frame>
|
||||||
</Frame>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
|
|
||||||
StyleClass="box-row"
|
|
||||||
RowDefinitions="Auto"
|
|
||||||
ColumnDefinitions="*,Auto,Auto">
|
|
||||||
<controls:MonoLabel
|
<controls:MonoLabel
|
||||||
x:Name="lblPassword"
|
x:Name="lblPassword"
|
||||||
StyleClass="text-lg, text-html"
|
StyleClass="text-lg, text-html"
|
||||||
Text="{Binding ColoredPassword, Mode=OneWay}"
|
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"
|
Margin="0, 20"
|
||||||
HorizontalOptions="Start" />
|
HorizontalTextAlignment="Center"
|
||||||
<controls:IconButton
|
HorizontalOptions="CenterAndExpand"
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
LineBreakMode="CharacterWrap" />
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
<Button Text="{u:I18n RegeneratePassword}"
|
||||||
Command="{Binding CopyCommand}"
|
StyleClass="btn-primary"
|
||||||
Grid.Column="1"
|
HorizontalOptions="FillAndExpand"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
Clicked="Regenerate_Clicked"></Button>
|
||||||
AutomationProperties.Name="{u:I18n CopyUsername}" />
|
<Button Text="{u:I18n CopyPassword}"
|
||||||
<controls:IconButton
|
HorizontalOptions="FillAndExpand"
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
Clicked="Copy_Clicked"></Button>
|
||||||
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>
|
||||||
<Label Text="{u:I18n Options, Header=True}"
|
<StackLayout StyleClass="box">
|
||||||
StyleClass="box-header, box-header-platform"
|
<StackLayout StyleClass="box-row-header">
|
||||||
Margin="0,10,0,0"/>
|
<Label Text="{u:I18n Options, Header=True}"
|
||||||
<!--USERNAME OPTIONS-->
|
StyleClass="box-header, box-header-platform" />
|
||||||
<StackLayout IsVisible="{Binding IsUsername}">
|
|
||||||
<StackLayout Orientation="Horizontal">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n UsernameType}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
VerticalOptions="Center"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
|
|
||||||
Command="{Binding UsernameTypePromptHelpCommand}"
|
|
||||||
TextColor="{DynamicResource HyperlinkColor}"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n UsernamePromptHelpLink}"
|
|
||||||
VerticalOptions="Center"/>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<Picker
|
|
||||||
x:Name="_usernameTypePicker"
|
|
||||||
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
|
|
||||||
SelectedItem="{Binding UsernameTypeSelected}"
|
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<Label
|
|
||||||
StyleClass="box-footer-label"
|
|
||||||
Text="{Binding UsernameTypeDescriptionLabel}" />
|
|
||||||
<!--PLUS ADDRESSED EMAIL OPTIONS-->
|
|
||||||
<StackLayout StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
|
|
||||||
<Label Text="{u:I18n EmailRequiredParenthesis}"
|
|
||||||
StyleClass="box-label" />
|
|
||||||
<Entry x:Name="_plusAddressedEmailEntry"
|
|
||||||
Text="{Binding PlusAddressedEmail}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{u:I18n EmailType}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Margin="0,10,0,0"/>
|
|
||||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
x:Name="_plusAddressedEmailTypePicker"
|
|
||||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
|
||||||
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
|
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{u:I18n Website}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Margin="0,10,0,0" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{Binding EmailWebsite}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
StyleClass="box-row-separator"
|
|
||||||
Margin="0,10,0,0" />
|
|
||||||
</StackLayout>
|
|
||||||
<!--CATCH-ALL EMAIL OPTIONS-->
|
|
||||||
<StackLayout StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
|
||||||
StyleClass="box-label" />
|
|
||||||
<Entry
|
|
||||||
x:Name="_catchAllEmailDomainNameEntry"
|
|
||||||
Text="{Binding CatchAllEmailDomain}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{u:I18n EmailType}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Margin="0,10,0,0"/>
|
|
||||||
<Picker IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
x:Name="_catchallEmailTypePicker"
|
|
||||||
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
|
|
||||||
SelectedItem="{Binding CatchAllEmailTypeSelected}"
|
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{u:I18n Website}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Margin="0,10,0,0" />
|
|
||||||
<Label IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
Text="{Binding EmailWebsite}"
|
|
||||||
StyleClass="box-value"/>
|
|
||||||
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
|
|
||||||
StyleClass="box-row-separator"
|
|
||||||
Margin="0,10,0,0"/>
|
|
||||||
</StackLayout>
|
|
||||||
<!--FORWARDED EMAIL OPTIONS-->
|
|
||||||
<StackLayout StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n Service}"
|
|
||||||
StyleClass="box-label" />
|
|
||||||
<Picker
|
|
||||||
x:Name="_serviceTypePicker"
|
|
||||||
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
|
|
||||||
SelectedItem="{Binding ForwardedEmailServiceSelected}"
|
|
||||||
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
|
|
||||||
StyleClass="box-value" />
|
|
||||||
<!--ANONADDY OPTIONS-->
|
|
||||||
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Margin="0,10,0,0"
|
|
||||||
Text="{u:I18n APIAccessToken}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_anonAddyApiAccessTokenEntry"
|
|
||||||
Text="{Binding AnonAddyApiAccessToken}"
|
|
||||||
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
|
|
||||||
Grid.Row="1"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowAnonAddyHiddenValueIcon}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
|
||||||
Text="{u:I18n DomainNameRequiredParenthesis}"
|
|
||||||
StyleClass="box-label"
|
|
||||||
Margin="0,10,0,0"/>
|
|
||||||
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
|
|
||||||
x:Name="_anonAddyDomainNameEntry"
|
|
||||||
Text="{Binding AnonAddyDomainName}"
|
|
||||||
StyleClass="box-value"/>
|
|
||||||
<!--FIREFOX RELAY OPTIONS-->
|
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n APIAccessToken}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_firefoxRelayApiAccessTokenEntry"
|
|
||||||
Text="{Binding FirefoxRelayApiAccessToken}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
<!--SIMPLELOGIN OPTIONS-->
|
|
||||||
<Grid StyleClass="box-row, box-row-input"
|
|
||||||
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
|
|
||||||
Grid.RowDefinitions="Auto,*"
|
|
||||||
Grid.ColumnDefinitions="*,Auto">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n APIKeyRequiredParenthesis}"
|
|
||||||
StyleClass="box-label"/>
|
|
||||||
<Entry
|
|
||||||
x:Name="_simpleLoginApiKeyEntry"
|
|
||||||
Text="{Binding SimpleLoginApiKey}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
Grid.Row="1"
|
|
||||||
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
|
|
||||||
<controls:IconButton
|
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
|
||||||
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
|
|
||||||
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"/>
|
|
||||||
</Grid>
|
|
||||||
</StackLayout>
|
|
||||||
<!--RANDOM WORD OPTIONS-->
|
|
||||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n Capitalize}"
|
|
||||||
StyleClass="box-label-regular"
|
|
||||||
HorizontalOptions="StartAndExpand" />
|
|
||||||
<Switch
|
|
||||||
IsToggled="{Binding CapitalizeRandomWordUsername}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
HorizontalOptions="End" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
|
||||||
StyleClass="box-row-separator" />
|
|
||||||
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n IncludeNumber}"
|
|
||||||
StyleClass="box-label-regular"
|
|
||||||
HorizontalOptions="StartAndExpand" />
|
|
||||||
<Switch
|
|
||||||
IsToggled="{Binding IncludeNumberRandomWordUsername}"
|
|
||||||
StyleClass="box-value"
|
|
||||||
HorizontalOptions="End" />
|
|
||||||
</Grid>
|
|
||||||
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
|
|
||||||
StyleClass="box-row-separator" />
|
|
||||||
</StackLayout>
|
|
||||||
<!--PASSWORD OPTIONS-->
|
|
||||||
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
|
|
||||||
<StackLayout StyleClass="box-row, box-row-input">
|
<StackLayout StyleClass="box-row, box-row-input">
|
||||||
<Label
|
<Label
|
||||||
Text="{u:I18n PasswordType}"
|
Text="{u:I18n Type}"
|
||||||
StyleClass="box-label" />
|
StyleClass="box-label" />
|
||||||
<Picker
|
<Picker
|
||||||
x:Name="_passwordTypePicker"
|
x:Name="_typePicker"
|
||||||
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
|
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
|
||||||
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
|
SelectedIndex="{Binding TypeSelectedIndex}"
|
||||||
StyleClass="box-value" />
|
StyleClass="box-value" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Spacing="0"
|
<StackLayout Spacing="0"
|
||||||
@@ -376,11 +128,7 @@
|
|||||||
Text="{Binding WordSeparator}"
|
Text="{Binding WordSeparator}"
|
||||||
IsSpellCheckEnabled="False"
|
IsSpellCheckEnabled="False"
|
||||||
IsTextPredictionEnabled="False"
|
IsTextPredictionEnabled="False"
|
||||||
StyleClass="box-value">
|
StyleClass="box-value" />
|
||||||
<Entry.Effects>
|
|
||||||
<effects:NoEmojiKeyboardEffect />
|
|
||||||
</Entry.Effects>
|
|
||||||
</Entry>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row, box-row-switch">
|
<StackLayout StyleClass="box-row, box-row-switch">
|
||||||
<Label
|
<Label
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Styles;
|
using Bit.App.Styles;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
@@ -19,20 +18,15 @@ namespace Bit.App.Pages
|
|||||||
private readonly Action<string> _selectAction;
|
private readonly Action<string> _selectAction;
|
||||||
private readonly TabsPage _tabsPage;
|
private readonly TabsPage _tabsPage;
|
||||||
|
|
||||||
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false, AppOptions appOptions = null)
|
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null)
|
||||||
{
|
{
|
||||||
_tabsPage = tabsPage;
|
_tabsPage = tabsPage;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_vm = BindingContext as GeneratorPageViewModel;
|
_vm = BindingContext as GeneratorPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_fromTabPage = fromTabPage;
|
_fromTabPage = fromTabPage;
|
||||||
_selectAction = selectAction;
|
_selectAction = selectAction;
|
||||||
_vm.ShowTypePicker = fromTabPage;
|
|
||||||
_vm.IsUsername = isUsernameGenerator;
|
|
||||||
_vm.EmailWebsite = emailWebsite;
|
|
||||||
_vm.EditMode = editMode;
|
|
||||||
_vm.IosExtension = appOptions?.IosExtension ?? false;
|
|
||||||
var isIos = Device.RuntimePlatform == Device.iOS;
|
var isIos = Device.RuntimePlatform == Device.iOS;
|
||||||
if (selectAction != null)
|
if (selectAction != null)
|
||||||
{
|
{
|
||||||
@@ -53,12 +47,10 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_historyItem);
|
ToolbarItems.Add(_historyItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
if (isIos)
|
||||||
_passwordTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
{
|
||||||
_usernameTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
|
_typePicker.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()
|
public async Task InitAsync()
|
||||||
@@ -105,6 +97,16 @@ namespace Bit.App.Pages
|
|||||||
return base.OnBackButtonPressed();
|
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)
|
private async void More_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (!DoOnce())
|
if (!DoOnce())
|
||||||
@@ -122,7 +124,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void Select_Clicked(object sender, EventArgs e)
|
private void Select_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
|
_selectAction?.Invoke(_vm.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void History_Clicked(object sender, EventArgs e)
|
private async void History_Clicked(object sender, EventArgs e)
|
||||||
@@ -136,24 +138,19 @@ namespace Bit.App.Pages
|
|||||||
await _vm.SliderChangedAsync();
|
await _vm.SliderChangedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void Close_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task UpdateOnThemeChanged()
|
public override async Task UpdateOnThemeChanged()
|
||||||
{
|
{
|
||||||
await base.UpdateOnThemeChanged();
|
await base.UpdateOnThemeChanged();
|
||||||
|
|
||||||
await Device.InvokeOnMainThreadAsync(() =>
|
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
|
||||||
{
|
|
||||||
if (_vm != null)
|
|
||||||
{
|
|
||||||
if (_vm.IsUsername)
|
|
||||||
{
|
|
||||||
_vm.RedrawUsername();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_vm.RedrawPassword();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user