mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
13 Commits
vault/pm-2
...
v2024.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79da56d94e | ||
|
|
e918307a24 | ||
|
|
ecb0cbce14 | ||
|
|
5e549ff2a2 | ||
|
|
c60fc28289 | ||
|
|
ad308c97c9 | ||
|
|
7f92358d9b | ||
|
|
5338fdea79 | ||
|
|
36c32fbabd | ||
|
|
6bf7a492c6 | ||
|
|
6b54fadacc | ||
|
|
06fe3f78f8 | ||
|
|
250d40663b |
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
Binary file not shown.
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
Binary file not shown.
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
Binary file not shown.
Binary file not shown.
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Binary file not shown.
Binary file not shown.
3
.github/secrets/google-services.json.gpg
vendored
3
.github/secrets/google-services.json.gpg
vendored
@@ -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>
|
||||
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
Binary file not shown.
BIN
.github/secrets/play_creds.json.gpg
vendored
BIN
.github/secrets/play_creds.json.gpg
vendored
Binary file not shown.
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
Binary file not shown.
326
.github/workflows/build.yml
vendored
326
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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){ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
244
src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
Normal file
244
src/Core/Utilities/WebAuthenticatorMAUI/WebAuthenticator.ios.cs
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
126
src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
Normal file
126
src/Core/Utilities/WebAuthenticatorMAUI/WebUtils.cs
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user