1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-22 04:13:19 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Federico Maccaroni
7a437a0649 Update ThemeManager.cs to fix build failing 2024-02-12 14:36:41 -03:00
81 changed files with 558 additions and 2016 deletions

4
.github/CODEOWNERS vendored
View File

@@ -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 ##

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>

Binary file not shown.

BIN
.github/secrets/play_creds.json.gpg vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +0,0 @@
---
name: Build Beta
on:
workflow_dispatch:

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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>

View File

@@ -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:

View File

@@ -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);

View File

@@ -5,8 +5,7 @@ namespace Bit.Core.Abstractions
{
public enum AwaiterPrecondition
{
EnvironmentUrlsInited,
AndroidWindowCreated
EnvironmentUrlsInited
}
public interface IConditionedAwaiterManager

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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>

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
}

View File

@@ -85,6 +85,7 @@
<Button
Text="{u:I18n DenyLogIn}"
Command="{Binding RejectRequestCommand}"
StyleClass="btn-secundary"
AutomationId="DenyLoginButton" />
</StackLayout>

View File

@@ -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());

View File

@@ -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);

View File

@@ -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"

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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}"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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)

View File

@@ -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()

View File

@@ -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>

View File

@@ -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>

View File

@@ -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, 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>Cant 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>

View File

@@ -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 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 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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">

View File

@@ -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){ }
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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";

View File

@@ -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">

View File

@@ -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>