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

Compare commits

...

13 Commits

Author SHA1 Message Date
Vince Grassia
79da56d94e Fix release 2024-03-07 10:02:16 -05:00
Vince Grassia
e918307a24 Update release workflow with proper paths (#3059)
(cherry picked from commit 82c2e91446)
2024-03-07 09:55:10 -05:00
Federico Maccaroni
ecb0cbce14 PM-6552 Added missing using 2024-03-05 18:27:24 -03:00
Federico Maccaroni
5e549ff2a2 [PM-6552] Fix for Android Window issues when opening Autofill/Accessibility 2024-03-05 18:26:58 -03:00
Federico Maccaroni
c60fc28289 PM-6552 Fix for App only showing Home (Login) Page after closed after opening Accessibility Settings 2024-03-05 18:26:28 -03:00
Federico Maccaroni
ad308c97c9 [PM-6539] Fix Autofill Extension TDE without MP flow (#3049) 2024-03-05 18:13:57 -03:00
Vince Grassia
7f92358d9b Fix GoogleServices file location (#3053)
(cherry picked from commit 6f6487ccc9)
2024-03-04 10:24:58 -05:00
Vince Grassia
5338fdea79 Change version to proper value (#3041)
(cherry picked from commit 22d0cc681c)
2024-02-28 15:58:46 -05:00
André Bispo
36c32fbabd [PM-6506] Fix double execution of command on returnType Go (#3039)
cherrypicked from: 4e0a18cce5
2024-02-28 17:52:10 +00:00
Vince Grassia
6bf7a492c6 DEVOPS-1746 - Update iOS distribution certificate and profiles (#3018)
(cherry picked from commit c9fdfa7a15)
2024-02-28 09:44:07 -05:00
Vince Grassia
6b54fadacc Uncomment Android (prod) build 2024-02-27 15:47:39 -05:00
Vince Grassia
06fe3f78f8 DEVOPS-1834 - Apply fix for signing issue (#3038)
(cherry picked from commit 850a7e754a)
2024-02-27 15:46:52 -05:00
Vince Grassia
250d40663b Disable Android (prod) job 2024-02-27 12:34:33 -05:00
43 changed files with 1028 additions and 316 deletions

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

@@ -1,3 +0,0 @@
<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.

Binary file not shown.

Binary file not shown.

View File

@@ -31,6 +31,7 @@ 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
@@ -58,6 +59,7 @@ jobs:
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
fi
android:
name: Android
runs-on: windows-2022
@@ -67,7 +69,8 @@ jobs:
matrix:
variant: ["prod", "qa"]
env:
android_folder_path: src/App/Platforms/Android
android_folder_path: src\App\Platforms\Android
android_folder_path_bash: src/App/Platforms/Android
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
@@ -77,14 +80,12 @@ jobs:
- name: Set up .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: |
3.1.x
8.0.x
dotnet-version: '8.0.x'
- 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
@@ -95,7 +96,8 @@ 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
@@ -111,27 +113,34 @@ jobs:
with:
fetch-depth: 0
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
run: |
mkdir -p ~/secrets
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
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
- name: Download secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p $HOME/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
shell: bash
- name: Decrypt secrets - Google Services
- name: Download secrets - Google Services
if: ${{ matrix.variant == 'prod' }}
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./${{ env.android_folder_path }}/google-services.json ./.github/secrets/google-services.json.gpg
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
shell: bash
- name: Increment version
@@ -141,9 +150,9 @@ jobs:
echo "########################################"
echo "##### Setting Version Code $BUILD_NUMBER"
echo "########################################"
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
./${{ env.android_folder_path }}/AndroidManifest.xml
./${{ env.android_folder_path_bash }}/AndroidManifest.xml
shell: bash
- name: Restore packages
@@ -152,78 +161,70 @@ jobs:
- name: Restore tools
run: dotnet tool restore
# - name: Verify Format
# run: dotnet tool run dotnet-format --check
# - name: Run Core tests
# run: |
# dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" `
# /p:CustomConstants=UT
- 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: 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 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
- name: Build & Sign Android
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 "########################################"
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
$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
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 "########################################"
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
$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
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
@@ -285,13 +286,12 @@ jobs:
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc' ) }}
run: |
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
CREDS_PATH="$HOME/secrets/play_creds.json"
AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab"
TRACK="internal"
$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"
dotnet $PUBLISHER_PATH $CREDS_PATH $AAB_PATH $TRACK
shell: bash
dotnet $publisherPath $credsPath $aabPath $track
f-droid:
@@ -314,7 +314,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,23 +339,26 @@ jobs:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
run: |
mkdir -p ~/secrets
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
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
- name: Download secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
FILE: app_fdroid-keystore.jks
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
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 }}
@@ -368,16 +371,12 @@ jobs:
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
Write-Output "########################################"
Write-Output "##### Backup project files"
Write-Output "########################################"
Write-Output "##### Back up project files"
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);
@@ -395,10 +394,8 @@ jobs:
$configuration = "Release";
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
Write-Output "########################################"
Write-Output "##### Build $configuration FDROID
Write-Output "########################################"
Write-Output "##### Build $configuration FDROID"
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android /p:CustomConstants="FDROID"
- name: Sign for F-Droid
@@ -408,15 +405,11 @@ 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");
@@ -442,6 +435,7 @@ jobs:
path: ./bw-fdroid-apk-sha256.txt
if-no-files-found: error
ios:
name: Apple iOS
runs-on: macos-13
@@ -460,13 +454,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
@@ -495,42 +489,42 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "appcenter-ios-token"
- name: Decrypt secrets
- name: Download Provisioning Profiles secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: profiles
run: |
mkdir -p ~/secrets
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"
)
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
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
- 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
@@ -538,30 +532,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 ~/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 import $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -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
@@ -570,8 +564,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_watch_app.mobileprovision
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_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
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
mkdir -p "$PROFILES_DIR_PATH"
@@ -599,68 +593,44 @@ 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: |
Write-Output "########################################"
Write-Output "##### Archive for Release ios-arm64
Write-Output "########################################"
echo "##### Archive for Release ios-arm64"
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: |
Write-Output "########################################"
Write-Output "##### Archive Debug for iossimulator-x64
Write-Output "########################################"
echo "##### Archive Debug for iossimulator-x64"
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
Write-Output "########################################"
Write-Output "##### Done"
Write-Output "########################################"
ls ~/Library/Developer/Xcode/Archives
shell: pwsh
ls $HOME/Library/Developer/Xcode/Archives
- 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
@@ -709,10 +679,7 @@ 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
@@ -728,7 +695,6 @@ 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: |
@@ -778,7 +744,7 @@ jobs:
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

@@ -28,7 +28,7 @@ jobs:
branch-name: ${{ steps.branch.outputs.branch-name }}
steps:
- name: Branch check
if: github.event.inputs.release_type != 'Dry Run'
if: 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: ${{ github.event.inputs.release_type }}
release-type: ${{ inputs.release_type }}
project-type: xamarin
file: src/Android/Properties/AndroidManifest.xml
file: src/App/Platforms/Android/AndroidManifest.xml
- name: Get branch name
id: branch
@@ -55,7 +55,7 @@ jobs:
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create GitHub deployment
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ inputs.release_type != 'Dry Run' }}
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
id: deployment
with:
@@ -67,7 +67,7 @@ jobs:
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
with:
workflow: build.yml
@@ -75,7 +75,7 @@ jobs:
branch: ${{ steps.branch.outputs.branch-name }}
- name: Dry Run - Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
if: ${{ inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
with:
workflow: build.yml
@@ -86,7 +86,7 @@ jobs:
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
- name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
with:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
@@ -103,16 +103,16 @@ jobs:
draft: true
- name: Update deployment status to Success
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
if: ${{ inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1
if: ${{ inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
@@ -129,7 +129,7 @@ jobs:
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
with:
workflow: build.yml
@@ -138,7 +138,7 @@ jobs:
name: com.x8bit.bitwarden-fdroid.apk
- name: Dry Run - Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
if: ${{ inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
with:
workflow: build.yml
@@ -176,13 +176,19 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Decrypt secrets
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Download secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ~/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./store/fdroid/keystore.jks ./.github/secrets/store_fdroid-keystore.jks.gpg
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
- name: Compile for F-Droid Store
env:
@@ -211,5 +217,5 @@ jobs:
cd $GITHUB_WORKSPACE
- name: Deploy to gh-pages
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
if: ${{ inputs.release_type != 'Dry Run' }}
run: npm run deploy

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.2.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.2.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden</string>
<key>CFBundleShortVersionString</key>
<string>2024.2.2</string>
<string>2024.2.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleIconName</key>

View File

@@ -46,7 +46,6 @@ 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
@@ -120,41 +119,8 @@ namespace Bit.App
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}
//"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));
_isResumed = true;
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
}
#else
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
@@ -171,7 +137,7 @@ namespace Bit.App
Application.Current.MainPage = value;
}
}
}
}
#endif
public App() : this(null)

View File

@@ -77,6 +77,7 @@
<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">
@@ -107,5 +108,6 @@
<ItemGroup>
<None Remove="Controls\Picker\" />
<None Remove="Controls\Avatar\" />
<None Remove="Utilities\WebAuthenticatorMAUI\" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
using Microsoft.Maui.Platform;
namespace Bit.App.Pages
{
@@ -26,7 +27,7 @@ namespace Bit.App.Pages
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
_identityEntry.ReturnType = ReturnType.Next;
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
_vm.SubmitSuccessAction = () => MainThread.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
_vm.SubmitSuccessTask = () => MainThread.InvokeOnMainThreadAsync(SubmitSuccessAsync);
_vm.CloseAction = async () =>
{
await Navigation.PopModalAsync();
@@ -37,6 +38,12 @@ 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 Action SubmitSuccessAction { get; set; }
public Func<Task> SubmitSuccessTask { get; set; }
public Action CloseAction { get; set; }
public async Task SubmitAsync()
@@ -73,7 +73,10 @@ namespace Bit.App.Pages
IconsUrl = resUrls.Icons;
NotificationsUrls = resUrls.Notifications;
SubmitSuccessAction?.Invoke();
if (SubmitSuccessTask != null)
{
await SubmitSuccessTask();
}
}
public bool ValidateUrls()

View File

@@ -21,6 +21,7 @@ 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,6 +15,16 @@ 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
{
@@ -64,6 +74,8 @@ 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; }
@@ -153,6 +165,9 @@ 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

@@ -1,24 +1,40 @@
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 readonly IAccountsManager _accountsManager;
private readonly IConditionedAwaiterManager _conditionedAwaiterManager;
public AndroidNavigationRedirectPage()
private AppOptions _options;
public AndroidNavigationRedirectPage(AppOptions options)
{
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
_options = options ?? new AppOptions();
InitializeComponent();
}
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
{
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
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"));
}
}
}

View File

@@ -32,14 +32,4 @@
IsActive = false;
}
}
public class MainAppWindow : ResumeWindow
{
public MainAppWindow(Page page) : base(page) { }
}
public class AutoFillWindow : ResumeWindow
{
public AutoFillWindow(Page page) : base(page){ }
}
}

