mirror of
https://github.com/bitwarden/mobile
synced 2026-01-22 04:13:19 +00:00
Compare commits
1 Commits
v2024.2.1
...
fedemkr-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a437a0649 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -11,11 +11,11 @@
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
|
||||
# DevOps for Version Bumping
|
||||
src/App/Platforms/Android/AndroidManifest.xml
|
||||
src/Android/Properties/AndroidManifest.xml
|
||||
src/iOS.Autofill/Info.plist
|
||||
src/iOS.Extension/Info.plist
|
||||
src/iOS.ShareExtension/Info.plist
|
||||
src/App/Platforms/iOS/Info.plist
|
||||
src/iOS/Info.plist
|
||||
|
||||
## Auth team files ##
|
||||
|
||||
|
||||
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Normal file
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
Normal file
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_share_extension.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_share_extension.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_watch_app_extension.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_watch_app_extension.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
3
.github/secrets/google-services.json.gpg
vendored
Normal file
3
.github/secrets/google-services.json.gpg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<EFBFBD>
|
||||
K<>Y#<23>(<28><><EFBFBD><EFBFBD>EI߄T?)l<><6C><EFBFBD><18><><10>"=<3D>|<7C>'e<><0E>m<EFBFBD>/~<7E><>'F<><46>><3E><><EFBFBD><EFBFBD>l<EFBFBD>b<EFBFBD>[<5B>+R<><52>iL<69><4C>"<22><><EFBFBD>~V:<3A><>p<EFBFBD>a<17>ڵel%8t<38><74>튖<EFBFBD>y<<3C>n<EFBFBD><6E><EFBFBD>aU<61>w<16>JD<4A><44><1F><>We<57>9<EFBFBD><39><EFBFBD><EFBFBD><x8d<38>O<EFBFBD>j\<14>ד<EFBFBD><D793><EFBFBD>Vq<56><71>
|
||||
Ǻ<EFBFBD>-<2D>#<23><><11><>]$<24>(<28>l,<2C>Br<42><02><>d<><64><EFBFBD>a-<2D><><EFBFBD>:<3A><>:<3A><04>9b,!Em<02><19><>Qf<>D<EFBFBD>g<EFBFBD><06><0E>x(P<>ȡ~<7E><EFBFBD><CDB9> <09><>[<06><>!:<3A>;f<><66>
|
||||
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/play_creds.json.gpg
vendored
Normal file
BIN
.github/secrets/play_creds.json.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
Normal file
Binary file not shown.
5
.github/workflows/build-beta.yml
vendored
5
.github/workflows/build-beta.yml
vendored
@@ -1,5 +0,0 @@
|
||||
---
|
||||
name: Build Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
336
.github/workflows/build.yml
vendored
336
.github/workflows/build.yml
vendored
@@ -31,7 +31,6 @@ jobs:
|
||||
- name: Print lines of code
|
||||
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
||||
|
||||
|
||||
setup:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -59,7 +58,6 @@ jobs:
|
||||
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
android:
|
||||
name: Android
|
||||
runs-on: windows-2022
|
||||
@@ -69,8 +67,7 @@ jobs:
|
||||
matrix:
|
||||
variant: ["prod", "qa"]
|
||||
env:
|
||||
android_folder_path: src\App\Platforms\Android
|
||||
android_folder_path_bash: src/App/Platforms/Android
|
||||
android_folder_path: src/App/Platforms/Android
|
||||
steps:
|
||||
- name: Setup NuGet
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
@@ -85,7 +82,7 @@ jobs:
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -96,8 +93,7 @@ jobs:
|
||||
- name: Install Microsoft OpenJDK 11
|
||||
run: |
|
||||
choco install microsoft-openjdk11 --no-progress
|
||||
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | `
|
||||
Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
Write-Output "Java Home: $env:JAVA_HOME"
|
||||
|
||||
- name: Print environment
|
||||
@@ -113,34 +109,27 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Download secrets
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
mkdir -p ~/secrets
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_play-keystore.jks --output none
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_upload-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_upload-keystore.jks --output none
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name play_creds.json --file $HOME/secrets/play_creds.json --output none
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||
shell: bash
|
||||
|
||||
- name: Download secrets - Google Services
|
||||
- name: Decrypt secrets - Google Services
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name google-services.json --file ./${{ env.android_folder_path_bash }}/google-services.json --output none
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.android_folder_path }}/google-services.json ./.github/secrets/google-services.json.gpg
|
||||
shell: bash
|
||||
|
||||
- name: Increment version
|
||||
@@ -150,9 +139,9 @@ jobs:
|
||||
echo "########################################"
|
||||
echo "##### Setting Version Code $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
|
||||
|
||||
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||
./${{ env.android_folder_path_bash }}/AndroidManifest.xml
|
||||
./${{ env.android_folder_path }}/AndroidManifest.xml
|
||||
shell: bash
|
||||
|
||||
- name: Restore packages
|
||||
@@ -161,75 +150,83 @@ jobs:
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
|
||||
# - name: Run Core tests
|
||||
# run: |
|
||||
# dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" `
|
||||
# /p:CustomConstants=UT
|
||||
# - name: Verify Format
|
||||
# run: dotnet tool run dotnet-format --check
|
||||
|
||||
# - name: Report test results
|
||||
# uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
||||
# if: always()
|
||||
# with:
|
||||
# name: Test Results
|
||||
# path: "**/test-results.trx"
|
||||
# reporter: dotnet-trx
|
||||
# fail-on-error: true
|
||||
- name: Run Core tests
|
||||
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" /p:CustomConstants=UT
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
|
||||
if: always()
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- 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 }})
|
||||
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
||||
|
||||
- name: Build & Sign Android
|
||||
- name: Build Android
|
||||
run: |
|
||||
$configuration = "Release";
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android
|
||||
|
||||
- name: Sign Android Build
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
$projToBuild = "$($env:GITHUB_WORKSPACE)/${{ env.main_app_project_path }}";
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
$packageName = "com.x8bit.bitwarden";
|
||||
|
||||
if ("${{ matrix.variant }}" -ne "prod")
|
||||
{
|
||||
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
||||
}
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signingUploadKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_upload-keystore.jks"
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||
/p:AndroidPackageFormats=aab `
|
||||
/p:AndroidKeyStore=true `
|
||||
/p:AndroidSigningKeyStore=$signingUploadKeyStore `
|
||||
/p:AndroidSigningKeyAlias=upload `
|
||||
/p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" `
|
||||
/p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidPackageFormats=aab /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_upload-keystore.jks") /p:AndroidSigningKeyAlias=upload /p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Google Play Bundle to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedAabPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.aab";
|
||||
$signedAabDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).aab";
|
||||
$signedAabPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.aab");
|
||||
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
|
||||
Copy-Item $signedAabPath $signedAabDestPath
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign APK Release Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signingPlayKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_play-keystore.jks"
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
|
||||
/p:AndroidKeyStore=true `
|
||||
/p:AndroidSigningKeyStore=$signingPlayKeyStore `
|
||||
/p:AndroidSigningKeyAlias=bitwarden `
|
||||
/p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" `
|
||||
/p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_play-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Release APK to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk");
|
||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
||||
|
||||
$signedApkPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.apk";
|
||||
$signedApkDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).apk";
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
- name: Upload Prod .aab artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: ./com.x8bit.bitwarden.aab
|
||||
@@ -237,7 +234,7 @@ jobs:
|
||||
|
||||
- name: Upload Prod .apk artifact
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: ./com.x8bit.bitwarden.apk
|
||||
@@ -245,7 +242,7 @@ jobs:
|
||||
|
||||
- name: Upload Other .apk artifact
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
@@ -265,7 +262,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for prod
|
||||
if: ${{ matrix.variant == 'prod' }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
@@ -273,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk sha file for other
|
||||
if: ${{ matrix.variant != 'prod' }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
@@ -286,12 +283,13 @@ jobs:
|
||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
||||
run: |
|
||||
$publisherPath = "$($env:GITHUB_WORKSPACE)\store\google\Publisher\bin\Release\net8.0\Publisher.dll"
|
||||
$credsPath = "$($HOME)\secrets\play_creds.json"
|
||||
$aabPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden.aab"
|
||||
$track = "internal"
|
||||
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/net7.0/Publisher.dll"
|
||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
||||
AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab"
|
||||
TRACK="internal"
|
||||
|
||||
dotnet $publisherPath $credsPath $aabPath $track
|
||||
dotnet $PUBLISHER_PATH $CREDS_PATH $AAB_PATH $TRACK
|
||||
shell: bash
|
||||
|
||||
|
||||
f-droid:
|
||||
@@ -314,7 +312,7 @@ jobs:
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ede762b26a2de8d110bb5a3db4d7e0e080c0e917 # v1.3.3
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -339,26 +337,23 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Download secrets
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
FILE: app_fdroid-keystore.jks
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file $HOME/secrets/$FILE --output none
|
||||
mkdir -p ~/secrets
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./${{ env.main_app_folder_path }}/app_fdroid-keystore.jks ./.github/secrets/app_fdroid-keystore.jks.gpg
|
||||
shell: bash
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Setting Version Code $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
|
||||
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
|
||||
./${{ env.android_manifest_path }}
|
||||
@@ -371,12 +366,16 @@ jobs:
|
||||
|
||||
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
|
||||
|
||||
Write-Output "##### Back up project files"
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Backup project files"
|
||||
Write-Output "########################################"
|
||||
|
||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||
Copy-Item $appPath $($appPath + ".original");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Cleanup Android Manifest"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
@@ -394,8 +393,10 @@ jobs:
|
||||
$configuration = "Release";
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
|
||||
Write-Output "##### Build $configuration FDROID"
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration FDROID
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android /p:CustomConstants="FDROID"
|
||||
|
||||
- name: Sign for F-Droid
|
||||
@@ -405,11 +406,15 @@ jobs:
|
||||
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
|
||||
$packageName = "com.x8bit.bitwarden";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign FDroid"
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_fdroid-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:CustomConstants="FDROID" --no-restore
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy FDroid apk to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk");
|
||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden-fdroid.apk");
|
||||
@@ -417,7 +422,7 @@ jobs:
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -429,13 +434,12 @@ jobs:
|
||||
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid sha file
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
ios:
|
||||
name: Apple iOS
|
||||
runs-on: macos-13
|
||||
@@ -454,13 +458,13 @@ jobs:
|
||||
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
|
||||
with:
|
||||
nuget-version: 6.4.0
|
||||
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
|
||||
# This step might be obsolete at some point as .NET MAUI workloads
|
||||
# are starting to come pre-installed on the GH Actions build agents.
|
||||
- name: Install MAUI Workload
|
||||
run: dotnet workload install maui --ignore-failed-sources
|
||||
@@ -489,42 +493,42 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "appcenter-ios-token"
|
||||
|
||||
- name: Download Provisioning Profiles secrets
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: profiles
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
profiles=(
|
||||
"dist_autofill.mobileprovision"
|
||||
"dist_bitwarden.mobileprovision"
|
||||
"dist_extension.mobileprovision"
|
||||
"dist_share_extension.mobileprovision"
|
||||
"dist_bitwarden_watch_app.mobileprovision"
|
||||
"dist_bitwarden_watch_app_extension.mobileprovision"
|
||||
)
|
||||
mkdir -p ~/secrets
|
||||
|
||||
for FILE in "${profiles[@]}"
|
||||
do
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file $HOME/secrets/$FILE --output none
|
||||
done
|
||||
|
||||
- name: Download Google Services secret
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
FILE: GoogleService-Info.plist
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
|
||||
--file src/watchOS/bitwarden/$FILE --output none
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/bitwarden-mobile-key.p12 ./.github/secrets/bitwarden-mobile-key.p12.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/iphone-distribution-cert.p12 ./.github/secrets/iphone-distribution-cert.p12.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_autofill.mobileprovision ./.github/secrets/dist_autofill.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_bitwarden.mobileprovision ./.github/secrets/dist_bitwarden.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_extension.mobileprovision ./.github/secrets/dist_extension.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_share_extension.mobileprovision \
|
||||
./.github/secrets/dist_share_extension.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_watch_app.mobileprovision \
|
||||
./.github/secrets/dist_watch_app.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output $HOME/secrets/dist_watch_app_extension.mobileprovision \
|
||||
./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
|
||||
echo "########################################"
|
||||
|
||||
echo "### CFBundleVersion $BUILD_NUMBER" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/Info.plist
|
||||
@@ -532,30 +536,30 @@ jobs:
|
||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
|
||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
|
||||
cd src/watchOS/bitwarden
|
||||
agvtool new-version -all $BUILD_NUMBER
|
||||
agvtool new-version -all $BUILD_NUMBER
|
||||
|
||||
- name: Update Entitlements
|
||||
run: |
|
||||
echo "########################################"
|
||||
echo "##### Updating Entitlements"
|
||||
echo "########################################"
|
||||
|
||||
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./${{ env.ios_folder_path }}/Entitlements.plist
|
||||
|
||||
- name: Get certificates
|
||||
run: |
|
||||
mkdir -p $HOME/certificates
|
||||
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/ios-distribution |
|
||||
jq -r .value | base64 -d > $HOME/certificates/ios-distribution.p12
|
||||
|
||||
|
||||
- name: Set up Keychain
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
|
||||
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
|
||||
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
||||
run: |
|
||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security set-keychain-settings -lut 1200 build.keychain
|
||||
|
||||
security import $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -T /usr/bin/codesign \
|
||||
-T /usr/bin/security
|
||||
security import ~/secrets/bitwarden-mobile-key.p12 -k build.keychain -P $MOBILE_KEY_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||
|
||||
- name: Set up provisioning profiles
|
||||
@@ -564,8 +568,8 @@ jobs:
|
||||
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
|
||||
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
|
||||
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision
|
||||
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app.mobileprovision
|
||||
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app_extension.mobileprovision
|
||||
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision
|
||||
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision
|
||||
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
mkdir -p "$PROFILES_DIR_PATH"
|
||||
@@ -593,50 +597,74 @@ jobs:
|
||||
|
||||
- name: Bulid WatchApp
|
||||
run: |
|
||||
echo "########################################"
|
||||
echo "##### Build WatchApp with Release Configuration"
|
||||
echo "########################################"
|
||||
|
||||
xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden
|
||||
|
||||
echo "########################################"
|
||||
echo "##### Done"
|
||||
echo "########################################"
|
||||
|
||||
- name: Archive Build for App Store
|
||||
run: |
|
||||
echo "##### Archive for Release ios-arm64"
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive for Release ios-arm64
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet publish ${{ env.main_app_project_path }} -c Release -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=ios-arm64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
shell: pwsh
|
||||
|
||||
- name: Archive Build for Mobile Automation
|
||||
run: |
|
||||
echo "##### Archive Debug for iossimulator-x64"
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive Debug for iossimulator-x64
|
||||
Write-Output "########################################"
|
||||
|
||||
dotnet build ${{ env.main_app_project_path }} -c Debug -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=iossimulator-x64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
|
||||
ls $HOME/Library/Developer/Xcode/Archives
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
ls ~/Library/Developer/Xcode/Archives
|
||||
shell: pwsh
|
||||
|
||||
- name: Export .ipa for App Store
|
||||
env:
|
||||
EXPORT_OPTIONS_PATH: ./.github/resources/export-options-app-store.plist
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
run: |
|
||||
EXPORT_OPTIONS_PATH="./.github/resources/export-options-app-store.plist"
|
||||
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
|
||||
-exportOptionsPlist $EXPORT_OPTIONS_PATH
|
||||
|
||||
- name: Export .app for Automation CI
|
||||
env:
|
||||
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
run: |
|
||||
ARCHIVE_PATH="./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
|
||||
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
|
||||
|
||||
- name: Copy all dSYMs files to upload
|
||||
env:
|
||||
EXPORT_PATH: ./bitwarden-export
|
||||
WATCH_ARCHIVE_DSYMS_PATH: ./src/watchOS/bitwarden.xcarchive/dSYMs/
|
||||
WATCH_DSYMS_EXPORT_PATH: ./bitwarden-export/Watch_dSYMs
|
||||
run: |
|
||||
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
||||
EXPORT_PATH="./bitwarden-export"
|
||||
|
||||
WATCH_ARCHIVE_DSYMS_PATH="./src/watchOS/bitwarden.xcarchive/dSYMs/"
|
||||
WATCH_DSYMS_EXPORT_PATH="$EXPORT_PATH/Watch_dSYMs"
|
||||
|
||||
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
||||
mkdir $WATCH_DSYMS_EXPORT_PATH
|
||||
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
||||
|
||||
- name: Upload App Store .ipa & dSYMs artifacts
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: Bitwarden iOS
|
||||
path: |
|
||||
@@ -645,7 +673,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .app file for Automation CI
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
with:
|
||||
name: ${{ env.app_ci_output_filename }}.app.zip
|
||||
path: ./bitwarden-export/${{ env.app_ci_output_filename }}.app.zip
|
||||
@@ -679,7 +707,10 @@ jobs:
|
||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
run: |
|
||||
echo "########################################"
|
||||
echo "##### Uploading Watch dSYMs to Firebase"
|
||||
echo "########################################"
|
||||
|
||||
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
|
||||
|
||||
- name: Validate app in App Store
|
||||
@@ -695,6 +726,7 @@ jobs:
|
||||
run: |
|
||||
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
|
||||
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
|
||||
shell: bash
|
||||
|
||||
- name: Deploy to App Store
|
||||
if: |
|
||||
@@ -738,13 +770,13 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload Sources
|
||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
||||
uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
crowdin_branch_name: main
|
||||
crowdin_branch_name: main
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0
|
||||
uses: crowdin/github-action@97bef4fd3f1b853eb105bc99b8d0d563760e024c # v1.17.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||
steps:
|
||||
- name: Branch check
|
||||
if: inputs.release_type != 'Dry Run'
|
||||
if: github.event.inputs.release_type != 'Dry Run'
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
|
||||
echo "==================================="
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
with:
|
||||
release-type: ${{ inputs.release_type }}
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: xamarin
|
||||
file: src/App/Platforms/Android/AndroidManifest.xml
|
||||
file: src/Android/Properties/AndroidManifest.xml
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
@@ -55,8 +55,8 @@ jobs:
|
||||
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub deployment
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: chrnorm/deployment-action@d42cde7132fcec920de534fffc3be83794335c00 # v2.0.5
|
||||
id: deployment
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
@@ -67,16 +67,16 @@ jobs:
|
||||
|
||||
|
||||
- name: Download all artifacts
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ steps.branch.outputs.branch-name }}
|
||||
|
||||
- name: Dry Run - Download all artifacts
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -86,8 +86,8 @@ jobs:
|
||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0
|
||||
with:
|
||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
||||
@@ -103,16 +103,16 @@ jobs:
|
||||
draft: true
|
||||
|
||||
- name: Update deployment status to Success
|
||||
if: ${{ inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
@@ -129,8 +129,8 @@ jobs:
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- name: Download F-Droid .apk artifact
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -138,8 +138,8 @@ jobs:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Dry Run - Download F-Droid .apk artifact
|
||||
if: ${{ inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
||||
@@ -176,19 +176,13 @@ jobs:
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Download secrets
|
||||
- name: Decrypt secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
run: |
|
||||
mkdir -p $HOME/secrets
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name store_fdroid-keystore.jks --file ./store/fdroid/keystore.jks --output none
|
||||
mkdir -p ~/secrets
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||
--output ./store/fdroid/keystore.jks ./.github/secrets/store_fdroid-keystore.jks.gpg
|
||||
|
||||
- name: Compile for F-Droid Store
|
||||
env:
|
||||
@@ -217,5 +211,5 @@ jobs:
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
run: npm run deploy
|
||||
|
||||
10
.github/workflows/version-bump.yml
vendored
10
.github/workflows/version-bump.yml
vendored
@@ -191,27 +191,21 @@ jobs:
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Install xmllint
|
||||
run: sudo apt install -y libxml2-utils
|
||||
|
||||
- name: Verify version has been updated
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
run: |
|
||||
# Wait for version to change.
|
||||
while : ; do
|
||||
do
|
||||
echo "Waiting for version to be updated..."
|
||||
git pull --force
|
||||
CURRENT_VERSION=$(xmllint --xpath '
|
||||
string(/manifest/@*[local-name()="versionName"
|
||||
and namespace-uri()="http://schemas.android.com/apk/res/android"])
|
||||
' src/App/Platforms/Android/AndroidManifest.xml)
|
||||
|
||||
# If the versions don't match we continue the loop, otherwise we break out of the loop.
|
||||
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
|
||||
sleep 10
|
||||
done
|
||||
done while [[ "$NEW_VERSION" != "$CURRENT_VERSION" ]]
|
||||
|
||||
- name: Cut RC branch
|
||||
run: |
|
||||
|
||||
11
.github/workflows/workflow-linter.yml
vendored
Normal file
11
.github/workflows/workflow-linter.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Workflow Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<MauiVersion>8.0.7</MauiVersion>
|
||||
<MauiVersion>8.0.4-nightly.*</MauiVersion>
|
||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||
@@ -10,4 +10,4 @@
|
||||
<!-- Uncomment this when Unit Testing-->
|
||||
<!-- <CustomConstants>UT</CustomConstants> -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -2,9 +2,9 @@ project_id_env: _CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: true
|
||||
files:
|
||||
- source: /src/Core/Resources/Localization/AppResources.resx
|
||||
dest: /src/Core/Resources/Localization/%original_file_name%
|
||||
translation: /src/Core/Resources/Localization/AppResources.%two_letters_code%.resx
|
||||
- source: /src/App/Resources/AppResources.resx
|
||||
dest: /src/App/Resources/%original_file_name%
|
||||
translation: /src/App/Resources/AppResources.%two_letters_code%.resx
|
||||
update_option: update_as_unapproved
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
|
||||
@@ -79,29 +79,24 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = context.PackageManager?.GetLaunchIntentForPackage(context.PackageName ?? string.Empty);
|
||||
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 builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId);
|
||||
if(intent != null && context.PackageManager != null && !string.IsNullOrEmpty(context.PackageName))
|
||||
{
|
||||
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 deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
|
||||
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
||||
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
|
||||
|
||||
builder.SetContentIntent(pendingIntent)
|
||||
.SetDeleteIntent(deletePendingIntent);
|
||||
}
|
||||
|
||||
builder.SetContentTitle(title)
|
||||
var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId)
|
||||
.SetContentIntent(pendingIntent)
|
||||
.SetContentTitle(title)
|
||||
.SetContentText(message)
|
||||
.SetSmallIcon(Bit.Core.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);
|
||||
|
||||
@@ -5,8 +5,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public enum AwaiterPrecondition
|
||||
{
|
||||
EnvironmentUrlsInited,
|
||||
AndroidWindowCreated
|
||||
EnvironmentUrlsInited
|
||||
}
|
||||
|
||||
public interface IConditionedAwaiterManager
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Bit.App
|
||||
// This queue keeps those actions so that when the app has resumed they can still be executed.
|
||||
// Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume
|
||||
private readonly Queue<Action> _onResumeActions = new Queue<Action>();
|
||||
private bool _hasNavigatedToAutofillWindow;
|
||||
|
||||
#if ANDROID
|
||||
|
||||
@@ -119,8 +120,41 @@ namespace Bit.App
|
||||
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
||||
}
|
||||
|
||||
_isResumed = true;
|
||||
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
|
||||
//"Internal" Autofill and Uri/Otp/CreateSend. This is where we create the autofill specific Window
|
||||
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null))
|
||||
{
|
||||
_isResumed = true; //Specifically for the Autofill scenario we need to manually set the _isResumed here
|
||||
_hasNavigatedToAutofillWindow = true;
|
||||
return new AutoFillWindow(new NavigationPage(new AndroidNavigationRedirectPage()));
|
||||
}
|
||||
|
||||
var homePage = new HomePage(Options);
|
||||
// WORKAROUND: If the user autofills with Accessibility Services enabled and goes back to the application then there is currently an issue
|
||||
// where this method is called again
|
||||
// thus it goes through here and the user goes to HomePage as we see here.
|
||||
// So to solve this, the next flag check has been added which then turns on a flag on the home page
|
||||
// that will trigger a navigation on the accounts manager when it loads; workarounding this behavior and navigating the user
|
||||
// to the proper page depending on its state.
|
||||
// WARNING: this doens't navigate the user to where they were but it acts as if the user had changed their account.
|
||||
if(_hasNavigatedToAutofillWindow)
|
||||
{
|
||||
homePage.PerformNavigationOnAccountChangedOnLoad = true;
|
||||
// this is needed because when coming back from AutofillWindow OnResume won't be called and we need this flag
|
||||
// so that void Navigate(NavigationTarget navTarget, INavigationParams navParams) doesn't enqueue the navigation
|
||||
// and it performs it directly.
|
||||
_isResumed = true;
|
||||
_hasNavigatedToAutofillWindow = false;
|
||||
}
|
||||
|
||||
//If we have an existing MainAppWindow we can use that one
|
||||
var mainAppWindow = Windows.OfType<MainAppWindow>().FirstOrDefault();
|
||||
if (mainAppWindow != null)
|
||||
{
|
||||
mainAppWindow.PendingPage = new NavigationPage(homePage);
|
||||
}
|
||||
|
||||
//Create new main window
|
||||
return new MainAppWindow(new NavigationPage(homePage));
|
||||
}
|
||||
#else
|
||||
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
|
||||
@@ -137,7 +171,7 @@ namespace Bit.App
|
||||
Application.Current.MainPage = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public App() : this(null)
|
||||
|
||||
@@ -47,7 +47,6 @@ namespace Bit.Core
|
||||
public const string ConfigsKey = "configsKey";
|
||||
public const string DisplayEuEnvironmentFlag = "display-eu-environment";
|
||||
public const string RegionEnvironment = "regionEnvironment";
|
||||
public const string DuoCallback = "bitwarden://duo-callback";
|
||||
|
||||
/// <summary>
|
||||
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
|
||||
|
||||
@@ -53,13 +53,12 @@ namespace Bit.App.Controls
|
||||
if (BindingContext is CipherItemViewModel cipherItemVM)
|
||||
{
|
||||
cipherItemVM.IconImageSuccesfullyLoaded = true;
|
||||
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Icon.IsVisible = cipherItemVM.ShowIconImage;
|
||||
IconPlaceholder.IsVisible = !cipherItemVM.ShowIconImage;
|
||||
});
|
||||
}
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Icon.IsVisible = true;
|
||||
IconPlaceholder.IsVisible = false;
|
||||
});
|
||||
}
|
||||
|
||||
public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e)
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
<Folder Include="Resources\Localization\" />
|
||||
<Folder Include="Controls\Picker\" />
|
||||
<Folder Include="Controls\Avatar\" />
|
||||
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MauiImage Include="Resources\Images\dotnet_bot.svg">
|
||||
@@ -108,6 +107,5 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\Picker\" />
|
||||
<None Remove="Controls\Avatar\" />
|
||||
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,7 +1,6 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -27,7 +26,7 @@ namespace Bit.App.Pages
|
||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||
_identityEntry.ReturnType = ReturnType.Next;
|
||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||
_vm.SubmitSuccessTask = () => MainThread.InvokeOnMainThreadAsync(SubmitSuccessAsync);
|
||||
_vm.SubmitSuccessAction = () => MainThread.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
@@ -38,12 +37,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Navigation.PopModalAsync();
|
||||
#if ANDROID
|
||||
if (Platform.CurrentActivity.CurrentFocus != null)
|
||||
{
|
||||
Platform.CurrentActivity.HideKeyboard(Platform.CurrentActivity.CurrentFocus);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Bit.App.Pages
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string IconsUrl { get; set; }
|
||||
public string NotificationsUrls { get; set; }
|
||||
public Func<Task> SubmitSuccessTask { get; set; }
|
||||
public Action SubmitSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
@@ -73,10 +73,7 @@ namespace Bit.App.Pages
|
||||
IconsUrl = resUrls.Icons;
|
||||
NotificationsUrls = resUrls.Notifications;
|
||||
|
||||
if (SubmitSuccessTask != null)
|
||||
{
|
||||
await SubmitSuccessTask();
|
||||
}
|
||||
SubmitSuccessAction?.Invoke();
|
||||
}
|
||||
|
||||
public bool ValidateUrls()
|
||||
|
||||
@@ -12,14 +12,12 @@ namespace Bit.App.Pages
|
||||
private readonly HomeViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
private IBroadcasterService _broadcasterService;
|
||||
private IConditionedAwaiterManager _conditionedAwaiterManager;
|
||||
|
||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public HomePage(AppOptions appOptions = null)
|
||||
{
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HomeViewModel;
|
||||
@@ -58,8 +56,6 @@ namespace Bit.App.Pages
|
||||
PerformNavigationOnAccountChangedOnLoad = false;
|
||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
}
|
||||
|
||||
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<Button
|
||||
Text="{u:I18n DenyLogIn}"
|
||||
Command="{Binding RejectRequestCommand}"
|
||||
StyleClass="btn-secundary"
|
||||
AutomationId="DenyLoginButton" />
|
||||
|
||||
</StackLayout>
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Bit.App.Pages
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginSsoPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.FromIosExtension = _appOptions?.IosExtension ?? false;
|
||||
_vm.StartTwoFactorAction = () => MainThread.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
|
||||
@@ -15,16 +15,6 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.Authentication;
|
||||
using Microsoft.Maui.Networking;
|
||||
using NetworkAccess = Microsoft.Maui.Networking.NetworkAccess;
|
||||
using Org.BouncyCastle.Asn1.Ocsp;
|
||||
|
||||
#if IOS
|
||||
using AuthenticationServices;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using WebAuthenticator = Bit.Core.Utilities.MAUI.WebAuthenticator;
|
||||
using WebAuthenticatorResult = Bit.Core.Utilities.MAUI.WebAuthenticatorResult;
|
||||
using WebAuthenticatorOptions = Bit.Core.Utilities.MAUI.WebAuthenticatorOptions;
|
||||
#endif
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -74,8 +64,6 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public bool FromIosExtension { get; set; }
|
||||
|
||||
public ICommand LogInCommand { get; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
@@ -165,9 +153,6 @@ namespace Bit.App.Pages
|
||||
CallbackUrl = new Uri(REDIRECT_URI),
|
||||
Url = new Uri(url),
|
||||
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
|
||||
#if IOS
|
||||
ShouldUseSharedApplicationKeyWindow = FromIosExtension
|
||||
#endif
|
||||
});
|
||||
|
||||
var code = GetResultCode(authResult, state);
|
||||
|
||||
@@ -132,26 +132,14 @@
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
Padding="0"
|
||||
IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="FillAndExpand">
|
||||
<Label
|
||||
StyleClass="box"
|
||||
Text="{Binding DuoFramelessLabel}"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
Margin="10,21"
|
||||
IsVisible="{Binding IsDuoFrameless}"/>
|
||||
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}"
|
||||
VerticalOptions="StartAndExpand">
|
||||
<controls:HybridWebView
|
||||
x:Name="_duoWebView"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}"
|
||||
IsVisible="{Binding IsDuoFrameless, Converter={StaticResource inverseBool}}"/>
|
||||
<StackLayout
|
||||
StyleClass="box"
|
||||
VerticalOptions="End">
|
||||
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}" />
|
||||
<StackLayout StyleClass="box" VerticalOptions="End">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n RememberMe}"
|
||||
@@ -163,12 +151,6 @@
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n LaunchDuo}"
|
||||
Margin="10,21"
|
||||
StyleClass="btn-primary"
|
||||
Command="{Binding AuthenticateWithDuoFramelessCommand}"
|
||||
AutomationId="DuoFramelessButton"
|
||||
IsVisible="{Binding IsDuoFrameless}"/>
|
||||
</StackLayout>
|
||||
<StackLayout
|
||||
Spacing="0"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -35,7 +34,6 @@ namespace Bit.App.Pages
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _enableContinue = false;
|
||||
private bool _showContinue = true;
|
||||
private bool _isDuoFrameless = false;
|
||||
private double _duoWebViewHeight;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
@@ -58,7 +56,6 @@ namespace Bit.App.Pages
|
||||
PageTitle = AppResources.TwoStepLogin;
|
||||
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
|
||||
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
AuthenticateWithDuoFramelessCommand = CreateDefaultAsyncRelayCommand(DuoFramelessAuthenticateAsync, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
public string TotpInstruction
|
||||
@@ -106,16 +103,6 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _enableContinue, value);
|
||||
}
|
||||
|
||||
public bool IsDuoFrameless
|
||||
{
|
||||
get => _isDuoFrameless;
|
||||
set => SetProperty(ref _isDuoFrameless, value, additionalPropertyNames: new string[] { nameof(DuoFramelessLabel) });
|
||||
}
|
||||
|
||||
public string DuoFramelessLabel => SelectedProviderType == TwoFactorProviderType.OrganizationDuo ?
|
||||
$"{AppResources.DuoTwoStepLoginIsRequiredForYourAccount} {AppResources.FollowTheStepsFromDuoToFinishLoggingIn}" :
|
||||
AppResources.FollowTheStepsFromDuoToFinishLoggingIn;
|
||||
|
||||
#if IOS
|
||||
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
|
||||
#else
|
||||
@@ -138,7 +125,6 @@ namespace Bit.App.Pages
|
||||
}
|
||||
public ICommand SubmitCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public ICommand AuthenticateWithDuoFramelessCommand { get; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action LockAction { get; set; }
|
||||
public Action StartDeviceApprovalOptionsAction { get; set; }
|
||||
@@ -193,29 +179,15 @@ namespace Bit.App.Pages
|
||||
break;
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
IsDuoFrameless = providerData.ContainsKey("AuthUrl");
|
||||
if (!IsDuoFrameless)
|
||||
SetDuoWebViewHeight();
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
{
|
||||
SetDuoWebViewHeight();
|
||||
var host = WebUtility.UrlEncode(providerData["Host"] as string);
|
||||
var req = WebUtility.UrlEncode(providerData["Signature"] as string);
|
||||
page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}";
|
||||
page.DuoWebView.RegisterAction(sig =>
|
||||
{
|
||||
Token = sig;
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleException(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Token = sig;
|
||||
SubmitCommand.Execute(null);
|
||||
});
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
|
||||
@@ -239,77 +211,6 @@ namespace Bit.App.Pages
|
||||
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
|
||||
}
|
||||
|
||||
private async Task DuoFramelessAuthenticateAsync()
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
|
||||
if (!_authService.TwoFactorProvidersData.TryGetValue(SelectedProviderType.Value, out var providerData) ||
|
||||
!providerData.TryGetValue("AuthUrl", out var urlObject))
|
||||
{
|
||||
throw new InvalidOperationException("Duo authentication error: Could not get ProviderData or AuthUrl");
|
||||
}
|
||||
|
||||
var url = urlObject as string;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
throw new ArgumentNullException("Duo authentication error: Could not get valid auth url");
|
||||
}
|
||||
|
||||
WebAuthenticatorResult authResult;
|
||||
try
|
||||
{
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
|
||||
{
|
||||
Url = new Uri(url),
|
||||
CallbackUrl = new Uri(Constants.DuoCallback)
|
||||
});
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// user canceled
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (authResult == null || authResult.Properties == null)
|
||||
{
|
||||
throw new InvalidOperationException("Duo authentication error: Could not get result from authentication");
|
||||
}
|
||||
|
||||
if (authResult.Properties.TryGetValue("error", out var resultError))
|
||||
{
|
||||
_logger.Error(resultError);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
string code = null;
|
||||
if (authResult.Properties.TryGetValue("code", out var resultCodeData))
|
||||
{
|
||||
code = Uri.UnescapeDataString(resultCodeData);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
throw new ArgumentException("Duo authentication error: response code is null or empty/whitespace");
|
||||
}
|
||||
|
||||
string state = null;
|
||||
if (authResult.Properties.TryGetValue("state", out var resultStateData))
|
||||
{
|
||||
state = Uri.UnescapeDataString(resultStateData);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state))
|
||||
{
|
||||
throw new ArgumentException("Duo authentication error: response state is null or empty/whitespace");
|
||||
}
|
||||
|
||||
Token = $"{code}|{state}";
|
||||
await SubmitAsync(true);
|
||||
}
|
||||
|
||||
public void SetDuoWebViewHeight()
|
||||
{
|
||||
var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density;
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Pages;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Pages;
|
||||
|
||||
public partial class AndroidNavigationRedirectPage : ContentPage
|
||||
{
|
||||
private AppOptions _options;
|
||||
public AndroidNavigationRedirectPage(AppOptions options)
|
||||
{
|
||||
_options = options ?? new AppOptions();
|
||||
private readonly IAccountsManager _accountsManager;
|
||||
|
||||
public AndroidNavigationRedirectPage()
|
||||
{
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
|
||||
{
|
||||
if (ServiceContainer.TryResolve<IAccountsManager>(out var accountsManager))
|
||||
{
|
||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
}
|
||||
else
|
||||
{
|
||||
Bit.App.App.MainPage = new NavigationPage(new HomePage(_options)); //Fallback scenario to load HomePage just in case something goes wrong when resolving IAccountsManager
|
||||
}
|
||||
|
||||
if (ServiceContainer.TryResolve<IConditionedAwaiterManager>(out var conditionedAwaiterManager))
|
||||
{
|
||||
conditionedAwaiterManager?.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("ConditionedAwaiterManager can't be resolved on Android Navigation redirection"));
|
||||
}
|
||||
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +266,6 @@
|
||||
AutomationId="SendShowHideOptionsButton" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsUp"
|
||||
InputTransparent="True"
|
||||
MinimumWidthRequest="25"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
@@ -276,8 +274,6 @@
|
||||
AutomationId="SendOptionsDisplayed" />
|
||||
<controls:IconButton
|
||||
x:Name="_btnOptionsDown"
|
||||
InputTransparent="True"
|
||||
MinimumWidthRequest="25"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
|
||||
StyleClass="box-row-button"
|
||||
TextColor="{DynamicResource PrimaryColor}"
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
x:DataType="pages:SendGroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
Spacing="6"
|
||||
AutomationId="{Binding AutomationId}">
|
||||
<controls:IconLabel Text="{Binding Icon, Mode=OneWay}"
|
||||
HorizontalOptions="Start"
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<Label
|
||||
Text="{u:I18n FingerprintPhrase}"
|
||||
FontSize="Small"
|
||||
Padding="0, 10, 0 ,0"
|
||||
FontAttributes="Bold"/>
|
||||
<controls:MonoLabel
|
||||
FormattedText="{Binding FingerprintPhrase}"
|
||||
@@ -69,70 +70,64 @@
|
||||
Grid.ColumnSpan="2"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<StackLayout
|
||||
x:Key="mainLayout"
|
||||
x:Name="_mainLayout"
|
||||
Padding="0, 10">
|
||||
<RefreshView
|
||||
IsRefreshing="{Binding IsRefreshing}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="FillAndExpand"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<StackLayout>
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
HorizontalOptions="Center"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
SemanticProperties.Description="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
HorizontalTextAlignment="Center"
|
||||
Margin="14,10,14,0"/>
|
||||
<controls:ExtendedCollectionView
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</StackLayout>
|
||||
</RefreshView>
|
||||
<controls:IconLabelButton
|
||||
VerticalOptions="End"
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
</StackLayout>
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<Grid
|
||||
RowDefinitions="*, Auto"
|
||||
Padding="0, 10">
|
||||
<RefreshView
|
||||
Grid.Row="0"
|
||||
IsRefreshing="{Binding IsRefreshing}"
|
||||
Command="{Binding RefreshCommand}"
|
||||
VerticalOptions="Fill"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
<VerticalStackLayout Grid.Row="0"
|
||||
HorizontalOptions="Center">
|
||||
<Image
|
||||
x:Name="_emptyPlaceholder"
|
||||
Source="empty_login_requests"
|
||||
WidthRequest="160"
|
||||
HeightRequest="160"
|
||||
Margin="0,70,0,0"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
SemanticProperties.Description="{u:I18n NoPendingRequests}" />
|
||||
<controls:CustomLabel
|
||||
StyleClass="box-label-regular"
|
||||
Text="{u:I18n NoPendingRequests}"
|
||||
IsVisible="{Binding HasLoginRequests, Converter={StaticResource inverseBool}}"
|
||||
FontAttributes="{OnPlatform iOS=Bold}"
|
||||
FontWeight="500"
|
||||
Margin="14,10,14,0"/>
|
||||
</VerticalStackLayout>
|
||||
<controls:ExtendedCollectionView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding LoginRequests}"
|
||||
ItemTemplate="{StaticResource loginRequestTemplate}"
|
||||
SelectionMode="Single"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
ExtraDataForLogging="Login requests page" >
|
||||
<controls:ExtendedCollectionView.Behaviors>
|
||||
<xct:EventToCommandBehavior
|
||||
EventName="SelectionChanged"
|
||||
Command="{Binding AnswerRequestCommand}"
|
||||
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||
</controls:ExtendedCollectionView.Behaviors>
|
||||
</controls:ExtendedCollectionView>
|
||||
</Grid>
|
||||
</RefreshView>
|
||||
|
||||
<controls:IconLabelButton
|
||||
Grid.Row="1"
|
||||
VerticalOptions="End"
|
||||
Margin="10,0"
|
||||
Icon="{Binding Source={x:Static core:BitwardenIcons.Trash}}"
|
||||
Label="{u:I18n DeclineAllRequests}"
|
||||
ButtonCommand="{Binding DeclineAllRequestsCommand}"
|
||||
IsVisible="{Binding HasLoginRequests}"
|
||||
AutomationId="DeleteAllRequestsButton" />
|
||||
|
||||
<Grid x:Name="_activityIndicatorGrid" Grid.Row="0" Grid.RowSpan="2" BackgroundColor="{DynamicResource BackgroundColor}">
|
||||
<ActivityIndicator IsRunning="True"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ContentView
|
||||
x:Name="_mainContent">
|
||||
</ContentView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
@@ -20,6 +19,7 @@ namespace Bit.App.Pages
|
||||
public LoginPasswordlessRequestsListPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator(_mainContent);
|
||||
_vm = BindingContext as LoginPasswordlessRequestsListViewModel;
|
||||
_vm.Page = this;
|
||||
}
|
||||
@@ -27,21 +27,9 @@ namespace Bit.App.Pages
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
try
|
||||
{
|
||||
_activityIndicatorGrid.IsVisible = true;
|
||||
await LoadOnAppearedAsync(_mainLayout, false, _vm.RefreshAsync, _mainContent);
|
||||
|
||||
await _vm.RefreshAsync();
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activityIndicatorGrid.IsVisible = false;
|
||||
}
|
||||
UpdatePlaceholder();
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
try
|
||||
{
|
||||
IsRefreshing = true;
|
||||
LoginRequests.ReplaceRange(await _authService.GetActivePasswordlessLoginRequestsAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -107,7 +108,7 @@ namespace Bit.App.Pages
|
||||
Origin = loginRequestData.Origin
|
||||
});
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||
}
|
||||
|
||||
private async Task DeclineAllRequestsAsync()
|
||||
|
||||
@@ -2335,15 +2335,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Duo two-step login is required for your account. .
|
||||
/// </summary>
|
||||
public static string DuoTwoStepLoginIsRequiredForYourAccount {
|
||||
get {
|
||||
return ResourceManager.GetString("DuoTwoStepLoginIsRequiredForYourAccount", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit.
|
||||
/// </summary>
|
||||
@@ -3208,15 +3199,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Follow the steps from Duo to finish logging in..
|
||||
/// </summary>
|
||||
public static string FollowTheStepsFromDuoToFinishLoggingIn {
|
||||
get {
|
||||
return ResourceManager.GetString("FollowTheStepsFromDuoToFinishLoggingIn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} is not correctly formatted..
|
||||
/// </summary>
|
||||
@@ -3811,15 +3793,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Launch Duo.
|
||||
/// </summary>
|
||||
public static string LaunchDuo {
|
||||
get {
|
||||
return ResourceManager.GetString("LaunchDuo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website..
|
||||
/// </summary>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="About" xml:space="preserve">
|
||||
<value>عن</value>
|
||||
<value>عن التطبيق</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>أضِف</value>
|
||||
@@ -2822,7 +2822,7 @@
|
||||
<value>مواصلة الاتصال بالدعم؟</value>
|
||||
</data>
|
||||
<data name="ContinueToPrivacyPolicy" xml:space="preserve">
|
||||
<value>هل تريد المتابعة إلى سياسة الخصوصية؟</value>
|
||||
<value>Continue to privacy policy?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>هل تريد المتابعة إلى متجر التطبيقات؟</value>
|
||||
@@ -2844,7 +2844,7 @@
|
||||
<value>لا يمكن العثور على ما تبحث عنه؟ قم بالتواصل مع دعم Bitwarden على bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="PrivacyPolicyDescriptionLong" xml:space="preserve">
|
||||
<value>اطلع على سياستنا للخصوصية على bitwarden.com.</value>
|
||||
<value>Check out our privacy policy on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>استكشف المزيد من الميزات لحساب Bitwarden الخاص بك على تطبيق الويب.</value>
|
||||
|
||||
@@ -907,7 +907,7 @@ Skanning vil ske automatisk.</value>
|
||||
<value>Kopiér TOTP</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomaticallyDescription" xml:space="preserve">
|
||||
<value>Har et login en godkendelsesnøgle, kopiér da TOTP-bekræftelseskoden til udklipsholderen, når login autoudfyldes.</value>
|
||||
<value>Har et login en godkendelsesnøgle, så kopiér TOTP-bekræftelseskoden til udklipsholderen, når login auto-udfyldes.</value>
|
||||
</data>
|
||||
<data name="CopyTotpAutomatically" xml:space="preserve">
|
||||
<value>Kopiér TOTP automatisk</value>
|
||||
@@ -2128,7 +2128,7 @@ Skanning vil ske automatisk.</value>
|
||||
<value>Denne organisation har en virksomhedspolitik, der automatisk tilmelder dig til nulstilling af adgangskode. Tilmelding giver organisationsadministratorer mulighed for at skifte din hovedadgangskode.</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutPolicyInEffect" xml:space="preserve">
|
||||
<value>Organisationspolitikkerne har sat den maksimalt tilladte bokstimeout til {0} tim(er) og {1} minut(ter).</value>
|
||||
<value>Dine organisationspolitikker påvirker din boks-timeout. Maksimum tilladte boks-timeout er {0} time(r) og {1} minut(ter)</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutPolicyWithActionInEffect" xml:space="preserve">
|
||||
<value>Organisationspolitikkerne påvirker boks-timeout. Maks. tilladt boks-timeout er {0} time(r) og {1} minut(ter). Boks-timeout er pt. sat til {2}.</value>
|
||||
@@ -2354,7 +2354,7 @@ vælg Tilføj TOTP for at gemme nøglen sikkert</value>
|
||||
<value>Godkend loginanmodninger</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Brug denne enhed til at godkende loginanmodninger fra andre enheder</value>
|
||||
<value>Brug denne enhed til at godkende loginanmodninger fra andre enheder.</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Tillad notifikationer</value>
|
||||
@@ -2668,7 +2668,7 @@ Vil du skifte til denne konto?</value>
|
||||
<value>Hjælp til genanmodning om hovedadgangskode</value>
|
||||
</data>
|
||||
<data name="UnlockingMayFailDueToInsufficientMemoryDecreaseYourKDFMemorySettingsToResolve" xml:space="preserve">
|
||||
<value>Oplåsning kan fejle grundet utilstrækkelig hukommelse. Reducér KDF-hukommelsesindstillinger eller opsæt biometrisk oplåsning for at afhjælpe.</value>
|
||||
<value>Unlocking may fail due to insufficient memory. Decrease your KDF memory settings or set up biometric unlock to resolve.</value>
|
||||
</data>
|
||||
<data name="InvalidAPIKey" xml:space="preserve">
|
||||
<value>Ugyldig API-nøgle</value>
|
||||
|
||||
@@ -2820,7 +2820,7 @@
|
||||
<value>Συνέχεια στην επικοινωνία με την υποστήριξη;</value>
|
||||
</data>
|
||||
<data name="ContinueToPrivacyPolicy" xml:space="preserve">
|
||||
<value>Συνέχεια στην πολιτική απορρήτου;</value>
|
||||
<value>Continue to privacy policy?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Συνέχεια στο κατάστημα εφαρμογών;</value>
|
||||
@@ -2842,7 +2842,7 @@
|
||||
<value>Δεν μπορείτε να βρείτε αυτό που ψάχνετε; Επικοινωνήστε με την υποστήριξη Bitwarden στο bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="PrivacyPolicyDescriptionLong" xml:space="preserve">
|
||||
<value>Δείτε την πολιτική απορρήτου μας στο bitwarden.com.</value>
|
||||
<value>Check out our privacy policy on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Εξερευνήστε περισσότερες δυνατότητες του Bitwarden λογαριασμού σας, στην εφαρμογή διαδικτύου.</value>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToDelete" xml:space="preserve">
|
||||
<value>Sei sicuro di volerlo eliminare? Questa operazione è irreversibile.</value>
|
||||
<value>Vuoi davvero eliminarlo? Questa operazione non può essere annullata.</value>
|
||||
<comment>Confirmation alert message when deleteing something.</comment>
|
||||
</data>
|
||||
<data name="Edit" xml:space="preserve">
|
||||
@@ -193,10 +193,10 @@
|
||||
<value>Contattaci</value>
|
||||
</data>
|
||||
<data name="EmailUsDescription" xml:space="preserve">
|
||||
<value>Inviaci un'email per ottenere aiuto o lasciare un feedback.</value>
|
||||
<value>Mandaci un'email per ottenere aiuto o lasciare un feedback</value>
|
||||
</data>
|
||||
<data name="EnterPIN" xml:space="preserve">
|
||||
<value>Inserisci il tuo PIN.</value>
|
||||
<value>Digita il tuo PIN.</value>
|
||||
</data>
|
||||
<data name="Favorites" xml:space="preserve">
|
||||
<value>Preferiti</value>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToDelete" xml:space="preserve">
|
||||
<value>Vai tiešām izdzēst? To nevar atsaukt.</value>
|
||||
<value>Vai jūs tiešām vēlaties dzēst? Šo darbību nav iespējams atsaukt.</value>
|
||||
<comment>Confirmation alert message when deleteing something.</comment>
|
||||
</data>
|
||||
<data name="Edit" xml:space="preserve">
|
||||
@@ -193,10 +193,10 @@
|
||||
<value>Nosūtīt mums e-pasta ziņu</value>
|
||||
</data>
|
||||
<data name="EmailUsDescription" xml:space="preserve">
|
||||
<value>Nosūtīt mums e-pasta ziņu, lai saņemtu palīdzību vai sniegtu atsauksmes.</value>
|
||||
<value>Sūti mums e-pasta ziņu, lai saņemtu palīdzību vai sniegtu atsauksmes!</value>
|
||||
</data>
|
||||
<data name="EnterPIN" xml:space="preserve">
|
||||
<value>Jāievada PIN kods.</value>
|
||||
<value>Ievadīt PIN kodu.</value>
|
||||
</data>
|
||||
<data name="Favorites" xml:space="preserve">
|
||||
<value>Izlase</value>
|
||||
@@ -232,7 +232,7 @@
|
||||
<value>Mape atjaunota.</value>
|
||||
</data>
|
||||
<data name="GoToWebsite" xml:space="preserve">
|
||||
<value>Doties uz tīmekļvietni</value>
|
||||
<value>Doties uz tīmekļa vietni</value>
|
||||
<comment>The button text that allows user to launch the website to their web browser.</comment>
|
||||
</data>
|
||||
<data name="HelpAndFeedback" xml:space="preserve">
|
||||
@@ -243,18 +243,18 @@
|
||||
<comment>Hide a secret value that is currently shown (password).</comment>
|
||||
</data>
|
||||
<data name="InternetConnectionRequiredMessage" xml:space="preserve">
|
||||
<value>Lūgums izveidot savienojumu ar internetu, lai varētu turpināt.</value>
|
||||
<value>Lūgums izveidot savienojumu ar tīmekli, lai varētu turpināt.</value>
|
||||
<comment>Description message for the alert when internet connection is required to continue.</comment>
|
||||
</data>
|
||||
<data name="InternetConnectionRequiredTitle" xml:space="preserve">
|
||||
<value>Nepieciešams interneta savienojums</value>
|
||||
<value>Nepieciešams tīmekļa savienojums</value>
|
||||
<comment>Title for the alert when internet connection is required to continue.</comment>
|
||||
</data>
|
||||
<data name="InvalidMasterPassword" xml:space="preserve">
|
||||
<value>Nederīga galvenā parole. Jāmēģina vēlreiz.</value>
|
||||
<value>Nederīga galvenā parole. Mēģini vēlreiz!</value>
|
||||
</data>
|
||||
<data name="InvalidPIN" xml:space="preserve">
|
||||
<value>Nederīgs PIN. Jāmēģina vēlreiz.</value>
|
||||
<value>Nederīgs PIN. Mēģini vēlreiz!</value>
|
||||
</data>
|
||||
<data name="Launch" xml:space="preserve">
|
||||
<value>Palaist</value>
|
||||
@@ -394,10 +394,10 @@
|
||||
<value>Skatīt</value>
|
||||
</data>
|
||||
<data name="VisitOurWebsite" xml:space="preserve">
|
||||
<value>Apmeklēt mūsu tīmekļvietni</value>
|
||||
<value>Apmeklēt mūsu tīmekļa vietni</value>
|
||||
</data>
|
||||
<data name="Website" xml:space="preserve">
|
||||
<value>Tīmekļvietne</value>
|
||||
<value>Tīmekļa vietne</value>
|
||||
<comment>Label for a website.</comment>
|
||||
</data>
|
||||
<data name="Yes" xml:space="preserve">
|
||||
@@ -419,7 +419,7 @@
|
||||
<value>Izmantot Bitwarden pieejamības pakalpojumu, lai automātiski aizpildītu pieteikšanās veidnes dažādās lietotnēs un tīmeklī.</value>
|
||||
</data>
|
||||
<data name="AutofillService" xml:space="preserve">
|
||||
<value>Automātiskās aizpildes pakalpojums</value>
|
||||
<value>Automātiskā aizpilde</value>
|
||||
</data>
|
||||
<data name="AvoidAmbiguousCharacters" xml:space="preserve">
|
||||
<value>Izvairīties no viegli sajaucamām rakstzīmēm</value>
|
||||
@@ -434,7 +434,7 @@
|
||||
<value>Izmantot Bitwarden Safari un citās lietotnēs, lai automātiski ievadītu pieteikšanās vienumus.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillService" xml:space="preserve">
|
||||
<value>Bitwarden automātiskās aizpildes pakalpojums</value>
|
||||
<value>Bitwarden automātiskā aizpilde</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillAccessibilityServiceDescription" xml:space="preserve">
|
||||
<value>Izmantot Bitwarden pieejamības pakalpojumu, lai automātiski aizpildītu pieteikšanās veidnes.</value>
|
||||
@@ -443,7 +443,7 @@
|
||||
<value>Mainīt e-pasta adresi</value>
|
||||
</data>
|
||||
<data name="ChangeEmailConfirmation" xml:space="preserve">
|
||||
<value>E-pasta adresi ir iespējams nomainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?</value>
|
||||
<value>E-pasta adresi ir iespējams nomainīt bitwarden.com tīmekļa glabātavā. Vai apmeklēt tīmekļa vietni?</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Mainīt galveno paroli</value>
|
||||
@@ -517,7 +517,7 @@
|
||||
<value>Ievietot vienumus</value>
|
||||
</data>
|
||||
<data name="ImportItemsConfirmation" xml:space="preserve">
|
||||
<value>Daudzu vienumu ievietošanu var veikt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?</value>
|
||||
<value>Daudzu vienumu ievietošanu var veikt bitwarden.com tīmekļa glabātavā. Vai apmeklēt tīmekļa vietni?</value>
|
||||
</data>
|
||||
<data name="ImportItemsDescription" xml:space="preserve">
|
||||
<value>Ātri ievietot daudzus vienumus no citām paroļu pārvaldības lietotnēm.</value>
|
||||
@@ -607,7 +607,7 @@
|
||||
<value>Glabātavā nav vienumu.</value>
|
||||
</data>
|
||||
<data name="NoItemsTap" xml:space="preserve">
|
||||
<value>Glabātāvā nav vienumu, ko izmantot šajā tīmekļvietnē/lietotnē. Piesist, lai pievienotu.</value>
|
||||
<value>Glabātāvā nav vienumu, ko izmantot šajā tīmekļa vietnē/lietotnē. Piesist, lai pievienotu.</value>
|
||||
</data>
|
||||
<data name="NoUsernamePasswordConfigured" xml:space="preserve">
|
||||
<value>Šajā pieteikšanās vienumā nav norādīts lietotājvārds vai parole.</value>
|
||||
@@ -748,7 +748,7 @@
|
||||
<value>Kad tiek atlasīts ievades lauks un parādās Bitwarden automātiskās aizpildes lodziņš, jāpiesit tam, lai palaistu automātiskās aizpildes pakalpojumu.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceNotificationContent" xml:space="preserve">
|
||||
<value>Jāpiesit šim paziņojumam, lai automātiski aizpildītu ar vienumu no glabātavas.</value>
|
||||
<value>Piesist šim paziņojumam, lai automātiski aizpildītu, izmantojot vienumu no glabātavas.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceOpenAccessibilitySettings" xml:space="preserve">
|
||||
<value>Atvērt pieejamības iestatījumus</value>
|
||||
@@ -775,7 +775,7 @@
|
||||
<value>Stāvoklis</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceAlert2" xml:space="preserve">
|
||||
<value>Vieglākais veids, kā glabātavā pievienot jaunus pieteikšanās vienumus, ir ar Bitwarden automātiskās aizpildes pakalpojumu. Vairāk par Bitwarden automātisko aizpildi var uzzināt iestatījumu skatā.</value>
|
||||
<value>Vieglākais veids, kā glabātavā pievienot jaunus pieteikšanās vienumus, ir no Bitwarden automātiskās aizpildes pakalpojuma. Vairāk par Bitwarden automātiskās aizpildi var uzzināt iestatījumu skatā.</value>
|
||||
</data>
|
||||
<data name="Autofill" xml:space="preserve">
|
||||
<value>Automātiskā aizpilde</value>
|
||||
@@ -1134,7 +1134,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Derīgums</value>
|
||||
</data>
|
||||
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||
<value>Rādīt tīmekļvietņu ikonas</value>
|
||||
<value>Rādīt vietņu ikonas</value>
|
||||
</data>
|
||||
<data name="ShowWebsiteIconsDescription" xml:space="preserve">
|
||||
<value>Attēlot atpazīstamu attēlu pie katra pieteikšanās vienuma.</value>
|
||||
@@ -1143,7 +1143,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Ikonu servera URL</value>
|
||||
</data>
|
||||
<data name="AutofillWithBitwarden" xml:space="preserve">
|
||||
<value>Automātiski aizpildīt ar Bitwarden</value>
|
||||
<value>Automātiskā aizpilde ar Bitwarden</value>
|
||||
</data>
|
||||
<data name="VaultIsLocked" xml:space="preserve">
|
||||
<value>Glabātava ir slēgta</value>
|
||||
@@ -1170,7 +1170,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Bitwarden automātiskās aizpildes pakalpojums izmanto Android automātiskās aizpildes ietvaru, lai palīdzētu aizpildīt pieteikšanās, kredītkartes, un identitātes informācijas veidnes citās ierīces lietotnēs.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceDescription" xml:space="preserve">
|
||||
<value>Bitwarden automātiskā aizpilde ir izmantojama, lai aizpildītu pieteikšanās, kredītkaršu un identitātes informācijas veidnes citās lietotnēs.</value>
|
||||
<value>Bitwarden automātiskās aizpilde tiek izmantota, lai aizpildītu pieteikšanās, kredītkaršu un identitātes informācijas veidnes citās lietotnēs.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceOpenAutofillSettings" xml:space="preserve">
|
||||
<value>Atvērt automātiskās aizpildes iestatījumus</value>
|
||||
@@ -1289,7 +1289,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Vispirms ir jāpiesakās galvenajā Bitwarden lietotnē, lai varētu izmantot automātisko aizpildi.</value>
|
||||
</data>
|
||||
<data name="AutofillSetup" xml:space="preserve">
|
||||
<value>Pieteikšanās vienumi tagad ir viegli pieejami tastatūrā, kad notiek pieteikšanās lietotnēs un tīmekļvietnēs.</value>
|
||||
<value>Pieteikšanāš vienumi tagad ir viegli pieejami tastatūrā, kad notiek pieteikšanās lietotnēs un tīmekļa vietnēs.</value>
|
||||
</data>
|
||||
<data name="AutofillSetup2" xml:space="preserve">
|
||||
<value>Mēs iesakām atspējot jebkuras citas automātiskās aizpildes lietotnes iestatījumos, ja nav iecerēts izmantot tās.</value>
|
||||
@@ -1462,7 +1462,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<comment>A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing.</comment>
|
||||
</data>
|
||||
<data name="LearnOrgConfirmation" xml:space="preserve">
|
||||
<value>Bitwarden nodrošina iespēju kopīgot glabātavas vienumus ar citiem, kad tiek izmantots apvienības konts. Vai apmeklēt bitwarden.com tīmekļvietni, lai uzzinātu vairāk?</value>
|
||||
<value>Bitwarden nodrošina iespēju kopīgot glabātavas vienumus ar citiem, kad tiek izmantots apvienības konts. Vai apmeklēt bitwarden.com tīmekļa vietni, lai uzzinātu vairāk?</value>
|
||||
</data>
|
||||
<data name="ExportVault" xml:space="preserve">
|
||||
<value>Izgūt glabātavas saturu</value>
|
||||
@@ -1586,7 +1586,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Pārsāknējot lietotni</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Automātiskā aizpildīšana padara drošu piekļuvi Bitwarden galbātavai vienkāršu no citām tīmekļvietnēm un lietotnēm. Izskatās, ka nav iespējota Bitwarden automātiskā aizpildīšana. To var izdarīt iestatījumu sadaļā.</value>
|
||||
<value>Automātiskā aizpilde padara drošu piekļuvi Bitwarden galbātavai vienkāršu no citām 0tīmekļa vietnēm un lietotnēm. Izskatās, ka nav iespējota Bitwarden automātiskā aizpilde. To var izdarīt iestatījumu sadaļā.</value>
|
||||
</data>
|
||||
<data name="ThemeAppliedOnRestart" xml:space="preserve">
|
||||
<value>Izskata izmaiņas tiks pielietotas pēc lietotnes pārsāknēšanas.</value>
|
||||
@@ -1729,7 +1729,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<comment>(action prompt) Label for the search text field when viewing the trash folder</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToPermanentlyDeleteCipher" xml:space="preserve">
|
||||
<value>Vai tiešām neatgriezeniski izdzēst? To nevar atsaukt.</value>
|
||||
<value>Vai tiešām vēlaties neatgriezeniski dzēst? To nevar atsaukt.</value>
|
||||
<comment>Confirmation alert message when permanently deleteing a cipher.</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToRestoreCipher" xml:space="preserve">
|
||||
@@ -2213,7 +2213,7 @@ Nolasīšana notiks automātiski.</value>
|
||||
<value>Atgadījās kļūda, kad apliecinājuma kods tika sūtīts uz e-pastu. Lūgums mēģināt vēlreiz</value>
|
||||
</data>
|
||||
<data name="EnterTheVerificationCodeThatWasSentToYourEmail" xml:space="preserve">
|
||||
<value>Jāievada apliecinājuma kods, kas tika nosūtīts e-pastā</value>
|
||||
<value>Ievadīt pārbaudes kodu, kas tika nosūtīts e-pastā</value>
|
||||
</data>
|
||||
<data name="SubmitCrashLogs" xml:space="preserve">
|
||||
<value>Iesniegt avāriju ierakstus</value>
|
||||
@@ -2442,7 +2442,7 @@ jāizvēlas "Pievienot TOTP", lai droši glabātu atslēgu.</value>
|
||||
<value>E-pasta veids</value>
|
||||
</data>
|
||||
<data name="WebsiteRequired" xml:space="preserve">
|
||||
<value>Tīmekļviente (nepieciešama)</value>
|
||||
<value>Mājaslapa (nepieciešama)</value>
|
||||
</data>
|
||||
<data name="UnknownXErrorMessage" xml:space="preserve">
|
||||
<value>Notika nezināma kļūda {0}.</value>
|
||||
@@ -2466,7 +2466,7 @@ jāizvēlas "Pievienot TOTP", lai droši glabātu atslēgu.</value>
|
||||
<value>Pieejamības pakalpojuma izmantošanas skaidrojums</value>
|
||||
</data>
|
||||
<data name="AccessibilityDisclosureText" xml:space="preserve">
|
||||
<value>Bitwarden izmanto pieejamības pakalpojumu, lai meklētu pieteikšanās laukus lietotnēs un tīmekļvietnēs, tad noskaidro atbistošus lauku identifikatorus lietotājvārda un paroles ievadīšanai, kad ir atrasta atbilstība lietotnei vai vietnei. Bitwarden neglabā neko no informācijas, ko nodrošina pakalpojums, kā arī nemēģina pārvaldīt ekrānā redzamās daļas, kas nav saistītas ar pieteikšanās datu ievadi.</value>
|
||||
<value>Bitwarden izmanto pieejamības pakalpojumu, lai meklētu pieteikšanās laukus lietotnēs un tīmekļa vietnēs, tad noskaidro atbistošus lauku identifikatorus lietotājvārda un paroles ievadīšanai, kad ir atrasta atbilstība lietotnei vai vietnei. Bitwarden neglabā neko no informācijas, ko nodrošina pakalpojums, kā arī nemēģina pārvaldīt ekrānā redzamās daļas, kas nav saistītas ar pieteikšanās datu ievadi.</value>
|
||||
</data>
|
||||
<data name="Accept" xml:space="preserve">
|
||||
<value>Pieņemt</value>
|
||||
@@ -2850,7 +2850,7 @@ Vai pārslēgties uz šo kontu?</value>
|
||||
<value>Vairāk sava Bitwarden konta iespēju var izpētīt tīmekļa vietnē.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>Bitwarden nodrošina iespēju kopīgot savas glabātavas vienumus ar citiem, kad tiek izmantota apvienība. Vairāk var uzzināt bitwarden.com tīmekļvietnē.</value>
|
||||
<value>Bitwarden nodrošina iespēju kopīgot savas glabātavas vienumus ar citiem, kad tiek izmantota apvienība. Vairāk var uzzināt bitwarden.com tīmekļa vietnē.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Var palīdzēt citiem noskaidrot, vai Bitwarden tiem der. To var izdarīt lietotņu veikalā, atstājot vērtējumu.</value>
|
||||
|
||||
@@ -2822,16 +2822,16 @@ Você deseja mudar para esta conta?</value>
|
||||
<value>Continuar e contatar o suporte?</value>
|
||||
</data>
|
||||
<data name="ContinueToPrivacyPolicy" xml:space="preserve">
|
||||
<value>Continuar para a política de privacidade?</value>
|
||||
<value>Continue to privacy policy?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continuar para a loja de apps?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Torne sua conta mais segura configurando o login em duas etapas no aplicativo web do Bitwarden.</value>
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Você pode alterar a sua senha mestra no aplicativo web Bitwarden.</value>
|
||||
<value>You can change your master password on the Bitwarden web app.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Você pode importar dados para o seu cofre no {0}.</value>
|
||||
@@ -2841,19 +2841,19 @@ Você deseja mudar para esta conta?</value>
|
||||
<value>Saiba mais sobre como usar o Bitwarden no centro de ajuda.</value>
|
||||
</data>
|
||||
<data name="ContactSupportDescriptionLong" xml:space="preserve">
|
||||
<value>Não encontrou o que está procurando? Entre em contato com o suporte do Bitwarden em bitwarden.com.</value>
|
||||
<value>Can’t find what you are looking for? Reach out to Bitwarden support on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="PrivacyPolicyDescriptionLong" xml:space="preserve">
|
||||
<value>Confira a nossa política de privacidade em bitwarden.com.</value>
|
||||
<value>Check out our privacy policy on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore mais recursos da sua conta no Bitwarden no aplicativo web.</value>
|
||||
<value>Explore more features of your Bitwarden account on the web app.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>O Bitwarden permite compartilhar os seus itens do cofre com outros ao utilizar uma organização. Gostaria de visitar o site bitwarden.com para saber mais?</value>
|
||||
<value>Bitwarden allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Ajude outros a descobrir se o Bitwarden é adequado para eles. Visite a loja de aplicativos e deixe uma avaliação agora.</value>
|
||||
<value>Help others find out if Bitwarden is right for them. Visit the app store and leave a rating now.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Usar o tema escuro quando o modo escuro do seu dispositivo estiver ativado</value>
|
||||
@@ -2869,12 +2869,12 @@ Você deseja mudar para esta conta?</value>
|
||||
<value>Conta desconectada.</value>
|
||||
</data>
|
||||
<data name="YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword" xml:space="preserve">
|
||||
<value>As permissões da sua organização foram atualizadas, exigindo que você defina uma senha mestra.</value>
|
||||
<value>Your organization permissions were updated, requiring you to set a master password.</value>
|
||||
</data>
|
||||
<data name="YourOrganizationRequiresYouToSetAMasterPassword" xml:space="preserve">
|
||||
<value>Sua organização requer que você defina uma senha mestra.</value>
|
||||
<value>Your organization requires you to set a master password.</value>
|
||||
</data>
|
||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||
<value>Configure um método de desbloqueio para alterar o tempo limite do cofre.</value>
|
||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToDelete" xml:space="preserve">
|
||||
<value>Pretende mesmo eliminar? Esta ação não pode ser anulada.</value>
|
||||
<value>Pretende mesmo eliminar? Isto não pode ser anulado.</value>
|
||||
<comment>Confirmation alert message when deleteing something.</comment>
|
||||
</data>
|
||||
<data name="Edit" xml:space="preserve">
|
||||
@@ -193,7 +193,7 @@
|
||||
<value>Enviar-nos um e-mail</value>
|
||||
</data>
|
||||
<data name="EmailUsDescription" xml:space="preserve">
|
||||
<value>Envie-nos um e-mail diretamente para obter ajuda ou dar feedback.</value>
|
||||
<value>Envie-nos um e-mail diretamente para obter ajuda ou deixar feedback.</value>
|
||||
</data>
|
||||
<data name="EnterPIN" xml:space="preserve">
|
||||
<value>Introduza o seu código PIN.</value>
|
||||
@@ -206,7 +206,7 @@
|
||||
<value>Apresentar um relatório de erros</value>
|
||||
</data>
|
||||
<data name="FileBugReportDescription" xml:space="preserve">
|
||||
<value>Abra um problema no nosso repositório do GitHub.</value>
|
||||
<value>Abrir um issue no nosso repositório do GitHub.</value>
|
||||
</data>
|
||||
<data name="FingerprintDirection" xml:space="preserve">
|
||||
<value>Utilize a sua impressão digital para verificar.</value>
|
||||
@@ -443,7 +443,7 @@
|
||||
<value>Alterar e-mail</value>
|
||||
</data>
|
||||
<data name="ChangeEmailConfirmation" xml:space="preserve">
|
||||
<value>Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Pretende visitar o site agora?</value>
|
||||
<value>Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPassword" xml:space="preserve">
|
||||
<value>Alterar palavra-passe mestra</value>
|
||||
@@ -511,13 +511,13 @@
|
||||
<value>Gerar palavra-passe</value>
|
||||
</data>
|
||||
<data name="GetPasswordHint" xml:space="preserve">
|
||||
<value>Obter a dica da sua palavra-passe mestra</value>
|
||||
<value>Obter dica da palavra-passe mestra</value>
|
||||
</data>
|
||||
<data name="ImportItems" xml:space="preserve">
|
||||
<value>Importar itens</value>
|
||||
</data>
|
||||
<data name="ImportItemsConfirmation" xml:space="preserve">
|
||||
<value>Pode importar itens em massa do cofre do site bitwarden.com. Pretende visitar o site agora?</value>
|
||||
<value>Pode importar itens em massa do cofre do site bitwarden.com. Deseja visitar o site agora?</value>
|
||||
</data>
|
||||
<data name="ImportItemsDescription" xml:space="preserve">
|
||||
<value>Importe rapidamente e em massa os seus itens de outras aplicações de gestão de palavras-passe.</value>
|
||||
@@ -613,7 +613,7 @@
|
||||
<value>Esta credencial não tem um nome de utilizador ou palavra-passe configurados.</value>
|
||||
</data>
|
||||
<data name="OkGotIt" xml:space="preserve">
|
||||
<value>Ok, percebido!</value>
|
||||
<value>Ok, entendido!</value>
|
||||
<comment>Confirmation, like "Ok, I understand it"</comment>
|
||||
</data>
|
||||
<data name="OptionDefaults" xml:space="preserve">
|
||||
@@ -718,7 +718,7 @@
|
||||
<value>Ver item</value>
|
||||
</data>
|
||||
<data name="WebVault" xml:space="preserve">
|
||||
<value>Cofre Web Bitwarden</value>
|
||||
<value>Cofre web Bitwarden</value>
|
||||
</data>
|
||||
<data name="Lost2FAApp" xml:space="preserve">
|
||||
<value>Perdeu a aplicação de autenticação?</value>
|
||||
@@ -895,7 +895,7 @@
|
||||
A leitura será efetuada automaticamente.</value>
|
||||
</data>
|
||||
<data name="ScanQrTitle" xml:space="preserve">
|
||||
<value>Ler código QR</value>
|
||||
<value>Digitalizar código QR</value>
|
||||
</data>
|
||||
<data name="Camera" xml:space="preserve">
|
||||
<value>Câmara</value>
|
||||
@@ -964,7 +964,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Os URLs de ambiente foram guardados.</value>
|
||||
</data>
|
||||
<data name="FormattedIncorrectly" xml:space="preserve">
|
||||
<value>{0} não está corretamente formatado.</value>
|
||||
<value>{0} não está formatado corretamente.</value>
|
||||
<comment>Validation error when something is not formatted correctly, such as a URL or email address.</comment>
|
||||
</data>
|
||||
<data name="IdentityUrl" xml:space="preserve">
|
||||
@@ -981,7 +981,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>URL do servidor</value>
|
||||
</data>
|
||||
<data name="WebVaultUrl" xml:space="preserve">
|
||||
<value>URL do servidor do cofre Web</value>
|
||||
<value>URL do servidor do cofre web</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceNotificationContentOld" xml:space="preserve">
|
||||
<value>Toque nesta notificação para ver itens do seu cofre.</value>
|
||||
@@ -1131,7 +1131,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Endereço</value>
|
||||
</data>
|
||||
<data name="Expiration" xml:space="preserve">
|
||||
<value>Prazo de validade</value>
|
||||
<value>Expiração</value>
|
||||
</data>
|
||||
<data name="ShowWebsiteIcons" xml:space="preserve">
|
||||
<value>Mostrar ícones do site</value>
|
||||
@@ -1201,7 +1201,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Booleano</value>
|
||||
</data>
|
||||
<data name="FieldTypeHidden" xml:space="preserve">
|
||||
<value>Oculto</value>
|
||||
<value>Ocultado</value>
|
||||
</data>
|
||||
<data name="FieldTypeLinked" xml:space="preserve">
|
||||
<value>Associado</value>
|
||||
@@ -1263,7 +1263,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>An entity of multiple related people (ex. a team or business organization).</comment>
|
||||
</data>
|
||||
<data name="HoldYubikeyNearTop" xml:space="preserve">
|
||||
<value>Segure a sua Yubikey perto da parte superior do dispositivo.</value>
|
||||
<value>Segure a sua Yubikey perto do topo do dispositivo.</value>
|
||||
</data>
|
||||
<data name="TryAgain" xml:space="preserve">
|
||||
<value>Tentar novamente</value>
|
||||
@@ -1310,7 +1310,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>3. Toque em "Autopreenchimento de palavras-passe"</value>
|
||||
</data>
|
||||
<data name="AutofillTurnOn4" xml:space="preserve">
|
||||
<value>4. Ative o Autopreenchimento</value>
|
||||
<value>4. Ligue o Autopreenchimento</value>
|
||||
</data>
|
||||
<data name="AutofillTurnOn5" xml:space="preserve">
|
||||
<value>5. Selecione "Bitwarden"</value>
|
||||
@@ -1369,10 +1369,10 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Tipos</value>
|
||||
</data>
|
||||
<data name="NoPasswordsToList" xml:space="preserve">
|
||||
<value>Não há palavras-passe a enumerar.</value>
|
||||
<value>Nenhuma palavra-passe para listar.</value>
|
||||
</data>
|
||||
<data name="NoItemsToList" xml:space="preserve">
|
||||
<value>Não há itens a enumerar.</value>
|
||||
<value>Não existem itens para listar.</value>
|
||||
</data>
|
||||
<data name="SearchCollection" xml:space="preserve">
|
||||
<value>Procurar na coleção</value>
|
||||
@@ -1406,7 +1406,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Quem é o proprietário deste item?</value>
|
||||
</data>
|
||||
<data name="NoCollectionsToList" xml:space="preserve">
|
||||
<value>Não há coleções a enumerar.</value>
|
||||
<value>Não existem coleções para listar.</value>
|
||||
</data>
|
||||
<data name="MovedItemToOrg" xml:space="preserve">
|
||||
<value>{0} movido para {1}.</value>
|
||||
@@ -1428,7 +1428,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Mover para organização</value>
|
||||
</data>
|
||||
<data name="NoOrgsToList" xml:space="preserve">
|
||||
<value>Não há organizações a enumerar.</value>
|
||||
<value>Nenhuma organização para listar.</value>
|
||||
</data>
|
||||
<data name="MoveToOrgDesc" xml:space="preserve">
|
||||
<value>Escolha uma organização para a qual pretende mover este item. Mover para uma organização transfere a propriedade do item para essa organização. Deixará de ser o proprietário direto deste item depois de este ter sido movido.</value>
|
||||
@@ -1451,7 +1451,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>Short for "Password Generator"</comment>
|
||||
</data>
|
||||
<data name="NoFoldersToList" xml:space="preserve">
|
||||
<value>Não há pastas a enumerar.</value>
|
||||
<value>Não existem pastas para listar.</value>
|
||||
</data>
|
||||
<data name="FingerprintPhrase" xml:space="preserve">
|
||||
<value>Frase de impressão digital</value>
|
||||
@@ -1556,7 +1556,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Sair</value>
|
||||
</data>
|
||||
<data name="ExitConfirmation" xml:space="preserve">
|
||||
<value>Tem a certeza de que pretende sair do Bitwarden?</value>
|
||||
<value>Tem a certeza de que deseja sair do Bitwarden?</value>
|
||||
</data>
|
||||
<data name="PINRequireMasterPasswordRestart" xml:space="preserve">
|
||||
<value>Pretende exigir o desbloqueio com a sua palavra-passe mestra quando a aplicação é reiniciada?</value>
|
||||
@@ -1570,7 +1570,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SolarizedDark" xml:space="preserve">
|
||||
<value>Solarized Dark</value>
|
||||
<value>Solarized (escuro)</value>
|
||||
<comment>'Solarized Dark' is the name of a specific color scheme. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="AutofillBlockedUris" xml:space="preserve">
|
||||
@@ -1583,7 +1583,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Pedir para adicionar um item se não o encontrar no seu cofre.</value>
|
||||
</data>
|
||||
<data name="OnRestart" xml:space="preserve">
|
||||
<value>Ao reiniciar a aplicação</value>
|
||||
<value>Quando reiniciar a aplicação</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>O preenchimento automático facilita o acesso seguro ao seu cofre do Bitwarden a partir de outros sites e aplicações. Parece que o utilizador não configurou um serviço de preenchimento automático do Bitwarden. Configure o preenchimento automático do Bitwarden a partir do ecrã "Definições".</value>
|
||||
@@ -1617,7 +1617,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Biometria</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Utilizar a biometria para desbloquear</value>
|
||||
<value>Utilizar biometria para desbloquear</value>
|
||||
</data>
|
||||
<data name="AccessibilityOverlayPermissionAlert" xml:space="preserve">
|
||||
<value>O Bitwarden precisa de atenção - Veja o "Serviço de acessibilidade de preenchimento automático" nas definições do Bitwarden</value>
|
||||
@@ -1689,7 +1689,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>Button text for an open operation (verb).</comment>
|
||||
</data>
|
||||
<data name="UnableToSaveAttachment" xml:space="preserve">
|
||||
<value>Houve um problema ao guardar este anexo. Se o problema persistir, poderá guardá-lo através do cofre Web.</value>
|
||||
<value>Houve um problema ao guardar este anexo. Se o problema persistir, poderá guardá-lo através do cofre web.</value>
|
||||
</data>
|
||||
<data name="SaveAttachmentSuccess" xml:space="preserve">
|
||||
<value>Anexo guardado com sucesso</value>
|
||||
@@ -1701,7 +1701,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Nenhum campo de palavra-passe detetado</value>
|
||||
</data>
|
||||
<data name="SoftDeleting" xml:space="preserve">
|
||||
<value>A mover para o lixo...</value>
|
||||
<value>A enviar para o lixo...</value>
|
||||
<comment>Message shown when interacting with the server</comment>
|
||||
</data>
|
||||
<data name="ItemSoftDeleted" xml:space="preserve">
|
||||
@@ -1729,7 +1729,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>(action prompt) Label for the search text field when viewing the trash folder</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToPermanentlyDeleteCipher" xml:space="preserve">
|
||||
<value>Pretende mesmo eliminar permanentemente? Esta ação não pode ser anulada.</value>
|
||||
<value>Pretende mesmo eliminar permanentemente? Isto não pode ser anulado.</value>
|
||||
<comment>Confirmation alert message when permanently deleteing a cipher.</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToRestoreCipher" xml:space="preserve">
|
||||
@@ -1737,7 +1737,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>Confirmation alert message when restoring a soft-deleted cipher.</comment>
|
||||
</data>
|
||||
<data name="DoYouReallyWantToSoftDeleteCipher" xml:space="preserve">
|
||||
<value>Pretende mesmo mover para o lixo?</value>
|
||||
<value>Pretende mesmo enviar para o lixo?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="AccountBiometricInvalidated" xml:space="preserve">
|
||||
@@ -1762,10 +1762,10 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Identificador da organização</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Atualmente não é possível iniciar sessão com SSO</value>
|
||||
<value>Não foi possível iniciar sessão com SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Definir a palavra-passe mestra</value>
|
||||
<value>Definir palavra-passe mestra</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>Para concluir o início de sessão com SSO, por favor, defina uma palavra-passe mestra para aceder e proteger o seu cofre.</value>
|
||||
@@ -1880,7 +1880,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Texto</value>
|
||||
</data>
|
||||
<data name="TypeTextInfo" xml:space="preserve">
|
||||
<value>O texto que pretende enviar.</value>
|
||||
<value>O texto que deseja enviar.</value>
|
||||
</data>
|
||||
<data name="HideTextByDefault" xml:space="preserve">
|
||||
<value>Ao aceder ao Send, ocultar o texto por defeito</value>
|
||||
@@ -1890,7 +1890,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Ficheiro</value>
|
||||
</data>
|
||||
<data name="TypeFileInfo" xml:space="preserve">
|
||||
<value>O ficheiro que pretende enviar.</value>
|
||||
<value>O ficheiro que deseja enviar.</value>
|
||||
</data>
|
||||
<data name="FileTypeIsSelected" xml:space="preserve">
|
||||
<value>O tipo de ficheiro está selecionado.</value>
|
||||
@@ -1908,7 +1908,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Data de eliminação</value>
|
||||
</data>
|
||||
<data name="DeletionTime" xml:space="preserve">
|
||||
<value>Hora de eliminação</value>
|
||||
<value>Hora da eliminação</value>
|
||||
</data>
|
||||
<data name="DeletionDateInfo" xml:space="preserve">
|
||||
<value>O Send será permanentemente eliminado na data e hora especificadas.</value>
|
||||
@@ -1918,7 +1918,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Eliminação pendente</value>
|
||||
</data>
|
||||
<data name="ExpirationDate" xml:space="preserve">
|
||||
<value>Prazo de validade</value>
|
||||
<value>Data de validade</value>
|
||||
</data>
|
||||
<data name="ExpirationTime" xml:space="preserve">
|
||||
<value>Hora de validade</value>
|
||||
@@ -1954,7 +1954,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Remover palavra-passe</value>
|
||||
</data>
|
||||
<data name="AreYouSureRemoveSendPassword" xml:space="preserve">
|
||||
<value>Tem a certeza de que pretende remover a palavra-passe?</value>
|
||||
<value>Tem a certeza de que pretende remover a palavras-passe?</value>
|
||||
</data>
|
||||
<data name="RemovingSendPassword" xml:space="preserve">
|
||||
<value>A remover palavra-passe</value>
|
||||
@@ -2009,7 +2009,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SendUpdated" xml:space="preserve">
|
||||
<value>Send guardado</value>
|
||||
<value>Send atualizado</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="NewSendCreated" xml:space="preserve">
|
||||
@@ -2058,7 +2058,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="SendFileEmailVerificationRequired" xml:space="preserve">
|
||||
<value>Tem de verificar o seu e-mail para utilizar ficheiros com o Send. Pode verificar o seu e-mail no cofre Web.</value>
|
||||
<value>Tem de verificar o seu e-mail para utilizar ficheiros com o Send. Pode verificar o seu e-mail no cofre web.</value>
|
||||
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
|
||||
</data>
|
||||
<data name="PasswordPrompt" xml:space="preserve">
|
||||
@@ -2080,7 +2080,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Palavra-passe mestra atualizada</value>
|
||||
</data>
|
||||
<data name="UpdateMasterPassword" xml:space="preserve">
|
||||
<value>Atualizar a palavra-passe mestra</value>
|
||||
<value>Atualizar palavra-passe mestra</value>
|
||||
</data>
|
||||
<data name="UpdateMasterPasswordWarning" xml:space="preserve">
|
||||
<value>A sua palavra-passe mestra foi recentemente alterada por um administrador da sua organização. Para aceder ao cofre, tem de atualizar a sua palavra-passe mestra agora. Ao prosseguir, terminará a sua sessão atual e terá de iniciar sessão novamente. As sessões ativas noutros dispositivos poderão continuar ativas até uma hora.</value>
|
||||
@@ -2221,10 +2221,10 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Ajude o Bitwarden a melhorar a estabilidade da aplicação ao enviar relatórios de falhas.</value>
|
||||
</data>
|
||||
<data name="OptionsExpanded" xml:space="preserve">
|
||||
<value>As opções estão expandidas, clique para as fechar.</value>
|
||||
<value>As opções foram expandidas, clique para fechar.</value>
|
||||
</data>
|
||||
<data name="OptionsCollapsed" xml:space="preserve">
|
||||
<value>As opções estão fechadas, clique para as expandir.</value>
|
||||
<value>As opções foram fechadas, clique para expandir.</value>
|
||||
</data>
|
||||
<data name="UppercaseAtoZ" xml:space="preserve">
|
||||
<value>Maiúsculas (A-Z)</value>
|
||||
@@ -2311,7 +2311,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Tem a certeza de que pretende ativar a captura de ecrã?</value>
|
||||
</data>
|
||||
<data name="LogInRequested" xml:space="preserve">
|
||||
<value>Pedido de início de sessão</value>
|
||||
<value>Início de sessão pedido</value>
|
||||
</data>
|
||||
<data name="AreYouTryingToLogIn" xml:space="preserve">
|
||||
<value>Está a tentar iniciar sessão?</value>
|
||||
@@ -2326,7 +2326,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Endereço IP</value>
|
||||
</data>
|
||||
<data name="Time" xml:space="preserve">
|
||||
<value>Hora</value>
|
||||
<value>Tempo</value>
|
||||
</data>
|
||||
<data name="Near" xml:space="preserve">
|
||||
<value>Perto</value>
|
||||
@@ -2458,7 +2458,7 @@ A leitura será efetuada automaticamente.</value>
|
||||
<value>Aleatório</value>
|
||||
</data>
|
||||
<data name="ConnectToWatch" xml:space="preserve">
|
||||
<value>Ligar ao Relógio</value>
|
||||
<value>Ligar ao relógio</value>
|
||||
</data>
|
||||
<data name="AccessibilityServiceDisclosure" xml:space="preserve">
|
||||
<value>Divulgação do serviço de acessibilidade</value>
|
||||
@@ -2484,7 +2484,7 @@ Deseja mudar para esta conta?</value>
|
||||
<value>É novo por cá?</value>
|
||||
</data>
|
||||
<data name="GetMasterPasswordwordHint" xml:space="preserve">
|
||||
<value>Obter a dica da palavra-passe mestra</value>
|
||||
<value>Obter dica da palavra-passe mestra</value>
|
||||
</data>
|
||||
<data name="LoggingInAsXOnY" xml:space="preserve">
|
||||
<value>A iniciar sessão como {0} em {1}</value>
|
||||
@@ -2758,10 +2758,10 @@ Deseja mudar para esta conta?</value>
|
||||
<value>Segurança da conta</value>
|
||||
</data>
|
||||
<data name="BitwardenHelpCenter" xml:space="preserve">
|
||||
<value>Centro de ajuda do Bitwarden</value>
|
||||
<value>Centro de ajuda Bitwarden</value>
|
||||
</data>
|
||||
<data name="ContactBitwardenSupport" xml:space="preserve">
|
||||
<value>Contactar o suporte do Bitwarden</value>
|
||||
<value>Contactar o suporte Bitwarden</value>
|
||||
</data>
|
||||
<data name="CopyAppInformation" xml:space="preserve">
|
||||
<value>Copiar informações da aplicação</value>
|
||||
@@ -2807,7 +2807,7 @@ Deseja mudar para esta conta?</value>
|
||||
<value>Opções adicionais</value>
|
||||
</data>
|
||||
<data name="ContinueToWebApp" xml:space="preserve">
|
||||
<value>Continuar para a aplicação Web?</value>
|
||||
<value>Continuar para a aplicação web?</value>
|
||||
</data>
|
||||
<data name="ContinueToX" xml:space="preserve">
|
||||
<value>Continuar para {0}?</value>
|
||||
@@ -2826,10 +2826,10 @@ Deseja mudar para esta conta?</value>
|
||||
<value>Continuar para a loja de aplicações?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Torne a sua conta mais segura configurando a verificação em dois passos na aplicação Web Bitwarden.</value>
|
||||
<value>Torne a sua conta mais segura configurando o login em duas etapas na aplicação web Bitwarden.</value>
|
||||
</data>
|
||||
<data name="ChangeMasterPasswordDescriptionLong" xml:space="preserve">
|
||||
<value>Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden.</value>
|
||||
<value>Pode alterar a sua palavra-passe mestra na aplicação web Bitwarden.</value>
|
||||
</data>
|
||||
<data name="YouCanImportDataToYourVaultOnX" xml:space="preserve">
|
||||
<value>Pode importar dados para o seu cofre em {0}.</value>
|
||||
@@ -2845,13 +2845,13 @@ Deseja mudar para esta conta?</value>
|
||||
<value>Consulte a nossa política de privacidade em bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Explore mais funcionalidades da sua conta Bitwarden na aplicação Web.</value>
|
||||
<value>Explore mais funcionalidades da sua conta Bitwarden na aplicação web.</value>
|
||||
</data>
|
||||
<data name="LearnAboutOrganizationsDescriptionLong" xml:space="preserve">
|
||||
<value>O Bitwarden permite-lhe partilhar os seus itens do cofre com outras pessoas através da utilização de uma organização. Saiba mais no site bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="RateAppDescriptionLong" xml:space="preserve">
|
||||
<value>Ajude outras pessoas a descobrir se o Bitwarden lhes é adequado. Visite a loja de aplicações e deixe uma avaliação agora.</value>
|
||||
<value>Ajude outras pessoas a descobrir se o Bitwarden é adequado para elas. Visite a loja de aplicações e deixe uma avaliação agora.</value>
|
||||
</data>
|
||||
<data name="DefaultDarkThemeDescriptionLong" xml:space="preserve">
|
||||
<value>Escolha o tema escuro a utilizar quando o modo escuro do seu dispositivo estiver a ser utilizado</value>
|
||||
|
||||
@@ -2877,13 +2877,4 @@ Do you want to switch to this account?</value>
|
||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||
</data>
|
||||
<data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve">
|
||||
<value>Duo two-step login is required for your account. </value>
|
||||
</data>
|
||||
<data name="FollowTheStepsFromDuoToFinishLoggingIn" xml:space="preserve">
|
||||
<value>Follow the steps from Duo to finish logging in.</value>
|
||||
</data>
|
||||
<data name="LaunchDuo" xml:space="preserve">
|
||||
<value>Launch Duo</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -778,7 +778,7 @@
|
||||
<value>Det lättaste sättet att lägga till nya inloggningar till ditt valv är från Bitwardens hjälpmedelsservice för automatisk ifyllnad. Läs mer om hur man använder Bitwardens hjälpmedelsservice för automatisk ifyllnad genom att navigera till fliken "Inställningar".</value>
|
||||
</data>
|
||||
<data name="Autofill" xml:space="preserve">
|
||||
<value>Autofyll</value>
|
||||
<value>Automatisk ifyllnad</value>
|
||||
</data>
|
||||
<data name="AutofillOrView" xml:space="preserve">
|
||||
<value>Vill du automatiskt fylla i den här inloggningen eller visa den?</value>
|
||||
@@ -2377,7 +2377,7 @@ välj Lägg till TOTP för att lagra nyckeln på ett säkert sätt</value>
|
||||
<value>Lösenordstyp</value>
|
||||
</data>
|
||||
<data name="WhatWouldYouLikeToGenerate" xml:space="preserve">
|
||||
<value>Vad skulle du vilja generera?</value>
|
||||
<value>Vad vill du skapa?</value>
|
||||
</data>
|
||||
<data name="UsernameType" xml:space="preserve">
|
||||
<value>Typ av användarnamn</value>
|
||||
|
||||
@@ -2353,7 +2353,7 @@ Kod otomatik olarak taranacaktır.</value>
|
||||
<value>Giriş isteklerini onayla</value>
|
||||
</data>
|
||||
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||
<value>Diğer cihazlardan yapılan giriş isteklerini bu cihazdan onayla</value>
|
||||
<value>Diğer cihazlardan yapılan giriş isteklerini onaylamak için bu cihazı kullan</value>
|
||||
</data>
|
||||
<data name="AllowNotifications" xml:space="preserve">
|
||||
<value>Bildirimlere izin ver</value>
|
||||
@@ -2820,7 +2820,7 @@ Bu hesaba geçmek ister misiniz?</value>
|
||||
<value>Destek ekibiyle iletişim kurmak ister misiniz?</value>
|
||||
</data>
|
||||
<data name="ContinueToPrivacyPolicy" xml:space="preserve">
|
||||
<value>Gizlilik ilkelerine gidilsin mi?</value>
|
||||
<value>Continue to privacy policy?</value>
|
||||
</data>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>App Store'a gitmek ister misiniz?</value>
|
||||
@@ -2842,7 +2842,7 @@ Bu hesaba geçmek ister misiniz?</value>
|
||||
<value>Aradığınızı bulamadınız mı? bitwarden.com sitesinden Bitwarden destek ekibine ulaşabilirsiniz.</value>
|
||||
</data>
|
||||
<data name="PrivacyPolicyDescriptionLong" xml:space="preserve">
|
||||
<value>Gizlilik ilkelerimizi bitwarden.com'da bulabilirsiniz.</value>
|
||||
<value>Check out our privacy policy on bitwarden.com.</value>
|
||||
</data>
|
||||
<data name="ExploreMoreFeaturesOfYourBitwardenAccountOnTheWebApp" xml:space="preserve">
|
||||
<value>Web uygulamasında Bitwarden hesabınızın diğer özelliklerini keşfedin.</value>
|
||||
|
||||
@@ -2447,7 +2447,7 @@
|
||||
<value>Сталася невідома помилка {0}.</value>
|
||||
</data>
|
||||
<data name="PlusAddressedEmailDescription" xml:space="preserve">
|
||||
<value>Використовуйте розширені можливості адрес вашого провайдера електронної пошти</value>
|
||||
<value>Використовуйте розширені можливості адрес вашого постачальника електронної пошти</value>
|
||||
</data>
|
||||
<data name="CatchAllEmailDescription" xml:space="preserve">
|
||||
<value>Використовуйте свою скриньку вхідних Catch-All власного домену.</value>
|
||||
|
||||
@@ -11,15 +11,11 @@
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="Margin"
|
||||
Value="-4, 0, -4, -4" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
</Style>
|
||||
<Style TargetType="Picker"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="TextColor"
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="Margin"
|
||||
Value="-4, 0, -4, -4" />
|
||||
</Style>
|
||||
@@ -43,8 +39,6 @@
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
Value="{DynamicResource InputPlaceholderColor}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="Margin"
|
||||
Value="-4, 0, -4, -4" />
|
||||
</Style>
|
||||
@@ -52,8 +46,6 @@
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="Transparent" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{DynamicResource TitleEntryTextColor}" />
|
||||
<Setter Property="CancelButtonColor"
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
Value="{DynamicResource InputPlaceholderColor}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 12" />
|
||||
</Style>
|
||||
@@ -18,8 +16,6 @@
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="TextColor"
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 12" />
|
||||
</Style>
|
||||
@@ -41,8 +37,6 @@
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="TextColor"
|
||||
Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource BackgroundColor}" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
@@ -57,8 +51,6 @@
|
||||
</Style>
|
||||
<Style TargetType="SearchBar"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="FontSize"
|
||||
Value="18" />
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ListHeaderBackgroundColor}" />
|
||||
<Setter Property="TextColor"
|
||||
@@ -143,10 +135,6 @@
|
||||
Value="Medium" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 0" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="MinimumHeightRequest"
|
||||
Value="45" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
@@ -187,6 +175,10 @@
|
||||
Value="Bold" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 0" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="MinimumHeightRequest"
|
||||
Value="45" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
@@ -223,6 +215,8 @@
|
||||
Value="{DynamicResource ButtonTextColorOpacity}" />
|
||||
<Setter Property="FontSize"
|
||||
Value="Medium" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
|
||||
@@ -32,4 +32,14 @@
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public class MainAppWindow : ResumeWindow
|
||||
{
|
||||
public MainAppWindow(Page page) : base(page) { }
|
||||
}
|
||||
|
||||
public class AutoFillWindow : ResumeWindow
|
||||
{
|
||||
public AutoFillWindow(Page page) : base(page){ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
|
||||
{
|
||||
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(),
|
||||
[AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>()
|
||||
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>()
|
||||
};
|
||||
|
||||
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
#if ANDROID
|
||||
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Security;
|
||||
using Android.Security.Keystore;
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Javax.Crypto.Spec;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
class AndroidKeyStore
|
||||
{
|
||||
const string androidKeyStore = "AndroidKeyStore"; // this is an Android const value
|
||||
const string aesAlgorithm = "AES";
|
||||
const string cipherTransformationAsymmetric = "RSA/ECB/PKCS1Padding";
|
||||
const string cipherTransformationSymmetric = "AES/GCM/NoPadding";
|
||||
const string prefsMasterKey = "SecureStorageKey";
|
||||
const int initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM
|
||||
|
||||
internal AndroidKeyStore(Context context, string keystoreAlias, bool alwaysUseAsymmetricKeyStorage)
|
||||
{
|
||||
alwaysUseAsymmetricKey = alwaysUseAsymmetricKeyStorage;
|
||||
appContext = context;
|
||||
alias = keystoreAlias;
|
||||
|
||||
keyStore = KeyStore.GetInstance(androidKeyStore);
|
||||
keyStore.Load(null);
|
||||
}
|
||||
|
||||
readonly Context appContext;
|
||||
readonly string alias;
|
||||
readonly bool alwaysUseAsymmetricKey;
|
||||
readonly string useSymmetricPreferenceKey = "essentials_use_symmetric";
|
||||
|
||||
KeyStore keyStore;
|
||||
bool useSymmetric = false;
|
||||
|
||||
ISecretKey GetKey()
|
||||
{
|
||||
// check to see if we need to get our key from past-versions or newer versions.
|
||||
// we want to use symmetric if we are >= 23 or we didn't set it previously.
|
||||
var hasApiLevel = Build.VERSION.SdkInt >= BuildVersionCodes.M;
|
||||
|
||||
useSymmetric = Preferences.Get(useSymmetricPreferenceKey, hasApiLevel, alias);
|
||||
|
||||
// If >= API 23 we can use the KeyStore's symmetric key
|
||||
if (useSymmetric && !alwaysUseAsymmetricKey)
|
||||
return GetSymmetricKey();
|
||||
|
||||
// NOTE: KeyStore in < API 23 can only store asymmetric keys
|
||||
// specifically, only RSA/ECB/PKCS1Padding
|
||||
// So we will wrap our symmetric AES key we just generated
|
||||
// with this and save the encrypted/wrapped key out to
|
||||
// preferences for future use.
|
||||
// ECB should be fine in this case as the AES key should be
|
||||
// contained in one block.
|
||||
|
||||
// Get the asymmetric key pair
|
||||
var keyPair = GetAsymmetricKeyPair();
|
||||
|
||||
var existingKeyStr = Preferences.Get(prefsMasterKey, null, alias);
|
||||
|
||||
if (!string.IsNullOrEmpty(existingKeyStr))
|
||||
{
|
||||
try
|
||||
{
|
||||
var wrappedKey = Convert.FromBase64String(existingKeyStr);
|
||||
|
||||
var unwrappedKey = UnwrapKey(wrappedKey, keyPair.Private);
|
||||
var kp = unwrappedKey.JavaCast<ISecretKey>();
|
||||
|
||||
return kp;
|
||||
}
|
||||
catch (InvalidKeyException ikEx)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Unable to unwrap key: Invalid Key. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ikEx.Message}");
|
||||
}
|
||||
catch (IllegalBlockSizeException ibsEx)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Unable to unwrap key: Illegal Block Size. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ibsEx.Message}");
|
||||
}
|
||||
catch (BadPaddingException paddingEx)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Unable to unwrap key: Bad Padding. This may be caused by system backup or upgrades. All secure storage items will now be removed. {paddingEx.Message}");
|
||||
}
|
||||
|
||||
LegacySecureStorage.RemoveAll();
|
||||
}
|
||||
|
||||
var keyGenerator = KeyGenerator.GetInstance(aesAlgorithm);
|
||||
var defSymmetricKey = keyGenerator.GenerateKey();
|
||||
|
||||
var newWrappedKey = WrapKey(defSymmetricKey, keyPair.Public);
|
||||
|
||||
Preferences.Set(prefsMasterKey, Convert.ToBase64String(newWrappedKey), alias);
|
||||
|
||||
return defSymmetricKey;
|
||||
}
|
||||
|
||||
// API 23+ Only
|
||||
#pragma warning disable CA1416
|
||||
ISecretKey GetSymmetricKey()
|
||||
{
|
||||
Preferences.Set(useSymmetricPreferenceKey, true, alias);
|
||||
|
||||
var existingKey = keyStore.GetKey(alias, null);
|
||||
|
||||
if (existingKey != null)
|
||||
{
|
||||
var existingSecretKey = existingKey.JavaCast<ISecretKey>();
|
||||
return existingSecretKey;
|
||||
}
|
||||
|
||||
var keyGenerator = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, androidKeyStore);
|
||||
var builder = new KeyGenParameterSpec.Builder(alias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)
|
||||
.SetRandomizedEncryptionRequired(false);
|
||||
|
||||
keyGenerator.Init(builder.Build());
|
||||
|
||||
return keyGenerator.GenerateKey();
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
|
||||
KeyPair GetAsymmetricKeyPair()
|
||||
{
|
||||
// set that we generated keys on pre-m device.
|
||||
Preferences.Set(useSymmetricPreferenceKey, false, alias);
|
||||
|
||||
var asymmetricAlias = $"{alias}.asymmetric";
|
||||
|
||||
var privateKey = keyStore.GetKey(asymmetricAlias, null)?.JavaCast<IPrivateKey>();
|
||||
var publicKey = keyStore.GetCertificate(asymmetricAlias)?.PublicKey;
|
||||
|
||||
// Return the existing key if found
|
||||
if (privateKey != null && publicKey != null)
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
|
||||
var originalLocale = Java.Util.Locale.Default;
|
||||
try
|
||||
{
|
||||
// Force to english for known bug in date parsing:
|
||||
// https://issuetracker.google.com/issues/37095309
|
||||
SetLocale(Java.Util.Locale.English);
|
||||
|
||||
// Otherwise we create a new key
|
||||
#pragma warning disable CA1416
|
||||
var generator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, androidKeyStore);
|
||||
#pragma warning restore CA1416
|
||||
|
||||
var end = DateTime.UtcNow.AddYears(20);
|
||||
var startDate = new Java.Util.Date();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var endDate = new Java.Util.Date(end.Year, end.Month, end.Day);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
#pragma warning disable CS0618
|
||||
var builder = new KeyPairGeneratorSpec.Builder(Platform.AppContext)
|
||||
.SetAlias(asymmetricAlias)
|
||||
.SetSerialNumber(Java.Math.BigInteger.One)
|
||||
.SetSubject(new Javax.Security.Auth.X500.X500Principal($"CN={asymmetricAlias} CA Certificate"))
|
||||
.SetStartDate(startDate)
|
||||
.SetEndDate(endDate);
|
||||
|
||||
generator.Initialize(builder.Build());
|
||||
#pragma warning restore CS0618
|
||||
|
||||
return generator.GenerateKeyPair();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetLocale(originalLocale);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] WrapKey(IKey keyToWrap, IKey withKey)
|
||||
{
|
||||
var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
|
||||
cipher.Init(CipherMode.WrapMode, withKey);
|
||||
return cipher.Wrap(keyToWrap);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416
|
||||
IKey UnwrapKey(byte[] wrappedData, IKey withKey)
|
||||
{
|
||||
var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
|
||||
cipher.Init(CipherMode.UnwrapMode, withKey);
|
||||
var unwrapped = cipher.Unwrap(wrappedData, KeyProperties.KeyAlgorithmAes, KeyType.SecretKey);
|
||||
return unwrapped;
|
||||
}
|
||||
#pragma warning restore CA1416
|
||||
|
||||
internal byte[] Encrypt(string data)
|
||||
{
|
||||
var key = GetKey();
|
||||
|
||||
// Generate initialization vector
|
||||
var iv = new byte[initializationVectorLen];
|
||||
|
||||
var sr = new SecureRandom();
|
||||
sr.NextBytes(iv);
|
||||
|
||||
Cipher cipher;
|
||||
|
||||
// Attempt to use GCMParameterSpec by default
|
||||
try
|
||||
{
|
||||
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||
cipher.Init(CipherMode.EncryptMode, key, new GCMParameterSpec(128, iv));
|
||||
}
|
||||
catch (InvalidAlgorithmParameterException)
|
||||
{
|
||||
// If we encounter this error, it's likely an old bouncycastle provider version
|
||||
// is being used which does not recognize GCMParameterSpec, but should work
|
||||
// with IvParameterSpec, however we only do this as a last effort since other
|
||||
// implementations will error if you use IvParameterSpec when GCMParameterSpec
|
||||
// is recognized and expected.
|
||||
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||
cipher.Init(CipherMode.EncryptMode, key, new IvParameterSpec(iv));
|
||||
}
|
||||
|
||||
var decryptedData = Encoding.UTF8.GetBytes(data);
|
||||
var encryptedBytes = cipher.DoFinal(decryptedData);
|
||||
|
||||
// Combine the IV and the encrypted data into one array
|
||||
var r = new byte[iv.Length + encryptedBytes.Length];
|
||||
Buffer.BlockCopy(iv, 0, r, 0, iv.Length);
|
||||
Buffer.BlockCopy(encryptedBytes, 0, r, iv.Length, encryptedBytes.Length);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
internal string Decrypt(byte[] data)
|
||||
{
|
||||
if (data.Length < initializationVectorLen)
|
||||
return null;
|
||||
|
||||
var key = GetKey();
|
||||
|
||||
// IV will be the first 16 bytes of the encrypted data
|
||||
var iv = new byte[initializationVectorLen];
|
||||
Buffer.BlockCopy(data, 0, iv, 0, initializationVectorLen);
|
||||
|
||||
Cipher cipher;
|
||||
|
||||
// Attempt to use GCMParameterSpec by default
|
||||
try
|
||||
{
|
||||
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||
cipher.Init(CipherMode.DecryptMode, key, new GCMParameterSpec(128, iv));
|
||||
}
|
||||
catch (InvalidAlgorithmParameterException)
|
||||
{
|
||||
// If we encounter this error, it's likely an old bouncycastle provider version
|
||||
// is being used which does not recognize GCMParameterSpec, but should work
|
||||
// with IvParameterSpec, however we only do this as a last effort since other
|
||||
// implementations will error if you use IvParameterSpec when GCMParameterSpec
|
||||
// is recognized and expected.
|
||||
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||
cipher.Init(CipherMode.DecryptMode, key, new IvParameterSpec(iv));
|
||||
}
|
||||
|
||||
// Decrypt starting after the first 16 bytes from the IV
|
||||
var decryptedData = cipher.DoFinal(data, initializationVectorLen, data.Length - initializationVectorLen);
|
||||
|
||||
return Encoding.UTF8.GetString(decryptedData);
|
||||
}
|
||||
|
||||
internal void SetLocale(Java.Util.Locale locale)
|
||||
{
|
||||
Java.Util.Locale.Default = locale;
|
||||
var resources = appContext.Resources;
|
||||
var config = resources.Configuration;
|
||||
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
|
||||
config.SetLocale(locale);
|
||||
else
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
config.Locale = locale;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
resources.UpdateConfiguration(config, resources.DisplayMetrics);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,128 +0,0 @@
|
||||
#if IOS
|
||||
|
||||
using System.Diagnostics;
|
||||
using Foundation;
|
||||
using Security;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
internal class KeyChain
|
||||
{
|
||||
SecAccessible accessible;
|
||||
|
||||
internal KeyChain(SecAccessible accessible) =>
|
||||
this.accessible = accessible;
|
||||
|
||||
SecRecord ExistingRecordForKey(string key, string service)
|
||||
{
|
||||
return new SecRecord(SecKind.GenericPassword)
|
||||
{
|
||||
Account = key,
|
||||
Service = service
|
||||
};
|
||||
}
|
||||
|
||||
internal string ValueForKey(string key, string service)
|
||||
{
|
||||
using (var record = ExistingRecordForKey(key, service))
|
||||
using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
|
||||
{
|
||||
if (resultCode == SecStatusCode.Success)
|
||||
return NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetValueForKey(string value, string key, string service)
|
||||
{
|
||||
using (var record = ExistingRecordForKey(key, service))
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ValueForKey(key, service)))
|
||||
RemoveRecord(record);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if the key already exists, remove it
|
||||
if (!string.IsNullOrEmpty(ValueForKey(key, service)))
|
||||
RemoveRecord(record);
|
||||
}
|
||||
|
||||
using (var newRecord = CreateRecordForNewKeyValue(key, value, service))
|
||||
{
|
||||
var result = SecKeyChain.Add(newRecord);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case SecStatusCode.DuplicateItem:
|
||||
{
|
||||
Debug.WriteLine("Duplicate item found. Attempting to remove and add again.");
|
||||
|
||||
// try to remove and add again
|
||||
if (Remove(key, service))
|
||||
{
|
||||
result = SecKeyChain.Add(newRecord);
|
||||
if (result != SecStatusCode.Success)
|
||||
throw new Exception($"Error adding record: {result}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("Unable to remove key.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SecStatusCode.Success:
|
||||
return;
|
||||
default:
|
||||
throw new Exception($"Error adding record: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool Remove(string key, string service)
|
||||
{
|
||||
using (var record = ExistingRecordForKey(key, service))
|
||||
using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
|
||||
{
|
||||
if (resultCode == SecStatusCode.Success)
|
||||
{
|
||||
RemoveRecord(record);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void RemoveAll(string service)
|
||||
{
|
||||
using (var query = new SecRecord(SecKind.GenericPassword) { Service = service })
|
||||
{
|
||||
SecKeyChain.Remove(query);
|
||||
}
|
||||
}
|
||||
|
||||
SecRecord CreateRecordForNewKeyValue(string key, string value, string service)
|
||||
{
|
||||
return new SecRecord(SecKind.GenericPassword)
|
||||
{
|
||||
Account = key,
|
||||
Service = service,
|
||||
Label = key,
|
||||
Accessible = accessible,
|
||||
ValueData = NSData.FromString(value, NSStringEncoding.UTF8),
|
||||
};
|
||||
}
|
||||
|
||||
bool RemoveRecord(SecRecord record)
|
||||
{
|
||||
var result = SecKeyChain.Remove(record);
|
||||
if (result != SecStatusCode.Success && result != SecStatusCode.ItemNotFound)
|
||||
throw new Exception($"Error removing record: {result}");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,108 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
#if IOS
|
||||
using Security;
|
||||
#endif
|
||||
|
||||
#if ANDROID
|
||||
using Javax.Crypto;
|
||||
#endif
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class LegacySecureStorage
|
||||
{
|
||||
internal static readonly string Alias = $"{AppInfo.PackageName}.xamarinessentials";
|
||||
|
||||
#if IOS
|
||||
private static SecAccessible DefaultAccessible { get; set; } = SecAccessible.AfterFirstUnlock;
|
||||
#endif
|
||||
|
||||
|
||||
public static Task<string?> GetAsync(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
|
||||
#if ANDROID
|
||||
return Task.Run(() =>
|
||||
{
|
||||
object locker = new object();
|
||||
string? encVal = Preferences.Get(key, null, Alias);
|
||||
|
||||
if (!string.IsNullOrEmpty(encVal))
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] encData = Convert.FromBase64String(encVal);
|
||||
lock (locker)
|
||||
{
|
||||
AndroidKeyStore keyStore = new AndroidKeyStore(Platform.AppContext, Alias, false);
|
||||
return keyStore.Decrypt(encData);
|
||||
}
|
||||
}
|
||||
catch (AEADBadTagException)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Unable to decrypt key, {key}, which is likely due to an app uninstall. Removing old key and returning null.");
|
||||
Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
#elif IOS
|
||||
var keyChain = new KeyChain(DefaultAccessible);
|
||||
return Task.FromResult<string?>(keyChain.ValueForKey(key, Alias));
|
||||
#else
|
||||
return Task.FromResult((string?)null);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static Task SetAsync(string key, string value)
|
||||
{
|
||||
#if ANDROID
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var context = Platform.AppContext;
|
||||
|
||||
byte[] encryptedData = null;
|
||||
object locker = new object();
|
||||
lock (locker)
|
||||
{
|
||||
AndroidKeyStore keyStore = new AndroidKeyStore(Platform.AppContext, Alias, false);
|
||||
encryptedData = keyStore.Encrypt(value);
|
||||
}
|
||||
|
||||
var encStr = Convert.ToBase64String(encryptedData);
|
||||
Preferences.Set(key, encStr, Alias);
|
||||
});
|
||||
#elif IOS
|
||||
KeyChain keyChain = new KeyChain(DefaultAccessible);
|
||||
keyChain.SetValueForKey(value, key, Alias);
|
||||
#endif
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static bool Remove(string key)
|
||||
{
|
||||
#if ANDROID
|
||||
Preferences.Remove(key, Alias);
|
||||
return true;
|
||||
#elif IOS
|
||||
var keyChain = new KeyChain(DefaultAccessible);
|
||||
return keyChain.Remove(key, Alias);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void RemoveAll()
|
||||
{
|
||||
#if ANDROID
|
||||
Preferences.Clear(Alias);
|
||||
#elif IOS
|
||||
var keyChain = new KeyChain(DefaultAccessible);
|
||||
keyChain.RemoveAll(Alias);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
#if !FDROID
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
@@ -51,5 +55,3 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Bit.App.Services
|
||||
public async Task<T> GetAsync<T>(string key)
|
||||
{
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
var val = await LegacySecureStorage.GetAsync(formattedKey);
|
||||
var val = await Microsoft.Maui.Storage.SecureStorage.GetAsync(formattedKey);
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)(object)val;
|
||||
@@ -37,11 +37,11 @@ namespace Bit.App.Services
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
await LegacySecureStorage.SetAsync(formattedKey, obj as string);
|
||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey, obj as string);
|
||||
}
|
||||
else
|
||||
{
|
||||
await LegacySecureStorage.SetAsync(formattedKey,
|
||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey,
|
||||
JsonConvert.SerializeObject(obj, _jsonSettings));
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace Bit.App.Services
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
LegacySecureStorage.Remove(formattedKey);
|
||||
Microsoft.Maui.Storage.SecureStorage.Remove(formattedKey);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,19 +849,17 @@ namespace Bit.Core.Services
|
||||
{
|
||||
// account data
|
||||
var state = await GetValueAsync<State>(Storage.Prefs, V7Keys.StateKey);
|
||||
if (state != null)
|
||||
{
|
||||
// Migrate environment data to use Regions
|
||||
foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null))
|
||||
{
|
||||
var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls();
|
||||
account.Value.Settings.Region = urls.Region;
|
||||
account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls;
|
||||
}
|
||||
|
||||
await SetValueAsync(Storage.Prefs, Constants.StateKey, state);
|
||||
// Migrate environment data to use Regions
|
||||
foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null))
|
||||
{
|
||||
var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls();
|
||||
account.Value.Settings.Region = urls.Region;
|
||||
account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls;
|
||||
}
|
||||
|
||||
await SetValueAsync(Storage.Prefs, Constants.StateKey, state);
|
||||
|
||||
// Update pre auth urls and region
|
||||
var preAuthUrls = await GetValueAsync<EnvironmentUrlData>(Storage.Prefs, V7Keys.PreAuthEnvironmentUrlsKey) ?? Region.US.GetUrls();
|
||||
await SetValueAsync(Storage.Prefs, V7Keys.RegionEnvironmentKey, preAuthUrls.Region);
|
||||
|
||||
@@ -60,9 +60,7 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
|
||||
{
|
||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
||||
#if ANDROID
|
||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.AndroidWindowCreated);
|
||||
#endif
|
||||
|
||||
appOptionsAction(Options);
|
||||
|
||||
await NavigateOnAccountChangeAsync();
|
||||
@@ -71,9 +69,6 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
|
||||
{
|
||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
||||
#if ANDROID
|
||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.AndroidWindowCreated);
|
||||
#endif
|
||||
|
||||
// TODO: this could be improved by doing chain of responsability pattern
|
||||
// but for now it may be an overkill, if logic gets more complex consider refactoring it
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
@@ -158,9 +158,7 @@ namespace Bit.App.Utilities
|
||||
{
|
||||
#if UT
|
||||
return false;
|
||||
#else
|
||||
|
||||
#if ANDROID
|
||||
#elif ANDROID
|
||||
return Application.Current.RequestedTheme == AppTheme.Dark;
|
||||
#else
|
||||
var requestedTheme = AppTheme.Unspecified;
|
||||
@@ -177,8 +175,6 @@ namespace Bit.App.Utilities
|
||||
_ => AppTheme.Unspecified
|
||||
};
|
||||
return requestedTheme == AppTheme.Dark;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
// This is a copy from MAUI Essentials WebAuthenticator with a fix for getting UIWindow without Scenes.
|
||||
|
||||
#if IOS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using Foundation;
|
||||
using SafariServices;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
using WebKit;
|
||||
using Microsoft.Maui.Authentication;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Utilities.MAUI
|
||||
{
|
||||
partial class WebAuthenticatorImplementation : IWebAuthenticator, IPlatformWebAuthenticatorCallback
|
||||
{
|
||||
#if IOS
|
||||
const int asWebAuthenticationSessionErrorCodeCanceledLogin = 1;
|
||||
const string asWebAuthenticationSessionErrorDomain = "com.apple.AuthenticationServices.WebAuthenticationSession";
|
||||
|
||||
const int sfAuthenticationErrorCanceledLogin = 1;
|
||||
const string sfAuthenticationErrorDomain = "com.apple.SafariServices.Authentication";
|
||||
#endif
|
||||
|
||||
TaskCompletionSource<WebAuthenticatorResult> tcsResponse;
|
||||
UIViewController currentViewController;
|
||||
Uri redirectUri;
|
||||
WebAuthenticatorOptions currentOptions;
|
||||
|
||||
#if IOS
|
||||
ASWebAuthenticationSession was;
|
||||
SFAuthenticationSession sf;
|
||||
#endif
|
||||
|
||||
public async Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
|
||||
{
|
||||
currentOptions = webAuthenticatorOptions;
|
||||
var url = webAuthenticatorOptions?.Url;
|
||||
var callbackUrl = webAuthenticatorOptions?.CallbackUrl;
|
||||
var prefersEphemeralWebBrowserSession = webAuthenticatorOptions?.PrefersEphemeralWebBrowserSession ?? false;
|
||||
|
||||
if (!VerifyHasUrlSchemeOrDoesntRequire(callbackUrl.Scheme))
|
||||
throw new InvalidOperationException("You must register your URL Scheme handler in your app's Info.plist.");
|
||||
|
||||
// Cancel any previous task that's still pending
|
||||
if (tcsResponse?.Task != null && !tcsResponse.Task.IsCompleted)
|
||||
tcsResponse.TrySetCanceled();
|
||||
|
||||
tcsResponse = new TaskCompletionSource<WebAuthenticatorResult>();
|
||||
redirectUri = callbackUrl;
|
||||
var scheme = redirectUri.Scheme;
|
||||
|
||||
#if IOS
|
||||
void AuthSessionCallback(NSUrl cbUrl, NSError error)
|
||||
{
|
||||
if (error == null)
|
||||
OpenUrlCallback(cbUrl);
|
||||
else if (error.Domain == asWebAuthenticationSessionErrorDomain && error.Code == asWebAuthenticationSessionErrorCodeCanceledLogin)
|
||||
tcsResponse.TrySetCanceled();
|
||||
else if (error.Domain == sfAuthenticationErrorDomain && error.Code == sfAuthenticationErrorCanceledLogin)
|
||||
tcsResponse.TrySetCanceled();
|
||||
else
|
||||
tcsResponse.TrySetException(new NSErrorException(error));
|
||||
|
||||
was = null;
|
||||
sf = null;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(12))
|
||||
{
|
||||
was = new ASWebAuthenticationSession(MAUI.WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
|
||||
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(13))
|
||||
{
|
||||
var ctx = new ContextProvider(webAuthenticatorOptions.ShouldUseSharedApplicationKeyWindow
|
||||
? GetWorkaroundedUIWindow()
|
||||
: WindowStateManager.Default.GetCurrentUIWindow());
|
||||
was.PresentationContextProvider = ctx;
|
||||
was.PrefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession;
|
||||
}
|
||||
else if (prefersEphemeralWebBrowserSession)
|
||||
{
|
||||
ClearCookies();
|
||||
}
|
||||
|
||||
using (was)
|
||||
{
|
||||
#pragma warning disable CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
|
||||
was.Start();
|
||||
#pragma warning restore CA1416
|
||||
return await tcsResponse.Task;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefersEphemeralWebBrowserSession)
|
||||
ClearCookies();
|
||||
|
||||
#pragma warning disable CA1422 // 'SFAuthenticationSession' is obsoleted on: 'ios' 12.0 and later
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(11))
|
||||
{
|
||||
sf = new SFAuthenticationSession(MAUI.WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
|
||||
using (sf)
|
||||
{
|
||||
sf.Start();
|
||||
return await tcsResponse.Task;
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1422
|
||||
|
||||
// This is only on iOS9+ but we only support 10+ in Essentials anyway
|
||||
var controller = new SFSafariViewController(MAUI.WebUtils.GetNativeUrl(url), false)
|
||||
{
|
||||
Delegate = new NativeSFSafariViewControllerDelegate
|
||||
{
|
||||
DidFinishHandler = (svc) =>
|
||||
{
|
||||
// Cancel our task if it wasn't already marked as completed
|
||||
if (!(tcsResponse?.Task?.IsCompleted ?? true))
|
||||
tcsResponse.TrySetCanceled();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
currentViewController = controller;
|
||||
await WindowStateManager.Default.GetCurrentUIViewController().PresentViewControllerAsync(controller, true);
|
||||
#else
|
||||
var opened = UIApplication.SharedApplication.OpenUrl(url);
|
||||
if (!opened)
|
||||
tcsResponse.TrySetException(new Exception("Error opening Safari"));
|
||||
#endif
|
||||
|
||||
return await tcsResponse.Task;
|
||||
}
|
||||
|
||||
private UIWindow GetWorkaroundedUIWindow(bool throwIfNull = false)
|
||||
{
|
||||
var window = UIApplication.SharedApplication.KeyWindow;
|
||||
|
||||
if (window != null && window.WindowLevel == UIWindowLevel.Normal)
|
||||
return window;
|
||||
|
||||
if (window == null)
|
||||
{
|
||||
window = UIApplication.SharedApplication
|
||||
.Windows
|
||||
.OrderByDescending(w => w.WindowLevel)
|
||||
.FirstOrDefault(w => w.RootViewController != null && w.WindowLevel == UIWindowLevel.Normal);
|
||||
}
|
||||
|
||||
if (throwIfNull && window == null)
|
||||
throw new InvalidOperationException("Could not find current window.");
|
||||
|
||||
return window;
|
||||
|
||||
}
|
||||
|
||||
void ClearCookies()
|
||||
{
|
||||
NSUrlCache.SharedCache.RemoveAllCachedResponses();
|
||||
|
||||
#if IOS
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(11))
|
||||
{
|
||||
WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.GetAllCookies((cookies) =>
|
||||
{
|
||||
foreach (var cookie in cookies)
|
||||
{
|
||||
#pragma warning disable CA1416 // Known false positive with lambda, here we can also assert the version
|
||||
WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.DeleteCookie(cookie, null);
|
||||
#pragma warning restore CA1416
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool OpenUrlCallback(Uri uri)
|
||||
{
|
||||
// If we aren't waiting on a task, don't handle the url
|
||||
if (tcsResponse?.Task?.IsCompleted ?? true)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// If we can't handle the url, don't
|
||||
if (!MAUI.WebUtils.CanHandleCallback(redirectUri, uri))
|
||||
return false;
|
||||
|
||||
currentViewController?.DismissViewControllerAsync(true);
|
||||
currentViewController = null;
|
||||
|
||||
tcsResponse.TrySetResult(new WebAuthenticatorResult(uri, currentOptions?.ResponseDecoder));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO change this to ILogger?
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool VerifyHasUrlSchemeOrDoesntRequire(string scheme)
|
||||
{
|
||||
// app is currently supporting iOS11+ so no need for these checks.
|
||||
return true;
|
||||
//// iOS11+ uses sfAuthenticationSession which handles its own url routing
|
||||
//if (OperatingSystem.IsIOSVersionAtLeast(11, 0) || OperatingSystem.IsTvOSVersionAtLeast(11, 0))
|
||||
// return true;
|
||||
|
||||
//return AppInfoImplementation.VerifyHasUrlScheme(scheme);
|
||||
}
|
||||
|
||||
#if IOS
|
||||
class NativeSFSafariViewControllerDelegate : SFSafariViewControllerDelegate
|
||||
{
|
||||
public Action<SFSafariViewController> DidFinishHandler { get; set; }
|
||||
|
||||
public override void DidFinish(SFSafariViewController controller) =>
|
||||
DidFinishHandler?.Invoke(controller);
|
||||
}
|
||||
|
||||
class ContextProvider : NSObject, IASWebAuthenticationPresentationContextProviding
|
||||
{
|
||||
public ContextProvider(UIWindow window) =>
|
||||
Window = window;
|
||||
|
||||
public readonly UIWindow Window;
|
||||
|
||||
[Export("presentationAnchorForWebAuthenticationSession:")]
|
||||
public UIWindow GetPresentationAnchor(ASWebAuthenticationSession session)
|
||||
=> Window;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,188 +0,0 @@
|
||||
// This is a copy from MAUI Essentials WebAuthenticator
|
||||
|
||||
#if IOS
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Utilities.MAUI
|
||||
{
|
||||
/// <summary>
|
||||
/// A web navigation API intended to be used for authentication with external web services such as OAuth.
|
||||
/// </summary>
|
||||
public interface IWebAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Begin an authentication flow by navigating to the specified URL and waiting for a callback/redirect to the callback URL scheme.
|
||||
/// </summary>
|
||||
/// <param name="webAuthenticatorOptions">A <see cref="WebAuthenticatorOptions"/> instance containing additional configuration for this authentication call.</param>
|
||||
/// <returns>A <see cref="WebAuthenticatorResult"/> object with the results of this operation.</returns>
|
||||
Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides abstractions for the platform web authenticator callbacks triggered when using <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public interface IPlatformWebAuthenticatorCallback
|
||||
{
|
||||
#if IOS || MACCATALYST || MACOS
|
||||
/// <summary>
|
||||
/// Opens the specified URI to start the authentication flow.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to open that will start the authentication flow.</param>
|
||||
/// <returns><see langword="true"/> when the URI has been opened, otherwise <see langword="false"/>.</returns>
|
||||
bool OpenUrlCallback(Uri uri);
|
||||
#elif ANDROID
|
||||
/// <summary>
|
||||
/// The event that is triggered when an authentication flow calls back into the Android application.
|
||||
/// </summary>
|
||||
/// <param name="intent">An <see cref="Android.Content.Intent"/> object containing additional data about this resume operation.</param>
|
||||
/// <returns><see langword="true"/> when the callback can be processed, otherwise <see langword="false"/>.</returns>
|
||||
bool OnResumeCallback(Android.Content.Intent intent);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides abstractions used for decoding a URI returned from a authentication request, for use with <see cref="IWebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public interface IWebAuthenticatorResponseDecoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Decodes the given URIs query string into a dictionary.
|
||||
/// </summary>
|
||||
/// <param name="uri">The <see cref="Uri"/> object to decode the query parameters from.</param>
|
||||
/// <returns>A <see cref="IDictionary{TKey, TValue}"/> object where each of the query parameters values of <paramref name="uri"/> are accessible through their respective keys.</returns>
|
||||
IDictionary<string, string>? DecodeResponse(Uri uri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A web navigation API intended to be used for Authentication with external web services such as OAuth.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This API helps with navigating to a start URL and waiting for a callback URL to the app. Your app must
|
||||
/// be registered to handle the callback scheme you provide in the call to authenticate.
|
||||
/// </remarks>
|
||||
public static class WebAuthenticator
|
||||
{
|
||||
/// <summary>Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.</summary>
|
||||
/// <param name="url"> Url to navigate to, beginning the authentication flow.</param>
|
||||
/// <param name="callbackUrl"> Expected callback url that the navigation flow will eventually redirect to.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(Uri url, Uri callbackUrl)
|
||||
=> Current.AuthenticateAsync(url, callbackUrl);
|
||||
|
||||
/// <summary>Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.The start url and callbackUrl are specified in the webAuthenticatorOptions.</summary>
|
||||
/// <param name="webAuthenticatorOptions">Options to configure the authentication request.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
|
||||
=> Current.AuthenticateAsync(webAuthenticatorOptions);
|
||||
|
||||
static IWebAuthenticator Current => Utilities.MAUI.WebAuthenticator.Default;
|
||||
|
||||
static IWebAuthenticator? defaultImplementation;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the default implementation for static usage of this API.
|
||||
/// </summary>
|
||||
public static IWebAuthenticator Default =>
|
||||
defaultImplementation ??= new MAUI.WebAuthenticatorImplementation();
|
||||
|
||||
internal static void SetDefault(IWebAuthenticator? implementation) =>
|
||||
defaultImplementation = implementation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class contains static extension methods for use with <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public static class WebAuthenticatorExtensions
|
||||
{
|
||||
static IPlatformWebAuthenticatorCallback AsPlatformCallback(this IWebAuthenticator webAuthenticator)
|
||||
{
|
||||
if (webAuthenticator is not IPlatformWebAuthenticatorCallback platform)
|
||||
throw new PlatformNotSupportedException("This implementation of IWebAuthenticator does not implement IPlatformWebAuthenticatorCallback.");
|
||||
return platform;
|
||||
}
|
||||
|
||||
#if ANDROID
|
||||
internal static bool IsAuthenticatingWithCustomTabs(this IWebAuthenticator webAuthenticator)
|
||||
=> (webAuthenticator as MAUI.WebAuthenticatorImplementation)?.AuthenticatingWithCustomTabs ?? false;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Begin an authentication flow by navigating to the specified url and waiting for a callback/redirect to the callbackUrl scheme.
|
||||
/// </summary>
|
||||
/// <param name="webAuthenticator">The <see cref="IWebAuthenticator"/> to use for the authentication flow.</param>
|
||||
/// <param name="url"> Url to navigate to, beginning the authentication flow.</param>
|
||||
/// <param name="callbackUrl"> Expected callback url that the navigation flow will eventually redirect to.</param>
|
||||
/// <returns>Returns a result parsed out from the callback url.</returns>
|
||||
public static Task<WebAuthenticatorResult> AuthenticateAsync(this IWebAuthenticator webAuthenticator, Uri url, Uri callbackUrl) =>
|
||||
webAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions { Url = url, CallbackUrl = callbackUrl });
|
||||
|
||||
#if IOS || MACCATALYST || MACOS
|
||||
/// <inheritdoc cref="IPlatformWebAuthenticatorCallback.OpenUrlCallback(Uri)"/>
|
||||
public static bool OpenUrl(this IWebAuthenticator webAuthenticator, Uri uri) =>
|
||||
webAuthenticator.AsPlatformCallback().OpenUrlCallback(uri);
|
||||
|
||||
/// <inheritdoc cref="ApplicationModel.Platform.OpenUrl(UIKit.UIApplication, Foundation.NSUrl, Foundation.NSDictionary)"/>
|
||||
public static bool OpenUrl(this IWebAuthenticator webAuthenticator, UIKit.UIApplication app, Foundation.NSUrl url, Foundation.NSDictionary options)
|
||||
{
|
||||
if(url?.AbsoluteString != null)
|
||||
{
|
||||
return webAuthenticator.OpenUrl(new Uri(url.AbsoluteString));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ApplicationModel.Platform.ContinueUserActivity(UIKit.UIApplication, Foundation.NSUserActivity, UIKit.UIApplicationRestorationHandler)"/>
|
||||
public static bool ContinueUserActivity(this IWebAuthenticator webAuthenticator, UIKit.UIApplication application, Foundation.NSUserActivity userActivity, UIKit.UIApplicationRestorationHandler completionHandler)
|
||||
{
|
||||
var uri = userActivity?.WebPageUrl?.AbsoluteString;
|
||||
if (string.IsNullOrEmpty(uri))
|
||||
return false;
|
||||
|
||||
return webAuthenticator.OpenUrl(new Uri(uri));
|
||||
}
|
||||
|
||||
#elif ANDROID
|
||||
/// <inheritdoc cref="IPlatformWebAuthenticatorCallback.OnResumeCallback(Android.Content.Intent)"/>
|
||||
public static bool OnResume(this IWebAuthenticator webAuthenticator, Android.Content.Intent intent) =>
|
||||
webAuthenticator.AsPlatformCallback().OnResumeCallback(intent);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents additional options for <see cref="WebAuthenticator"/>.
|
||||
/// </summary>
|
||||
public class WebAuthenticatorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL that will start the authentication flow.
|
||||
/// </summary>
|
||||
public Uri? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback URL that should be called when authentication completes.
|
||||
/// </summary>
|
||||
public Uri? CallbackUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the browser used for the authentication flow is short-lived.
|
||||
/// This means it will not share session nor cookies with the regular browser on this device if set the <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This setting only has effect on iOS.</remarks>
|
||||
public bool PrefersEphemeralWebBrowserSession { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the decoder implementation used to decode the incoming authentication result URI.
|
||||
/// </summary>
|
||||
public IWebAuthenticatorResponseDecoder? ResponseDecoder { get; set; }
|
||||
|
||||
public bool ShouldUseSharedApplicationKeyWindow { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,152 +0,0 @@
|
||||
// This is a copy from MAUI Essentials WebAuthenticator
|
||||
|
||||
#if IOS
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Utilities.MAUI;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
|
||||
namespace Bit.Core.Utilities.MAUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Web Authenticator Result object parsed from the callback Url.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All of the query string or url fragment properties are parsed into a dictionary and can be accessed by their key.
|
||||
/// </remarks>
|
||||
public class WebAuthenticatorResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class.
|
||||
/// </summary>
|
||||
public WebAuthenticatorResult()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class by parsing a URI's query string parameters.
|
||||
/// </summary>
|
||||
/// <param name="uri">The callback uri that was used to end the authentication sequence.</param>
|
||||
public WebAuthenticatorResult(Uri uri) : this(uri, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebAuthenticatorResult"/> class by parsing a URI's query string parameters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the responseDecoder is non-null, then it is used to decode the fragment or query string
|
||||
/// returned by the authorization service. Otherwise, a default response decoder is used.
|
||||
/// </remarks>
|
||||
/// <param name="uri">The callback uri that was used to end the authentication sequence.</param>
|
||||
/// <param name="responseDecoder">The decoder that can be used to decode the callback uri.</param>
|
||||
public WebAuthenticatorResult(Uri uri, IWebAuthenticatorResponseDecoder responseDecoder)
|
||||
{
|
||||
CallbackUri = uri;
|
||||
var properties = responseDecoder?.DecodeResponse(uri) ?? WebUtils.ParseQueryString(uri);
|
||||
foreach (var kvp in properties)
|
||||
{
|
||||
Properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance from an existing dictionary.
|
||||
/// </summary>
|
||||
/// <param name="properties">The dictionary of properties to incorporate.</param>
|
||||
public WebAuthenticatorResult(IDictionary<string, string> properties)
|
||||
{
|
||||
foreach (var kvp in properties)
|
||||
Properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The uri that was used to call back with the access token.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The value of the callback URI, including the fragment or query string bearing
|
||||
/// the access token and associated information.
|
||||
/// </value>
|
||||
public Uri CallbackUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp when the class was instantiated, which usually corresponds with the parsed result of a request.
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; set; } = new DateTimeOffset(DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// The dictionary of key/value pairs parsed form the callback URI's query string.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Properties { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Puts a key/value pair into the dictionary.</summary>
|
||||
public void Put(string key, string value)
|
||||
=> Properties[key] = value;
|
||||
|
||||
/// <summary>Gets a value for a given key from the dictionary.</summary>
|
||||
/// <param name="key">Key from the callback URI's query string.</param>
|
||||
public string Get(string key)
|
||||
{
|
||||
if (Properties.TryGetValue(key, out var v))
|
||||
return v;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>The value for the `access_token` key.</summary>
|
||||
/// <value>Access Token parsed from the callback URI access_token parameter.</value>
|
||||
public string AccessToken
|
||||
=> Get("access_token");
|
||||
|
||||
/// <summary>The value for the `refresh_token` key.</summary>
|
||||
/// <value>Refresh Token parsed from the callback URI refresh_token parameter.</value>
|
||||
public string RefreshToken
|
||||
=> Get("refresh_token");
|
||||
|
||||
/// <summary>The value for the `id_token` key.</summary>
|
||||
/// <value>The value for the `id_token` key.</value>
|
||||
/// <remarks>Apple doesn't return an access token on iOS native sign in, but it does return id_token as a JWT.</remarks>
|
||||
public string IdToken
|
||||
=> Get("id_token");
|
||||
|
||||
/// <summary>
|
||||
/// The refresh token expiry date as calculated by the timestamp of when the result was created plus
|
||||
/// the value in seconds for the refresh_token_expires_in key.
|
||||
/// </summary>
|
||||
/// <value>Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.</value>
|
||||
public DateTimeOffset? RefreshTokenExpiresIn
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Properties.TryGetValue("refresh_token_expires_in", out var v))
|
||||
{
|
||||
if (int.TryParse(v, out var i))
|
||||
return Timestamp.AddSeconds(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The expiry date as calculated by the timestamp of when the result was created plus
|
||||
/// the value in seconds for the `expires_in` key.
|
||||
/// </summary>
|
||||
/// <value>Timestamp of the creation of the object instance plus the expires_in seconds parsed from the callback URI.</value>
|
||||
public DateTimeOffset? ExpiresIn
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Properties.TryGetValue("expires_in", out var v))
|
||||
{
|
||||
if (int.TryParse(v, out var i))
|
||||
return Timestamp.AddSeconds(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,126 +0,0 @@
|
||||
// This is copied from MAUI repo to be used from WebAuthenticator
|
||||
// https://github.com/dotnet/maui/blob/main/src/Essentials/src/Types/Shared/WebUtils.shared.cs
|
||||
|
||||
#if IOS
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Bit.Core.Utilities.MAUI
|
||||
{
|
||||
static class WebUtils
|
||||
{
|
||||
internal static IDictionary<string, string> ParseQueryString(Uri uri)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
if (uri == null)
|
||||
return parameters;
|
||||
|
||||
// Note: Uri.Query starts with a '?'
|
||||
if (!string.IsNullOrEmpty(uri.Query))
|
||||
UnpackParameters(uri.Query.AsSpan(1), parameters);
|
||||
|
||||
// Note: Uri.Fragment starts with a '#'
|
||||
if (!string.IsNullOrEmpty(uri.Fragment))
|
||||
UnpackParameters(uri.Fragment.AsSpan(1), parameters);
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
// The following method is a port of the logic found in https://source.dot.net/#Microsoft.AspNetCore.WebUtilities/src/Shared/QueryStringEnumerable.cs
|
||||
// but refactored such that it:
|
||||
//
|
||||
// 1. avoids the IEnumerable overhead that isn't needed (the ASP.NET logic was clearly designed that way to offer a public API whereas we don't need that)
|
||||
// 2. avoids the use of unsafe code
|
||||
static void UnpackParameters(ReadOnlySpan<char> query, Dictionary<string, string> parameters)
|
||||
{
|
||||
while (!query.IsEmpty)
|
||||
{
|
||||
int delimeterIndex = query.IndexOf('&');
|
||||
ReadOnlySpan<char> segment;
|
||||
|
||||
if (delimeterIndex >= 0)
|
||||
{
|
||||
segment = query.Slice(0, delimeterIndex);
|
||||
query = query.Slice(delimeterIndex + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = query;
|
||||
query = default;
|
||||
}
|
||||
|
||||
// If it's nonempty, emit it
|
||||
if (!segment.IsEmpty)
|
||||
{
|
||||
var equalIndex = segment.IndexOf('=');
|
||||
string name, value;
|
||||
|
||||
if (equalIndex >= 0)
|
||||
{
|
||||
name = segment.Slice(0, equalIndex).ToString();
|
||||
|
||||
var span = segment.Slice(equalIndex + 1);
|
||||
var chars = new char[span.Length];
|
||||
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
chars[i] = span[i] == '+' ? ' ' : span[i];
|
||||
|
||||
value = new string(chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = segment.ToString();
|
||||
value = string.Empty;
|
||||
}
|
||||
|
||||
name = Uri.UnescapeDataString(name);
|
||||
|
||||
parameters[name] = Uri.UnescapeDataString(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Uri EscapeUri(Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
|
||||
var idn = new global::System.Globalization.IdnMapping();
|
||||
return new Uri(uri.Scheme + "://" + idn.GetAscii(uri.Authority) + uri.PathAndQuery + uri.Fragment);
|
||||
}
|
||||
|
||||
internal static bool CanHandleCallback(Uri expectedUrl, Uri callbackUrl)
|
||||
{
|
||||
if (!callbackUrl.Scheme.Equals(expectedUrl.Scheme, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(expectedUrl.Host))
|
||||
{
|
||||
if (!callbackUrl.Host.Equals(expectedUrl.Host, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if __IOS__ || __TVOS__ || __MACOS__
|
||||
internal static Foundation.NSUrl GetNativeUrl(Uri uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Foundation.NSUrl(uri.OriginalString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Unable to create NSUrl from Original string, trying Absolute URI: {ex.Message}");
|
||||
return new Foundation.NSUrl(uri.AbsoluteUri);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -55,6 +55,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
ExtContext = ExtensionContext
|
||||
};
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -155,7 +156,6 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
@@ -452,11 +452,7 @@ namespace Bit.iOS.Autofill
|
||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||
{
|
||||
vm.SubmitSuccessTask = async () =>
|
||||
{
|
||||
await DismissViewControllerAsync(false);
|
||||
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
|
||||
};
|
||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
@@ -522,9 +518,8 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private void LaunchLoginSsoFlow()
|
||||
{
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var loginPage = new LoginSsoPage(appOptions);
|
||||
var app = new App.App(appOptions);
|
||||
var loginPage = new LoginSsoPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(app.Resources);
|
||||
ThemeManager.ApplyResourcesTo(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using MapKit;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
@@ -33,29 +33,22 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
public override async void ViewDidLoad()
|
||||
{
|
||||
try
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
{
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
{
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
}
|
||||
|
||||
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
||||
|
||||
@@ -16,7 +16,6 @@ using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using Microsoft.Maui.Controls.Compatibility;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
@@ -223,27 +222,20 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.ViewDidAppear(animated);
|
||||
base.ViewDidAppear(animated);
|
||||
|
||||
// Users with key connector and without biometric or pin has no MP to unlock with
|
||||
if (!_hasMasterPassword)
|
||||
// Users with key connector and without biometric or pin has no MP to unlock with
|
||||
if (!_hasMasterPassword)
|
||||
{
|
||||
if (!(_pinEnabled || _biometricEnabled) ||
|
||||
(_biometricEnabled && !_biometricIntegrityValid))
|
||||
{
|
||||
if (!(_pinEnabled || _biometricEnabled) ||
|
||||
(_biometricEnabled && !_biometricIntegrityValid))
|
||||
{
|
||||
PromptSSO();
|
||||
}
|
||||
}
|
||||
else if (!_biometricEnabled || !_biometricIntegrityValid)
|
||||
{
|
||||
MasterPasswordCell.TextField.BecomeFirstResponder();
|
||||
PromptSSO();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (!_biometricEnabled || !_biometricIntegrityValid)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
MasterPasswordCell.TextField.BecomeFirstResponder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,9 +433,8 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
public void PromptSSO()
|
||||
{
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var loginPage = new LoginSsoPage(appOptions);
|
||||
var app = new App.App(appOptions);
|
||||
var loginPage = new LoginSsoPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(app.Resources);
|
||||
ThemeManager.ApplyResourcesTo(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
@@ -453,7 +444,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
}
|
||||
|
||||
var navigationPage = new NavigationPage(loginPage);
|
||||
var loginController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
|
||||
var loginController = navigationPage.CreateViewController();
|
||||
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(loginController, true, null);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using Microsoft.Maui.Controls.Compatibility;
|
||||
using Microsoft.Maui.Platform;
|
||||
|
||||
namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
@@ -240,7 +239,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
ThemeManager.ApplyResourcesTo(generatorPage);
|
||||
|
||||
var navigationPage = new NavigationPage(generatorPage);
|
||||
var generatorController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
|
||||
var generatorController = navigationPage.CreateViewController();
|
||||
generatorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
PresentViewController(generatorController, true, null);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ using Bit.iOS.Core.Views;
|
||||
using Bit.iOS.Extension.Models;
|
||||
using CoreNFC;
|
||||
using Foundation;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using MobileCoreServices;
|
||||
@@ -529,11 +528,7 @@ namespace Bit.iOS.Extension
|
||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||
{
|
||||
vm.SubmitSuccessTask = async () =>
|
||||
{
|
||||
await DismissViewControllerAsync(false);
|
||||
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
|
||||
};
|
||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
||||
}
|
||||
|
||||
@@ -599,9 +594,8 @@ namespace Bit.iOS.Extension
|
||||
|
||||
private void LaunchLoginSsoFlow()
|
||||
{
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var loginPage = new LoginSsoPage(appOptions);
|
||||
var app = new App.App(appOptions);
|
||||
var loginPage = new LoginSsoPage();
|
||||
var app = new App.App(new AppOptions { IosExtension = true });
|
||||
ThemeManager.SetTheme(app.Resources);
|
||||
ThemeManager.ApplyResourcesTo(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
|
||||
@@ -17,7 +17,6 @@ using Bit.iOS.Core.Views;
|
||||
using Bit.iOS.ShareExtension.Models;
|
||||
using CoreNFC;
|
||||
using Foundation;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using MobileCoreServices;
|
||||
@@ -322,11 +321,7 @@ namespace Bit.iOS.ShareExtension
|
||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||
{
|
||||
vm.SubmitSuccessTask = async () =>
|
||||
{
|
||||
await DismissViewControllerAsync(false);
|
||||
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
|
||||
};
|
||||
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
|
||||
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
|
||||
}
|
||||
|
||||
@@ -381,7 +376,7 @@ namespace Bit.iOS.ShareExtension
|
||||
|
||||
private void LaunchLoginSsoFlow()
|
||||
{
|
||||
var loginPage = new LoginSsoPage(_appOptions.Value);
|
||||
var loginPage = new LoginSsoPage();
|
||||
SetupAppAndApplyResources(loginPage);
|
||||
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"ThereAreNoItemsToList" = "Não há itens a enumerar";
|
||||
"ThereAreNoItemsToList" = "Não existem itens para listar";
|
||||
"ToViewVerificationCodesUpgradeToPremium" = "Para ver os códigos de verificação, atualize para o Premium";
|
||||
"Add2FactorAutenticationToAnItemToViewVerificationCodes" = "Adicione a autenticação de 2 fatores a um item para ver os códigos de verificação";
|
||||
"LogInToBitwardenOnYourIPhoneToViewVerificationCodes" = "Inicie sessão no Bitwarden no seu iPhone para ver os códigos de verificação";
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>„Bitwarden“ slaptažodžių tvarkyklė</value>
|
||||
<value>Bitwarden – nemokama slaptažodžių tvarkyklė</value>
|
||||
<comment>Max 30 characters</comment>
|
||||
</data>
|
||||
<data name="Description" xml:space="preserve">
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RootNamespace>Bit.Publisher</RootNamespace>
|
||||
<Configurations>Debug;Release;FDroid</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Apis.AndroidPublisher.v3" Version="1.66.0.3338" />
|
||||
<PackageReference Include="Google.Apis.AndroidPublisher.v3" Version="1.52.0.2350" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user