View File

@@ -1,10 +1,6 @@
#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
@@ -55,3 +51,5 @@ namespace Bit.Core.Services
}
}
#endif

View File

@@ -849,16 +849,18 @@ namespace Bit.Core.Services
{
// account data
var state = await GetValueAsync<State>(Storage.Prefs, V7Keys.StateKey);
// Migrate environment data to use Regions
foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null))
if (state != null)
{
var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls();
account.Value.Settings.Region = urls.Region;
account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls;
}
// 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);
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();

View File

@@ -0,0 +1,244 @@
// 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

@@ -0,0 +1,188 @@
// 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

@@ -0,0 +1,152 @@
// 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

@@ -0,0 +1,126 @@
// 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,7 +55,6 @@ namespace Bit.iOS.Autofill
{
ExtContext = ExtensionContext
};
}
catch (Exception ex)
{
@@ -156,6 +155,7 @@ namespace Bit.iOS.Autofill
{
InitAppIfNeeded();
_context.Configuring = true;
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
@@ -452,7 +452,11 @@ namespace Bit.iOS.Autofill
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessTask = async () =>
{
await DismissViewControllerAsync(false);
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
};
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
@@ -518,8 +522,9 @@ namespace Bit.iOS.Autofill
private void LaunchLoginSsoFlow()
{
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var loginPage = new LoginSsoPage(appOptions);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.autofill</string>
<key>CFBundleShortVersionString</key>
<string>2024.2.2</string>
<string>2024.2.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleLocalizations</key>

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,22 +33,29 @@ namespace Bit.iOS.Autofill
public override async void ViewDidLoad()
{
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
base.ViewDidLoad();
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
try
{
_cancelButton,
new UIBarButtonItem(_accountSwitchButton)
}, false);
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
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);
}
}
private void CancelButton_TouchUpInside(object sender, EventArgs e)

View File

@@ -16,6 +16,7 @@ using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Platform;
namespace Bit.iOS.Core.Controllers
{
@@ -222,20 +223,27 @@ namespace Bit.iOS.Core.Controllers
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
// Users with key connector and without biometric or pin has no MP to unlock with
if (!_hasMasterPassword)
try
{
if (!(_pinEnabled || _biometricEnabled) ||
(_biometricEnabled && !_biometricIntegrityValid))
base.ViewDidAppear(animated);
// Users with key connector and without biometric or pin has no MP to unlock with
if (!_hasMasterPassword)
{
PromptSSO();
if (!(_pinEnabled || _biometricEnabled) ||
(_biometricEnabled && !_biometricIntegrityValid))
{
PromptSSO();
}
}
else if (!_biometricEnabled || !_biometricIntegrityValid)
{
MasterPasswordCell.TextField.BecomeFirstResponder();
}
}
else if (!_biometricEnabled || !_biometricIntegrityValid)
catch (Exception ex)
{
MasterPasswordCell.TextField.BecomeFirstResponder();
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
@@ -433,8 +441,9 @@ namespace Bit.iOS.Core.Controllers
public void PromptSSO()
{
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var loginPage = new LoginSsoPage(appOptions);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
@@ -444,7 +453,7 @@ namespace Bit.iOS.Core.Controllers
}
var navigationPage = new NavigationPage(loginPage);
var loginController = navigationPage.CreateViewController();
var loginController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(loginController, true, null);
}

View File

@@ -18,6 +18,7 @@ using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Platform;
namespace Bit.iOS.Core.Controllers
{
@@ -239,7 +240,7 @@ namespace Bit.iOS.Core.Controllers
ThemeManager.ApplyResourcesTo(generatorPage);
var navigationPage = new NavigationPage(generatorPage);
var generatorController = navigationPage.CreateViewController();
var generatorController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
generatorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(generatorController, true, null);
}

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.find-login-action-extension</string>
<key>CFBundleShortVersionString</key>
<string>2024.2.2</string>
<string>2024.2.1</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>

View File

@@ -18,6 +18,7 @@ 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;
@@ -528,7 +529,11 @@ namespace Bit.iOS.Extension
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessTask = async () =>
{
await DismissViewControllerAsync(false);
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
};
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
}
@@ -594,8 +599,9 @@ namespace Bit.iOS.Extension
private void LaunchLoginSsoFlow()
{
var loginPage = new LoginSsoPage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var loginPage = new LoginSsoPage(appOptions);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2024.2.2</string>
<string>2024.2.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>MinimumOSVersion</key>

View File

@@ -17,6 +17,7 @@ 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;
@@ -321,7 +322,11 @@ namespace Bit.iOS.ShareExtension
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
vm.SubmitSuccessTask = async () =>
{
await DismissViewControllerAsync(false);
await MainThread.InvokeOnMainThreadAsync(() => LaunchHomePage());
};
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
}
@@ -376,7 +381,7 @@ namespace Bit.iOS.ShareExtension
private void LaunchLoginSsoFlow()
{
var loginPage = new LoginSsoPage();
var loginPage = new LoginSsoPage(_appOptions.Value);
SetupAppAndApplyResources(loginPage);
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
{

View File

@@ -2,13 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Bit.Publisher</RootNamespace>
<Configurations>Debug;Release;FDroid</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.AndroidPublisher.v3" Version="1.52.0.2350" />
<PackageReference Include="Google.Apis.AndroidPublisher.v3" Version="1.66.0.3338" />
</ItemGroup>
</Project>