mirror of
https://github.com/bitwarden/mobile
synced 2025-12-11 13:53:29 +00:00
Compare commits
18 Commits
bugfix/SG-
...
v2022.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b34f68794 | ||
|
|
c9eb3f7f2d | ||
|
|
eab32ef59e | ||
|
|
bb759c16ea | ||
|
|
b4c0fba5e9 | ||
|
|
5e4192b7db | ||
|
|
0187676b5e | ||
|
|
3b796c6599 | ||
|
|
eeb0f61986 | ||
|
|
13eff49372 | ||
|
|
47d3a8b345 | ||
|
|
76042a2ef7 | ||
|
|
0507fcd54a | ||
|
|
1a69cc0591 | ||
|
|
0866a78802 | ||
|
|
cfda5fd6ff | ||
|
|
526904d1d8 | ||
|
|
b9b9c2e5ff |
@@ -7,6 +7,12 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-format"
|
"dotnet-format"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"cake.tool": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-cake"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
.github/workflows/build.yml
vendored
73
.github/workflows/build.yml
vendored
@@ -59,6 +59,10 @@ jobs:
|
|||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
needs: setup
|
needs: setup
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
variant: ['prod', 'qa']
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
@@ -67,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
- name: Work Around for broken Windows 2022 Runner Image
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
run: |
|
run: |
|
||||||
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
@@ -87,7 +91,6 @@ jobs:
|
|||||||
Write-Host "components were not installed"
|
Write-Host "components were not installed"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -98,7 +101,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
@@ -109,12 +113,17 @@ jobs:
|
|||||||
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
|
||||||
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||||
shell: bash
|
shell: bash
|
||||||
|
- name: Decrypt secrets - Google Services
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
|
env:
|
||||||
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
|
--output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg
|
||||||
|
shell: bash
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||||
@@ -142,26 +151,35 @@ jobs:
|
|||||||
run: dotnet test test/Core.Test/Core.Test.csproj
|
run: dotnet test test/Core.Test/Core.Test.csproj
|
||||||
|
|
||||||
- name: Build Play Store publisher
|
- name: Build Play Store publisher
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
||||||
|
|
||||||
- name: Build for Play Store
|
- name: Setup Android build (${{ matrix.variant }})
|
||||||
|
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
|
||||||
|
|
||||||
|
- name: Build Android
|
||||||
run: |
|
run: |
|
||||||
$configuration = "Release";
|
$configuration = "Release";
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Build $configuration Configuration"
|
Write-Output "##### Build $configuration Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
||||||
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Sign for Play Store
|
- name: Sign Android Build
|
||||||
env:
|
env:
|
||||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
||||||
|
$packageName = "com.x8bit.bitwarden";
|
||||||
|
|
||||||
|
if ("${{ matrix.variant }}" -ne "prod")
|
||||||
|
{
|
||||||
|
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
|
||||||
|
}
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -175,9 +193,8 @@ jobs:
|
|||||||
Write-Output "##### Copy Google Play Bundle to project root"
|
Write-Output "##### Copy Google Play Bundle to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
|
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab");
|
||||||
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab");
|
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
|
||||||
|
|
||||||
Copy-Item $signedAabPath $signedAabDestPath
|
Copy-Item $signedAabPath $signedAabDestPath
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -193,33 +210,41 @@ jobs:
|
|||||||
Write-Output "##### Copy Release APK to project root"
|
Write-Output "##### Copy Release APK to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
|
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk");
|
||||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk");
|
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
- name: Upload Prod .aab artifact
|
||||||
- name: Upload Play Store .aab artifact
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Play Store .apk artifact
|
- name: Upload Prod .apk artifact
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Other .apk artifact
|
||||||
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||||
|
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Deploy to Play Store
|
- name: Deploy to Play Store
|
||||||
if: |
|
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
|
||||||
(github.ref == 'refs/heads/master'
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
&& needs.setup.outputs.hotfix_branch_exists == 0)
|
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|
||||||
run: |
|
run: |
|
||||||
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
||||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
CREDS_PATH="$HOME/secrets/play_creds.json"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -208,4 +208,5 @@ FakesAssemblies/
|
|||||||
# Other
|
# Other
|
||||||
project.lock.json
|
project.lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
src/App/Css
|
src/App/Css
|
||||||
|
tools
|
||||||
|
|||||||
345
build.cake
Normal file
345
build.cake
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
#addin nuget:?package=Cake.FileHelpers&version=5.0.0
|
||||||
|
#addin nuget:?package=Cake.AndroidAppManifest&version=1.1.2
|
||||||
|
#addin nuget:?package=Cake.Plist&version=0.7.0
|
||||||
|
#addin nuget:?package=Cake.Incubator&version=7.0.0
|
||||||
|
#tool dotnet:?package=GitVersion.Tool&version=5.10.3
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
var debugScript = Argument<bool>("debugScript", false);
|
||||||
|
var target = Argument("target", "Default");
|
||||||
|
var configuration = Argument("configuration", "Release");
|
||||||
|
var variant = Argument("variant", "dev");
|
||||||
|
|
||||||
|
abstract record VariantConfig(
|
||||||
|
string AppName,
|
||||||
|
string AndroidPackageName,
|
||||||
|
string iOSBundleId,
|
||||||
|
string ApsEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
|
||||||
|
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
|
||||||
|
|
||||||
|
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development");
|
||||||
|
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development");
|
||||||
|
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production");
|
||||||
|
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production");
|
||||||
|
|
||||||
|
VariantConfig GetVariant() => variant.ToLower() switch{
|
||||||
|
"qa" => new QA(),
|
||||||
|
"beta" => new Beta(),
|
||||||
|
"prod" => new Prod(),
|
||||||
|
_ => new Dev()
|
||||||
|
};
|
||||||
|
|
||||||
|
GitVersion _gitVersion; //will be set by GetGitInfo task
|
||||||
|
var _slnPath = Path.Combine(""); //base path used to access files. If build.cake file is moved, just update this
|
||||||
|
string _androidPackageName = string.Empty; //will be set by UpdateAndroidManifest task
|
||||||
|
string CreateFeatureBranch(string prevVersionName, GitVersion git) => $"{prevVersionName}-{git.BranchName.Replace("/","-")}";
|
||||||
|
string GetVersionName(string prevVersionName, VariantConfig buildVariant, GitVersion git) => buildVariant is Prod? prevVersionName : CreateFeatureBranch(prevVersionName, git);
|
||||||
|
int CreateBuildNumber(int previousNumber) => ++previousNumber;
|
||||||
|
|
||||||
|
Task("GetGitInfo")
|
||||||
|
.Does(()=> {
|
||||||
|
_gitVersion = GitVersion(new GitVersionSettings());
|
||||||
|
|
||||||
|
if(debugScript)
|
||||||
|
{
|
||||||
|
Information($"GitVersion Dump:\n{_gitVersion.Dump()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Information("Git data Load successfully.");
|
||||||
|
});
|
||||||
|
|
||||||
|
#region Android
|
||||||
|
Task("UpdateAndroidAppIcon")
|
||||||
|
.Does(()=>{
|
||||||
|
//TODO we'll implement variant icons later
|
||||||
|
//manifest.ApplicationIcon = "@mipmap/ic_launcher";
|
||||||
|
Information($"Updated Androix App Icon with success");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Task("UpdateAndroidManifest")
|
||||||
|
.IsDependentOn("GetGitInfo")
|
||||||
|
.Does(()=>
|
||||||
|
{
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var manifestPath = Path.Combine(_slnPath, "src", "Android", "Properties", "AndroidManifest.xml");
|
||||||
|
|
||||||
|
// Cake.AndroidAppManifest doesn't currently enable us to access nested items so, quick (not ideal) fix:
|
||||||
|
var manifestText = FileReadText(manifestPath);
|
||||||
|
manifestText = manifestText.Replace("com.x8bit.bitwarden.", buildVariant.AndroidPackageName + ".");
|
||||||
|
manifestText = manifestText.Replace("android:label=\"Bitwarden\"", $"android:label=\"{buildVariant.AppName}\"");
|
||||||
|
FileWriteText(manifestPath, manifestText);
|
||||||
|
|
||||||
|
var manifest = DeserializeAppManifest(manifestPath);
|
||||||
|
|
||||||
|
var prevVersionCode = manifest.VersionCode;
|
||||||
|
var prevVersionName = manifest.VersionName;
|
||||||
|
_androidPackageName = manifest.PackageName;
|
||||||
|
|
||||||
|
//manifest.VersionCode = CreateBuildNumber(prevVersionCode);
|
||||||
|
manifest.VersionName = GetVersionName(prevVersionName, buildVariant, _gitVersion);
|
||||||
|
manifest.PackageName = buildVariant.AndroidPackageName;
|
||||||
|
manifest.ApplicationLabel = buildVariant.AppName;
|
||||||
|
|
||||||
|
//Information($"AndroidManigest.xml VersionCode from {prevVersionCode} to {manifest.VersionCode}");
|
||||||
|
Information($"AndroidManigest.xml VersionName from {prevVersionName} to {manifest.VersionName}");
|
||||||
|
Information($"AndroidManigest.xml PackageName from {_androidPackageName} to {buildVariant.AndroidPackageName}");
|
||||||
|
Information($"AndroidManigest.xml ApplicationLabel to {buildVariant.AppName}");
|
||||||
|
|
||||||
|
SerializeAppManifest(manifestPath, manifest);
|
||||||
|
|
||||||
|
Information("AndroidManifest updated with success!");
|
||||||
|
});
|
||||||
|
|
||||||
|
void ReplaceInFile(string filePath, string oldtext, string newtext)
|
||||||
|
{
|
||||||
|
var fileText = FileReadText(filePath);
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(fileText) || !fileText.Contains(oldtext))
|
||||||
|
{
|
||||||
|
throw new Exception($"Couldn't find {filePath} or it didn't contain: {oldtext}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fileText = fileText.Replace(oldtext, newtext);
|
||||||
|
|
||||||
|
FileWriteText(filePath, fileText);
|
||||||
|
Information($"{filePath} modified successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Task("UpdateAndroidCodeFiles")
|
||||||
|
.IsDependentOn("UpdateAndroidManifest")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
|
||||||
|
//We're not using _androidPackageName here because the codefile is currently slightly different string than the one in AndroidManifest.xml
|
||||||
|
var keyName = "com.8bit.bitwarden";
|
||||||
|
var fixedPackageName = buildVariant.AndroidPackageName.Replace("x8bit", "8bit");
|
||||||
|
var filePath = Path.Combine(_slnPath, "src", "Android", "Services", "BiometricService.cs");
|
||||||
|
ReplaceInFile(filePath, keyName, fixedPackageName);
|
||||||
|
|
||||||
|
var packageFileList = new string[] {
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "MainActivity.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "MainApplication.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Constants.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Accessibility", "AccessibilityService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillHelpers.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Receivers", "ClearClipboardAlarmReceiver.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Receivers", "EventUploadReceiver.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Receivers", "PackageReplacedReceiver.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Receivers", "RestrictionsChangedReceiver.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Services", "DeviceActionService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Tiles", "AutofillTileService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Tiles", "GeneratorTileService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Tiles", "MyVaultTileService.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "google-services.json"),
|
||||||
|
Path.Combine(_slnPath, "store", "google", "Publisher", "Program.cs"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach(string path in packageFileList)
|
||||||
|
{
|
||||||
|
ReplaceInFile(path, "com.x8bit.bitwarden", buildVariant.AndroidPackageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelFileList = new string[] {
|
||||||
|
Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach(string path in labelFileList)
|
||||||
|
{
|
||||||
|
ReplaceInFile(path, "Bitwarden\"", $"{buildVariant.AppName}\"");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#endregion Android
|
||||||
|
|
||||||
|
#region iOS
|
||||||
|
enum iOSProjectType
|
||||||
|
{
|
||||||
|
Null,
|
||||||
|
MainApp,
|
||||||
|
Autofill,
|
||||||
|
Extension,
|
||||||
|
ShareExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetiOSBundleId(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
|
||||||
|
{
|
||||||
|
iOSProjectType.Autofill => $"{buildVariant.iOSBundleId}.autofill",
|
||||||
|
iOSProjectType.Extension => $"{buildVariant.iOSBundleId}.find-login-action-extension",
|
||||||
|
iOSProjectType.ShareExtension => $"{buildVariant.iOSBundleId}.share-extension",
|
||||||
|
_ => buildVariant.iOSBundleId
|
||||||
|
};
|
||||||
|
|
||||||
|
string GetiOSBundleName(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch
|
||||||
|
{
|
||||||
|
iOSProjectType.Autofill => $"{buildVariant.AppName} Autofill",
|
||||||
|
iOSProjectType.Extension => $"{buildVariant.AppName} Extension",
|
||||||
|
iOSProjectType.ShareExtension => $"{buildVariant.AppName} Share Extension",
|
||||||
|
_ => buildVariant.AppName
|
||||||
|
};
|
||||||
|
|
||||||
|
private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, GitVersion git, iOSProjectType projectType = iOSProjectType.MainApp)
|
||||||
|
{
|
||||||
|
var plistFile = File(plistPath);
|
||||||
|
dynamic plist = DeserializePlist(plistFile);
|
||||||
|
|
||||||
|
var prevVersionName = plist["CFBundleShortVersionString"];
|
||||||
|
var prevVersionString = plist["CFBundleVersion"];
|
||||||
|
var prevVersion = int.Parse(plist["CFBundleVersion"]);
|
||||||
|
var prevBundleId = plist["CFBundleIdentifier"];
|
||||||
|
var prevBundleName = plist["CFBundleName"];
|
||||||
|
//var newVersion = CreateBuildNumber(prevVersion).ToString();
|
||||||
|
var newVersionName = GetVersionName(prevVersionName, buildVariant, git);
|
||||||
|
var newBundleId = GetiOSBundleId(buildVariant, projectType);
|
||||||
|
var newBundleName = GetiOSBundleName(buildVariant, projectType);
|
||||||
|
|
||||||
|
plist["CFBundleName"] = newBundleName;
|
||||||
|
plist["CFBundleDisplayName"] = newBundleName;
|
||||||
|
//plist["CFBundleVersion"] = newVersion;
|
||||||
|
plist["CFBundleShortVersionString"] = newVersionName;
|
||||||
|
plist["CFBundleIdentifier"] = newBundleId;
|
||||||
|
|
||||||
|
if(projectType == iOSProjectType.MainApp)
|
||||||
|
{
|
||||||
|
plist["CFBundleURLTypes"][0]["CFBundleURLName"] = $"{buildVariant.iOSBundleId}.url";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(projectType == iOSProjectType.Extension)
|
||||||
|
{
|
||||||
|
var keyText = plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"];
|
||||||
|
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializePlist(plistFile, plist);
|
||||||
|
|
||||||
|
Information($"Changed app name from {prevBundleName} to {newBundleName}");
|
||||||
|
//Information($"Changed Bundle Version from {prevVersion} to {newVersion}");
|
||||||
|
Information($"Changed Bundle Short Version name from {prevVersionName} to {newVersionName}");
|
||||||
|
Information($"Changed Bundle Identifier from {prevBundleId} to {newBundleId}");
|
||||||
|
Information($"{plistPath} updated with success!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant)
|
||||||
|
{
|
||||||
|
var EntitlementlistFile = File(entitlementsPath);
|
||||||
|
dynamic Entitlements = DeserializePlist(EntitlementlistFile);
|
||||||
|
|
||||||
|
Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
|
||||||
|
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
|
||||||
|
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
|
||||||
|
|
||||||
|
Information($"Changed ApsEnvironment name to {buildVariant.ApsEnvironment}");
|
||||||
|
Information($"Changed keychain-access-groups bundleID to {buildVariant.iOSBundleId}");
|
||||||
|
|
||||||
|
SerializePlist(EntitlementlistFile, Entitlements);
|
||||||
|
|
||||||
|
Information($"{entitlementsPath} updated with success!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Task("UpdateiOSIcon")
|
||||||
|
.Does(()=>{
|
||||||
|
//TODO we'll implement variant icons later
|
||||||
|
Information($"Updating IOS App Icon");
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("UpdateiOSPlist")
|
||||||
|
.IsDependentOn("GetGitInfo")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var infoPath = Path.Combine(_slnPath, "src", "iOS", "Info.plist");
|
||||||
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS", "Entitlements.plist");
|
||||||
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
|
||||||
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("UpdateiOSAutofillPlist")
|
||||||
|
.IsDependentOn("GetGitInfo")
|
||||||
|
.IsDependentOn("UpdateiOSPlist")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
|
||||||
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
|
||||||
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
|
||||||
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("UpdateiOSExtensionPlist")
|
||||||
|
.IsDependentOn("GetGitInfo")
|
||||||
|
.IsDependentOn("UpdateiOSPlist")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
|
||||||
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
|
||||||
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
|
||||||
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("UpdateiOSShareExtensionPlist")
|
||||||
|
.IsDependentOn("GetGitInfo")
|
||||||
|
.IsDependentOn("UpdateiOSPlist")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
|
||||||
|
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
|
||||||
|
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
|
||||||
|
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant);
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("UpdateiOSCodeFiles")
|
||||||
|
.IsDependentOn("UpdateiOSPlist")
|
||||||
|
.Does(()=> {
|
||||||
|
var buildVariant = GetVariant();
|
||||||
|
var fileList = new string[] {
|
||||||
|
Path.Combine(_slnPath, "src", "iOS.Core", "Utilities", "iOSCoreHelpers.cs"),
|
||||||
|
Path.Combine(_slnPath, "src", "iOS.Core", "Constants.cs"),
|
||||||
|
Path.Combine(".github", "resources", "export-options-ad-hoc.plist"),
|
||||||
|
Path.Combine(".github", "resources", "export-options-app-store.plist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach(string path in fileList)
|
||||||
|
{
|
||||||
|
ReplaceInFile(path, "com.8bit.bitwarden", buildVariant.iOSBundleId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#endregion iOS
|
||||||
|
|
||||||
|
#region Main Tasks
|
||||||
|
Task("Android")
|
||||||
|
//.IsDependentOn("UpdateAndroidAppIcon")
|
||||||
|
.IsDependentOn("UpdateAndroidManifest")
|
||||||
|
.IsDependentOn("UpdateAndroidCodeFiles")
|
||||||
|
.Does(()=>
|
||||||
|
{
|
||||||
|
Information("Android app updated");
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("iOS")
|
||||||
|
//.IsDependentOn("UpdateiOSIcon")
|
||||||
|
.IsDependentOn("UpdateiOSPlist")
|
||||||
|
.IsDependentOn("UpdateiOSAutofillPlist")
|
||||||
|
.IsDependentOn("UpdateiOSExtensionPlist")
|
||||||
|
.IsDependentOn("UpdateiOSShareExtensionPlist")
|
||||||
|
.IsDependentOn("UpdateiOSCodeFiles")
|
||||||
|
.Does(()=>
|
||||||
|
{
|
||||||
|
Information("iOS app updated");
|
||||||
|
});
|
||||||
|
|
||||||
|
Task("Default")
|
||||||
|
.Does(() => {
|
||||||
|
var usage = @"Missing target.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
dotnet cake build.cake --target (Android | iOS) --variant (dev | qa | beta | prod)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--debugScript=<bool> Script debug mode.
|
||||||
|
";
|
||||||
|
Information(usage);
|
||||||
|
});
|
||||||
|
#endregion Main Tasks
|
||||||
|
|
||||||
|
RunTarget(target);
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
<Compile Include="Services\FileService.cs" />
|
<Compile Include="Services\FileService.cs" />
|
||||||
<Compile Include="Services\AutofillHandler.cs" />
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
<Compile Include="Constants.cs" />
|
<Compile Include="Constants.cs" />
|
||||||
|
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
|
|||||||
Binary file not shown.
23
src/Android/Effects/RemoveFontPaddingEffect.cs
Normal file
23
src/Android/Effects/RemoveFontPaddingEffect.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Android.Widget;
|
||||||
|
using Bit.Droid.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
|
[assembly: ExportEffect(typeof(RemoveFontPaddingEffect), nameof(RemoveFontPaddingEffect))]
|
||||||
|
namespace Bit.Droid.Effects
|
||||||
|
{
|
||||||
|
public class RemoveFontPaddingEffect : PlatformEffect
|
||||||
|
{
|
||||||
|
protected override void OnAttached()
|
||||||
|
{
|
||||||
|
if (Control is TextView textView)
|
||||||
|
{
|
||||||
|
textView.SetIncludeFontPadding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetached()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.10.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.11.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ namespace Bit.App.Abstractions
|
|||||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||||
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||||
|
Task PromptToSwitchToExistingAccountAsync(string userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
||||||
<Folder Include="Utilities\AccountManagement\" />
|
<Folder Include="Utilities\AccountManagement\" />
|
||||||
<Folder Include="Controls\DateTime\" />
|
<Folder Include="Controls\DateTime\" />
|
||||||
|
<Folder Include="Controls\IconLabelButton\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -430,5 +431,6 @@
|
|||||||
<None Remove="Controls\AccountSwitchingOverlay\" />
|
<None Remove="Controls\AccountSwitchingOverlay\" />
|
||||||
<None Remove="Utilities\AccountManagement\" />
|
<None Remove="Utilities\AccountManagement\" />
|
||||||
<None Remove="Controls\DateTime\" />
|
<None Remove="Controls\DateTime\" />
|
||||||
|
<None Remove="Controls\IconLabelButton\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
@@ -33,8 +34,9 @@ namespace Bit.App
|
|||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
|
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
private static bool _pendingCheckPasswordlessLoginRequests;
|
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||||
|
private static object _processingLoginRequestLock = new object();
|
||||||
|
|
||||||
public App(AppOptions appOptions)
|
public App(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
@@ -143,9 +145,15 @@ namespace Bit.App
|
|||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||||
|
|| message.Command == "unlocked"
|
||||||
|
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||||
{
|
{
|
||||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
lock (_processingLoginRequestLock)
|
||||||
|
{
|
||||||
|
// lock doesn't allow for async execution
|
||||||
|
CheckPasswordlessLoginRequestsAsync().Wait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -162,7 +170,6 @@ namespace Bit.App
|
|||||||
_pendingCheckPasswordlessLoginRequests = true;
|
_pendingCheckPasswordlessLoginRequests = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingCheckPasswordlessLoginRequests = false;
|
_pendingCheckPasswordlessLoginRequests = false;
|
||||||
if (await _vaultTimeoutService.IsLockedAsync())
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
{
|
{
|
||||||
@@ -182,6 +189,11 @@ namespace Bit.App
|
|||||||
|
|
||||||
// Delay to wait for the vault page to appear
|
// Delay to wait for the vault page to appear
|
||||||
await Task.Delay(2000);
|
await Task.Delay(2000);
|
||||||
|
// if there is a request modal opened ignore all incoming requests
|
||||||
|
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
|
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
|
||||||
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
||||||
{
|
{
|
||||||
@@ -196,7 +208,7 @@ namespace Bit.App
|
|||||||
});
|
});
|
||||||
await _stateService.SetPasswordlessLoginNotificationAsync(null);
|
await _stateService.SetPasswordlessLoginNotificationAsync(null);
|
||||||
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
|
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
|
||||||
if (loginRequestData.CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
|
if (!loginRequestData.IsExpired)
|
||||||
{
|
{
|
||||||
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||||
}
|
}
|
||||||
@@ -211,13 +223,20 @@ namespace Bit.App
|
|||||||
}
|
}
|
||||||
|
|
||||||
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
|
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
|
||||||
await Device.InvokeOnMainThreadAsync(async () =>
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
{
|
{
|
||||||
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
|
try
|
||||||
if (result == AppResources.Ok)
|
|
||||||
{
|
{
|
||||||
await _stateService.SetActiveUserAsync(notification.UserId);
|
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
|
||||||
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
if (result == AppResources.Ok)
|
||||||
|
{
|
||||||
|
await _stateService.SetActiveUserAsync(notification.UserId);
|
||||||
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -242,7 +261,7 @@ namespace Bit.App
|
|||||||
}
|
}
|
||||||
if (_pendingCheckPasswordlessLoginRequests)
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
{
|
{
|
||||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
||||||
}
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
@@ -278,7 +297,7 @@ namespace Bit.App
|
|||||||
_isResumed = true;
|
_isResumed = true;
|
||||||
if (_pendingCheckPasswordlessLoginRequests)
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
{
|
{
|
||||||
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
||||||
}
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
@@ -440,7 +459,14 @@ namespace Bit.App
|
|||||||
switch (navTarget)
|
switch (navTarget)
|
||||||
{
|
{
|
||||||
case NavigationTarget.HomeLogin:
|
case NavigationTarget.HomeLogin:
|
||||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
if (navParams is HomeNavigationParams homeParams)
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new HomePage(Options, homeParams.ShouldCheckRememberEmail));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.Login:
|
case NavigationTarget.Login:
|
||||||
if (navParams is LoginNavigationParams loginParams)
|
if (navParams is LoginNavigationParams loginParams)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Xamarin.Forms;
|
using Bit.App.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,8 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Effects.Add(new RemoveFontPaddingEffect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Xamarin.Forms;
|
using Bit.App.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,8 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Effects.Add(new RemoveFontPaddingEffect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/App/Controls/IconLabelButton/IconLabelButton.xaml
Normal file
40
src/App/Controls/IconLabelButton/IconLabelButton.xaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Controls.IconLabelButton"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Name="_iconLabelButton"
|
||||||
|
HeightRequest="45"
|
||||||
|
Padding="1"
|
||||||
|
StyleClass="btn-icon-secondary"
|
||||||
|
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
HasShadow="False">
|
||||||
|
<Frame.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
|
||||||
|
</Frame.GestureRecognizers>
|
||||||
|
<Frame
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
IsClippedToBounds="True"
|
||||||
|
HasShadow="False">
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalOptions="Center">
|
||||||
|
<controls:IconLabel
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
FontSize="Large"
|
||||||
|
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
|
||||||
|
</controls:IconLabel>
|
||||||
|
<Label
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
FontSize="Medium"
|
||||||
|
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
|
||||||
|
</StackLayout>
|
||||||
|
</Frame>
|
||||||
|
</Frame>
|
||||||
75
src/App/Controls/IconLabelButton/IconLabelButton.xaml.cs
Normal file
75
src/App/Controls/IconLabelButton/IconLabelButton.xaml.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Xaml;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class IconLabelButton : Frame
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty IconProperty = BindableProperty.Create(
|
||||||
|
nameof(Icon), typeof(string), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
|
||||||
|
nameof(Label), typeof(string), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(ButtonCommand), typeof(Command), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public IconLabelButton()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Icon
|
||||||
|
{
|
||||||
|
get => GetValue(IconProperty) as string;
|
||||||
|
set => SetValue(IconProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label
|
||||||
|
{
|
||||||
|
get => GetValue(LabelProperty) as string;
|
||||||
|
set => SetValue(LabelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ButtonCommand
|
||||||
|
{
|
||||||
|
get => GetValue(ButtonCommandProperty) as ICommand;
|
||||||
|
set => SetValue(ButtonCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelColorProperty); }
|
||||||
|
set { SetValue(IconLabelColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelBackgroundColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
|
||||||
|
set { SetValue(IconLabelBackgroundColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelBorderColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelBorderColorProperty); }
|
||||||
|
set { SetValue(IconLabelBorderColorProperty, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
13
src/App/Effects/RemoveFontPaddingEffect.cs
Normal file
13
src/App/Effects/RemoveFontPaddingEffect.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Effects
|
||||||
|
{
|
||||||
|
public class RemoveFontPaddingEffect : RoutingEffect
|
||||||
|
{
|
||||||
|
public RemoveFontPaddingEffect()
|
||||||
|
: base("Bitwarden.RemoveFontPaddingEffect")
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,11 +6,12 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
private HintPageViewModel _vm;
|
private HintPageViewModel _vm;
|
||||||
|
|
||||||
public HintPage()
|
public HintPage(string email = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HintPageViewModel;
|
_vm = BindingContext as HintPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
|
_vm.Email = email;
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ToolbarItems.RemoveAt(0);
|
ToolbarItems.RemoveAt(0);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private string _email;
|
||||||
|
|
||||||
public HintPageViewModel()
|
public HintPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -34,7 +35,12 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ICommand SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public string Email { get; set; }
|
|
||||||
|
public string Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => SetProperty(ref _email, value);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}" />
|
||||||
|
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
|
||||||
<ToolbarItem
|
<ToolbarItem
|
||||||
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
@@ -32,30 +33,66 @@
|
|||||||
|
|
||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="0" Padding="10, 5">
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
|
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
|
||||||
<Image
|
<Image
|
||||||
x:Name="_logo"
|
x:Name="_logo"
|
||||||
Source="logo.png"
|
Source="logo.png"
|
||||||
VerticalOptions="Center" />
|
VerticalOptions="Center" />
|
||||||
<Label Text="{u:I18n LoginOrCreateNewAccount}"
|
<Label Text="{u:I18n LoginOrCreateNewAccount}"
|
||||||
StyleClass="text-lg"
|
StyleClass="text-lg"
|
||||||
HorizontalTextAlignment="Center">
|
HorizontalTextAlignment="Center"/>
|
||||||
</Label>
|
<StackLayout
|
||||||
<StackLayout Spacing="5">
|
StyleClass="box-row">
|
||||||
<Button Text="{u:I18n LogIn}"
|
<Label
|
||||||
StyleClass="btn-primary"
|
Text="{u:I18n EmailAddress}"
|
||||||
Clicked="LogIn_Clicked" />
|
StyleClass="box-label" />
|
||||||
<Button Text="{u:I18n CreateAccount}"
|
<Entry
|
||||||
Clicked="Register_Clicked" />
|
x:Name="_email"
|
||||||
<Button Text="{u:I18n LogInSso}"
|
Text="{Binding Email}"
|
||||||
Clicked="LogInSso_Clicked" />
|
Keyboard="Email"
|
||||||
<Button Text="{u:I18n Cancel}"
|
StyleClass="box-value">
|
||||||
IsVisible="{Binding ShowCancelButton}"
|
<VisualStateManager.VisualStateGroups>
|
||||||
Margin="0,10,0,0"
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
Clicked="Cancel_Clicked" />
|
<VisualState x:Name="Disabled">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Entry>
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Margin="0, 16, 0 ,0">
|
||||||
|
<StackLayout.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer
|
||||||
|
Command="{Binding RememberEmailCommand}" />
|
||||||
|
</StackLayout.GestureRecognizers>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n RememberMe}"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
VerticalOptions="Center"
|
||||||
|
VerticalTextAlignment="Center"/>
|
||||||
|
<Switch
|
||||||
|
Scale="0.8"
|
||||||
|
IsToggled="{Binding RememberEmail}"
|
||||||
|
VerticalOptions="Center"/>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
<Button Text="{u:I18n Continue}"
|
||||||
|
StyleClass="btn-primary"
|
||||||
|
IsEnabled="{Binding CanContinue}"
|
||||||
|
Command="{Binding ContinueCommand}" />
|
||||||
|
|
||||||
|
<Label FormattedText="{Binding CreateAccountText}"
|
||||||
|
Margin="0, 10"
|
||||||
|
StyleClass="box-footer-label">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding CreateAccountCommand}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ContentPage.Resources>
|
</ContentPage.Resources>
|
||||||
|
|||||||
@@ -10,28 +10,35 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
public partial class HomePage : BaseContentPage
|
public partial class HomePage : BaseContentPage
|
||||||
{
|
{
|
||||||
|
private bool _checkRememberedEmail;
|
||||||
private readonly HomeViewModel _vm;
|
private readonly HomeViewModel _vm;
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
public HomePage(AppOptions appOptions = null)
|
public HomePage(AppOptions appOptions = null, bool shouldCheckRememberEmail = true)
|
||||||
{
|
{
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HomeViewModel;
|
_vm = BindingContext as HomeViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
|
_vm.ShouldCheckRememberEmail = shouldCheckRememberEmail;
|
||||||
|
_vm.ShowCancelButton = _appOptions?.IosExtension ?? false;
|
||||||
|
_vm.StartLoginAction = async () => await StartLoginAsync();
|
||||||
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
||||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||||
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
||||||
|
_vm.CloseAction = async () =>
|
||||||
|
{
|
||||||
|
await _accountListOverlay.HideAsync();
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
};
|
||||||
UpdateLogo();
|
UpdateLogo();
|
||||||
|
|
||||||
if (_appOptions?.IosExtension ?? false)
|
if (!_vm.ShowCancelButton)
|
||||||
{
|
{
|
||||||
_vm.ShowCancelButton = true;
|
ToolbarItems.Remove(_closeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_appOptions?.HideAccountSwitcher ?? false)
|
if (_appOptions?.HideAccountSwitcher ?? false)
|
||||||
{
|
{
|
||||||
ToolbarItems.Remove(_accountAvatar);
|
ToolbarItems.Remove(_accountAvatar);
|
||||||
@@ -52,7 +59,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
if (!_appOptions?.HideAccountSwitcher ?? false)
|
if (!_appOptions?.HideAccountSwitcher ?? false)
|
||||||
{
|
{
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(false);
|
||||||
}
|
}
|
||||||
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||||
{
|
{
|
||||||
@@ -64,6 +71,8 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_vm.CheckNavigateLoginStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
@@ -96,28 +105,12 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogIn_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
_vm.StartLoginAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartLoginAsync()
|
private async Task StartLoginAsync()
|
||||||
{
|
{
|
||||||
var page = new LoginPage(null, _appOptions);
|
var page = new LoginPage(_vm.Email, _appOptions);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Register_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
_vm.StartRegisterAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartRegisterAsync()
|
private async Task StartRegisterAsync()
|
||||||
{
|
{
|
||||||
var page = new RegisterPage(this);
|
var page = new RegisterPage(this);
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -12,19 +19,37 @@ namespace Bit.App.Pages
|
|||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
|
private bool _rememberEmail;
|
||||||
|
private string _email;
|
||||||
|
private bool _isEmailEnabled;
|
||||||
|
private bool _canLogin;
|
||||||
|
private IPlatformUtilsService _platformUtilsService;
|
||||||
|
private ILogger _logger;
|
||||||
|
private IEnvironmentService _environmentService;
|
||||||
|
private IAccountsManager _accountManager;
|
||||||
|
|
||||||
public HomeViewModel()
|
public HomeViewModel()
|
||||||
{
|
{
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||||
var logger = ServiceContainer.Resolve<ILogger>("logger");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
|
||||||
|
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
{
|
{
|
||||||
AllowActiveAccountSelection = true
|
AllowActiveAccountSelection = true
|
||||||
};
|
};
|
||||||
|
RememberEmailCommand = new Command(() => RememberEmail = !RememberEmail);
|
||||||
|
ContinueCommand = new AsyncCommand(ContinueToLoginStepAsync, allowsMultipleExecutions: false);
|
||||||
|
CreateAccountCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(StartRegisterAction),
|
||||||
|
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
CloseCommand = new AsyncCommand(async () => await Device.InvokeOnMainThreadAsync(CloseAction),
|
||||||
|
onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
InitAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShowCancelButton
|
public bool ShowCancelButton
|
||||||
@@ -33,11 +58,102 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _showCancelButton, value);
|
set => SetProperty(ref _showCancelButton, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RememberEmail
|
||||||
|
{
|
||||||
|
get => _rememberEmail;
|
||||||
|
set => SetProperty(ref _rememberEmail, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => SetProperty(ref _email, value,
|
||||||
|
additionalPropertyNames: new[] { nameof(CanContinue) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanContinue => !string.IsNullOrEmpty(Email);
|
||||||
|
|
||||||
|
public bool ShouldCheckRememberEmail { get; set; }
|
||||||
|
|
||||||
|
public FormattedString CreateAccountText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var fs = new FormattedString();
|
||||||
|
fs.Spans.Add(new Span
|
||||||
|
{
|
||||||
|
Text = $"{AppResources.NewAroundHere} "
|
||||||
|
});
|
||||||
|
fs.Spans.Add(new Span
|
||||||
|
{
|
||||||
|
Text = AppResources.CreateAccount,
|
||||||
|
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
|
||||||
|
});
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
public Action StartLoginAction { get; set; }
|
public Action StartLoginAction { get; set; }
|
||||||
public Action StartRegisterAction { get; set; }
|
public Action StartRegisterAction { get; set; }
|
||||||
public Action StartSsoLoginAction { get; set; }
|
public Action StartSsoLoginAction { get; set; }
|
||||||
public Action StartEnvironmentAction { get; set; }
|
public Action StartEnvironmentAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
public Command RememberEmailCommand { get; set; }
|
||||||
|
public AsyncCommand ContinueCommand { get; }
|
||||||
|
public AsyncCommand CloseCommand { get; }
|
||||||
|
public AsyncCommand CreateAccountCommand { get; }
|
||||||
|
|
||||||
|
public async Task InitAsync()
|
||||||
|
{
|
||||||
|
Email = await _stateService.GetRememberedEmailAsync();
|
||||||
|
RememberEmail = !string.IsNullOrEmpty(Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckNavigateLoginStep()
|
||||||
|
{
|
||||||
|
if (ShouldCheckRememberEmail && RememberEmail)
|
||||||
|
{
|
||||||
|
StartLoginAction();
|
||||||
|
}
|
||||||
|
ShouldCheckRememberEmail = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ContinueToLoginStepAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(
|
||||||
|
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||||
|
AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Email.Contains("@"))
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.InvalidEmail, AppResources.AnErrorHasOccurred,
|
||||||
|
AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _stateService.SetRememberedEmailAsync(RememberEmail ? Email : null);
|
||||||
|
var userId = await _stateService.GetUserIdAsync(Email);
|
||||||
|
if (!string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
var userEnvUrls = await _stateService.GetEnvironmentUrlsAsync(userId);
|
||||||
|
if (userEnvUrls?.Base == _environmentService.BaseUrl)
|
||||||
|
{
|
||||||
|
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StartLoginAction();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred, AppResources.Ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.LoginPage"
|
x:Class="Bit.App.Pages.LoginPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
x:DataType="pages:LoginPageViewModel"
|
x:DataType="pages:LoginPageViewModel"
|
||||||
@@ -46,33 +47,13 @@
|
|||||||
Order="Secondary" />
|
Order="Secondary" />
|
||||||
|
|
||||||
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
|
||||||
<StackLayout Spacing="20">
|
<StackLayout Spacing="0">
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
<StackLayout StyleClass="box-row">
|
|
||||||
<Label
|
|
||||||
Text="{u:I18n EmailAddress}"
|
|
||||||
StyleClass="box-label" />
|
|
||||||
<Entry
|
|
||||||
x:Name="_email"
|
|
||||||
Text="{Binding Email}"
|
|
||||||
IsEnabled="{Binding IsEmailEnabled}"
|
|
||||||
Keyboard="Email"
|
|
||||||
StyleClass="box-value">
|
|
||||||
<VisualStateManager.VisualStateGroups>
|
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
|
||||||
<VisualState x:Name="Disabled">
|
|
||||||
<VisualState.Setters>
|
|
||||||
<Setter Property="TextColor" Value="{DynamicResource MutedColor}" />
|
|
||||||
</VisualState.Setters>
|
|
||||||
</VisualState>
|
|
||||||
</VisualStateGroup>
|
|
||||||
</VisualStateManager.VisualStateGroups>
|
|
||||||
</Entry>
|
|
||||||
</StackLayout>
|
|
||||||
<Grid StyleClass="box-row">
|
<Grid StyleClass="box-row">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -81,6 +62,7 @@
|
|||||||
<Label
|
<Label
|
||||||
Text="{u:I18n MasterPassword}"
|
Text="{u:I18n MasterPassword}"
|
||||||
StyleClass="box-label"
|
StyleClass="box-label"
|
||||||
|
Padding="0, 10, 0, 0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0" />
|
Grid.Column="0" />
|
||||||
<controls:MonoEntry
|
<controls:MonoEntry
|
||||||
@@ -98,21 +80,60 @@
|
|||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding ShowPasswordIcon}"
|
Text="{Binding ShowPasswordIcon}"
|
||||||
Command="{Binding TogglePasswordCommand}"
|
Command="{Binding TogglePasswordCommand}"
|
||||||
Grid.Row="0"
|
Grid.Row="1"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="1"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n GetMasterPasswordwordHint}"
|
||||||
|
StyleClass="box-footer-label"
|
||||||
|
TextColor="{DynamicResource HyperlinkColor}"
|
||||||
|
Padding="0,5,0,0"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="2">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="Hint_Clicked" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
<StackLayout Padding="10, 0">
|
<StackLayout Padding="10, 10">
|
||||||
<Button Text="{u:I18n LogIn}"
|
<Button x:Name="_loginWithMasterPassword"
|
||||||
|
Text="{u:I18n LogInWithMasterPassword}"
|
||||||
StyleClass="btn-primary"
|
StyleClass="btn-primary"
|
||||||
Clicked="LogIn_Clicked" />
|
Clicked="LogIn_Clicked" />
|
||||||
<Button Text="{u:I18n Cancel}"
|
<controls:IconLabelButton
|
||||||
IsVisible="{Binding ShowCancelButton}"
|
HorizontalOptions="Fill"
|
||||||
Clicked="Cancel_Clicked" />
|
VerticalOptions="CenterAndExpand"
|
||||||
|
Icon="{Binding Source={x:Static core:BitwardenIcons.Device}}"
|
||||||
|
Label="{u:I18n LogInWithAnotherDevice}"
|
||||||
|
ButtonCommand="{Binding LogInCommand}"
|
||||||
|
IsVisible="False"/>
|
||||||
|
<controls:IconLabelButton
|
||||||
|
HorizontalOptions="Fill"
|
||||||
|
VerticalOptions="CenterAndExpand"
|
||||||
|
Icon="{Binding Source={x:Static core:BitwardenIcons.Suitcase}}"
|
||||||
|
Label="{u:I18n LogInSso}">
|
||||||
|
<controls:IconLabelButton.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="LogInSSO_Clicked" />
|
||||||
|
</controls:IconLabelButton.GestureRecognizers>
|
||||||
|
</controls:IconLabelButton>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoggingInAsText}"
|
||||||
|
StyleClass="text-sm"
|
||||||
|
Margin="0,40,0,0"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n NotYou}"
|
||||||
|
StyleClass="text-md"
|
||||||
|
HorizontalOptions="Start"
|
||||||
|
TextColor="{DynamicResource HyperlinkColor}">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="Cancel_Clicked" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ using System.Threading.Tasks;
|
|||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -25,6 +27,7 @@ namespace Bit.App.Pages
|
|||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||||
|
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||||
_vm.UpdateTempPasswordAction =
|
_vm.UpdateTempPasswordAction =
|
||||||
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
|
||||||
_vm.CloseAction = async () =>
|
_vm.CloseAction = async () =>
|
||||||
@@ -42,9 +45,6 @@ namespace Bit.App.Pages
|
|||||||
_vm.Email = email;
|
_vm.Email = email;
|
||||||
MasterPasswordEntry = _masterPassword;
|
MasterPasswordEntry = _masterPassword;
|
||||||
|
|
||||||
_email.ReturnType = ReturnType.Next;
|
|
||||||
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
|
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_moreItem);
|
ToolbarItems.Add(_moreItem);
|
||||||
@@ -54,11 +54,6 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_getPasswordHint);
|
ToolbarItems.Add(_getPasswordHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
|
|
||||||
{
|
|
||||||
ToolbarItems.Add(_removeAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_appOptions?.IosExtension ?? false)
|
if (_appOptions?.IosExtension ?? false)
|
||||||
{
|
{
|
||||||
_vm.ShowCancelButton = true;
|
_vm.ShowCancelButton = true;
|
||||||
@@ -78,16 +73,20 @@ namespace Bit.App.Pages
|
|||||||
_mainContent.Content = _mainLayout;
|
_mainContent.Content = _mainLayout;
|
||||||
_accountAvatar?.OnAppearing();
|
_accountAvatar?.OnAppearing();
|
||||||
|
|
||||||
|
await _vm.InitAsync();
|
||||||
if (!_appOptions?.HideAccountSwitcher ?? false)
|
if (!_appOptions?.HideAccountSwitcher ?? false)
|
||||||
{
|
{
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(_vm.EmailIsInSavedAccounts);
|
||||||
}
|
}
|
||||||
await _vm.InitAsync();
|
|
||||||
if (!_inputFocused)
|
if (!_inputFocused)
|
||||||
{
|
{
|
||||||
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
|
RequestFocus(_masterPassword);
|
||||||
_inputFocused = true;
|
_inputFocused = true;
|
||||||
}
|
}
|
||||||
|
if (Device.RuntimePlatform == Device.Android && !_vm.CanRemoveAccount)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_removeAccount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
@@ -115,11 +114,25 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogInSSO_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
_vm.StartSsoLoginAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartSsoLoginAsync()
|
||||||
|
{
|
||||||
|
var page = new LoginSsoPage(_appOptions);
|
||||||
|
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
private void Hint_Clicked(object sender, EventArgs e)
|
private void Hint_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
_vm.ShowMasterPasswordHintAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.App.Utilities.AccountManagement;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -25,12 +30,15 @@ namespace Bit.App.Pages
|
|||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IApiService _apiService;
|
||||||
|
private readonly IAppIdService _appIdService;
|
||||||
|
private readonly IAccountsManager _accountManager;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
private bool _isEmailEnabled;
|
private bool _isEmailEnabled;
|
||||||
|
private bool _isKnownDevice;
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -43,6 +51,9 @@ namespace Bit.App.Pages
|
|||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
_apiService = ServiceContainer.Resolve<IApiService>();
|
||||||
|
_appIdService = ServiceContainer.Resolve<IAppIdService>();
|
||||||
|
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||||
|
|
||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
@@ -76,7 +87,11 @@ namespace Bit.App.Pages
|
|||||||
public string Email
|
public string Email
|
||||||
{
|
{
|
||||||
get => _email;
|
get => _email;
|
||||||
set => SetProperty(ref _email, value);
|
set => SetProperty(ref _email, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(LoggingInAsText)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MasterPassword
|
public string MasterPassword
|
||||||
@@ -91,20 +106,29 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _isEmailEnabled, value);
|
set => SetProperty(ref _isEmailEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsIosExtension { get; set; }
|
public bool IsKnownDevice
|
||||||
|
{
|
||||||
|
get => _isKnownDevice;
|
||||||
|
set => SetProperty(ref _isKnownDevice, value);
|
||||||
|
}
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
public ICommand MoreCommand { get; internal set; }
|
public ICommand MoreCommand { get; internal set; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
|
public string LoggingInAsText => string.Format(AppResources.LoggingInAsX, Email);
|
||||||
|
public bool IsIosExtension { get; set; }
|
||||||
|
public bool CanRemoveAccount { get; set; }
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
public Action LogInSuccessAction { get; set; }
|
public Action LogInSuccessAction { get; set; }
|
||||||
public Action UpdateTempPasswordAction { get; set; }
|
public Action UpdateTempPasswordAction { get; set; }
|
||||||
|
public Action StartSsoLoginAction { get; set; }
|
||||||
public Action CloseAction { get; set; }
|
public Action CloseAction { get; set; }
|
||||||
|
|
||||||
|
public bool EmailIsInSavedAccounts => _stateService.AccountViews != null && _stateService.AccountViews.Any(e => e.Email == Email);
|
||||||
|
|
||||||
protected override II18nService i18nService => _i18nService;
|
protected override II18nService i18nService => _i18nService;
|
||||||
protected override IEnvironmentService environmentService => _environmentService;
|
protected override IEnvironmentService environmentService => _environmentService;
|
||||||
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
protected override IDeviceActionService deviceActionService => _deviceActionService;
|
||||||
@@ -112,9 +136,22 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
try
|
||||||
{
|
{
|
||||||
Email = await _stateService.GetRememberedEmailAsync();
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
|
{
|
||||||
|
Email = await _stateService.GetRememberedEmailAsync();
|
||||||
|
}
|
||||||
|
var deviceIdentifier = await _appIdService.GetAppIdAsync();
|
||||||
|
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, deviceIdentifier);
|
||||||
|
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +195,7 @@ namespace Bit.App.Pages
|
|||||||
var userEnvUrls = await _stateService.GetEnvironmentUrlsAsync(userId);
|
var userEnvUrls = await _stateService.GetEnvironmentUrlsAsync(userId);
|
||||||
if (userEnvUrls?.Base == _environmentService.BaseUrl)
|
if (userEnvUrls?.Base == _environmentService.BaseUrl)
|
||||||
{
|
{
|
||||||
await PromptToSwitchToExistingAccountAsync(userId);
|
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +207,6 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
|
||||||
await _stateService.SetRememberedEmailAsync(Email);
|
|
||||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||||
|
|
||||||
if (response.CaptchaNeeded)
|
if (response.CaptchaNeeded)
|
||||||
@@ -216,19 +252,14 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task MoreAsync()
|
private async Task MoreAsync()
|
||||||
{
|
{
|
||||||
var buttons = IsEmailEnabled
|
var buttons = IsEmailEnabled || CanRemoveAccount
|
||||||
? new[] { AppResources.GetPasswordHint }
|
? new[] { AppResources.GetPasswordHint }
|
||||||
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
||||||
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
||||||
|
|
||||||
if (selection == AppResources.GetPasswordHint)
|
if (selection == AppResources.GetPasswordHint)
|
||||||
{
|
{
|
||||||
var hintNavigationPage = new NavigationPage(new HintPage());
|
await ShowMasterPasswordHintAsync();
|
||||||
if (IsIosExtension)
|
|
||||||
{
|
|
||||||
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
|
||||||
}
|
|
||||||
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
|
||||||
}
|
}
|
||||||
else if (selection == AppResources.RemoveAccount)
|
else if (selection == AppResources.RemoveAccount)
|
||||||
{
|
{
|
||||||
@@ -236,6 +267,16 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ShowMasterPasswordHintAsync()
|
||||||
|
{
|
||||||
|
var hintNavigationPage = new NavigationPage(new HintPage(Email));
|
||||||
|
if (IsIosExtension)
|
||||||
|
{
|
||||||
|
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
||||||
|
}
|
||||||
|
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
||||||
|
}
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
@@ -261,16 +302,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PromptToSwitchToExistingAccountAsync(string userId)
|
private void HandleException(Exception ex)
|
||||||
{
|
{
|
||||||
var switchToAccount = await _platformUtilsService.ShowDialogAsync(
|
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
AppResources.SwitchToAlreadyAddedAccountConfirmation,
|
|
||||||
AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
|
|
||||||
if (switchToAccount)
|
|
||||||
{
|
{
|
||||||
await _stateService.SetActiveUserAsync(userId);
|
await _deviceActionService.HideLoadingAsync();
|
||||||
_messagingService.Send("switchedAccount");
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||||
}
|
}).FireAndForget();
|
||||||
|
_logger.Exception(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
_vm.LoginRequest = loginPasswordlessDetails;
|
_vm.LoginRequest = loginPasswordlessDetails;
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
ToolbarItems.Add(_closeItem);
|
||||||
{
|
|
||||||
ToolbarItems.Add(_closeItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ namespace Bit.App.Pages
|
|||||||
private async Task UpdateRequestTime()
|
private async Task UpdateRequestTime()
|
||||||
{
|
{
|
||||||
TriggerPropertyChanged(nameof(TimeOfRequestText));
|
TriggerPropertyChanged(nameof(TimeOfRequestText));
|
||||||
if (DateTime.UtcNow > LoginRequest?.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
|
if (LoginRequest?.IsExpired ?? false)
|
||||||
{
|
{
|
||||||
StopRequestTimeUpdater();
|
StopRequestTimeUpdater();
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||||
@@ -110,7 +110,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task PasswordlessLoginAsync(bool approveRequest)
|
private async Task PasswordlessLoginAsync(bool approveRequest)
|
||||||
{
|
{
|
||||||
if (LoginRequest.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
|
if (LoginRequest.IsExpired)
|
||||||
{
|
{
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||||
await Page.Navigation.PopModalAsync();
|
await Page.Navigation.PopModalAsync();
|
||||||
@@ -171,5 +171,7 @@ namespace Bit.App.Pages
|
|||||||
public string DeviceType { get; set; }
|
public string DeviceType { get; set; }
|
||||||
|
|
||||||
public string IpAddress { get; set; }
|
public string IpAddress { get; set; }
|
||||||
|
|
||||||
|
public bool IsExpired => RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ namespace Bit.App.Pages
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||||
|
await _syncService.SyncPasswordlessLoginRequestsAsync();
|
||||||
var success = await _syncService.FullSyncAsync(true);
|
var success = await _syncService.FullSyncAsync(true);
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
if (success)
|
if (success)
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ namespace Bit.App.Pages
|
|||||||
if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing)
|
if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing)
|
||||||
{
|
{
|
||||||
SyncRefreshing = true;
|
SyncRefreshing = true;
|
||||||
|
await _syncService.SyncPasswordlessLoginRequestsAsync();
|
||||||
await _syncService.FullSyncAsync(false);
|
await _syncService.FullSyncAsync(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/App/Resources/AppResources.Designer.cs
generated
61
src/App/Resources/AppResources.Designer.cs
generated
@@ -2893,6 +2893,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Get master password hint.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetMasterPasswordwordHint {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetMasterPasswordwordHint", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Get your master password hint.
|
/// Looks up a localized string similar to Get your master password hint.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3406,6 +3415,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Logging in as {0}.
|
||||||
|
/// </summary>
|
||||||
|
public static string LoggingInAsX {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LoggingInAsX", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Log In.
|
/// Looks up a localized string similar to Log In.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3434,10 +3452,11 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Login attempt from {0}. Do you want to switch to this account?.
|
/// Looks up a localized string similar to Login attempt from:
|
||||||
|
///{0}
|
||||||
|
///Do you want to switch to this account?.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount
|
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount {
|
||||||
{
|
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("LoginAttemptFromXDoYouWantToSwitchToThisAccount", resourceCulture);
|
return ResourceManager.GetString("LoginAttemptFromXDoYouWantToSwitchToThisAccount", resourceCulture);
|
||||||
}
|
}
|
||||||
@@ -3542,6 +3561,24 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Log In with another device.
|
||||||
|
/// </summary>
|
||||||
|
public static string LogInWithAnotherDevice {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInWithAnotherDevice", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Log In with master password.
|
||||||
|
/// </summary>
|
||||||
|
public static string LogInWithMasterPassword {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInWithMasterPassword", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Log out.
|
/// Looks up a localized string similar to Log out.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3947,6 +3984,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to New around here?.
|
||||||
|
/// </summary>
|
||||||
|
public static string NewAroundHere {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NewAroundHere", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to New custom field.
|
/// Looks up a localized string similar to New custom field.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -4181,6 +4227,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Not you?.
|
||||||
|
/// </summary>
|
||||||
|
public static string NotYou {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NotYou", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This login does not have a username or password configured..
|
/// Looks up a localized string similar to This login does not have a username or password configured..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1762,7 +1762,7 @@ Scanning will happen automatically.</value>
|
|||||||
<value>Syncing vault with pull down gesture.</value>
|
<value>Syncing vault with pull down gesture.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LogInSso" xml:space="preserve">
|
<data name="LogInSso" xml:space="preserve">
|
||||||
<value>Enterprise Single Sign-On</value>
|
<value>Enterprise single sign-on</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="LogInSsoSummary" xml:space="preserve">
|
<data name="LogInSsoSummary" xml:space="preserve">
|
||||||
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
||||||
@@ -2473,4 +2473,22 @@ select Add TOTP to store the key safely</value>
|
|||||||
{0}
|
{0}
|
||||||
Do you want to switch to this account?</value>
|
Do you want to switch to this account?</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewAroundHere" xml:space="preserve">
|
||||||
|
<value>New around here?</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetMasterPasswordwordHint" xml:space="preserve">
|
||||||
|
<value>Get master password hint</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoggingInAsX" xml:space="preserve">
|
||||||
|
<value>Logging in as {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="NotYou" xml:space="preserve">
|
||||||
|
<value>Not you?</value>
|
||||||
|
</data>
|
||||||
|
<data name="LogInWithMasterPassword" xml:space="preserve">
|
||||||
|
<value>Log in with master password</value>
|
||||||
|
</data>
|
||||||
|
<data name="LogInWithAnotherDevice" xml:space="preserve">
|
||||||
|
<value>Log In with another device</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace Bit.App.Services
|
|||||||
};
|
};
|
||||||
|
|
||||||
_pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData);
|
_pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData);
|
||||||
_messagingService.Value.Send("passwordlessLoginRequest", passwordlessLoginMessage);
|
_messagingService.Value.Send(Constants.PasswordlessLoginRequestKey, passwordlessLoginMessage);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -228,7 +228,9 @@ namespace Bit.App.Services
|
|||||||
if (data is PasswordlessNotificationData passwordlessNotificationData)
|
if (data is PasswordlessNotificationData passwordlessNotificationData)
|
||||||
{
|
{
|
||||||
var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail);
|
var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail);
|
||||||
if (notificationUserId != null)
|
var activeUserEmail = await _stateService.Value.GetActiveUserEmailAsync();
|
||||||
|
var notificationSaved = await _stateService.Value.GetPasswordlessLoginNotificationAsync();
|
||||||
|
if (activeUserEmail != passwordlessNotificationData.UserEmail && notificationUserId != null && notificationSaved != null)
|
||||||
{
|
{
|
||||||
await _stateService.Value.SetActiveUserAsync(notificationUserId);
|
await _stateService.Value.SetActiveUserAsync(notificationUserId);
|
||||||
_messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
_messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
Value="{DynamicResource StepperForegroundColor}" />
|
Value="{DynamicResource StepperForegroundColor}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="Frame"
|
<Style TargetType="Frame"
|
||||||
|
ApplyToDerivedTypes="True"
|
||||||
Class="btn-icon-row">
|
Class="btn-icon-row">
|
||||||
<Setter Property="BackgroundColor"
|
<Setter Property="BackgroundColor"
|
||||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||||
|
|||||||
@@ -130,7 +130,36 @@
|
|||||||
<Setter Property="BackgroundColor"
|
<Setter Property="BackgroundColor"
|
||||||
Value="{DynamicResource FabColor}" />
|
Value="{DynamicResource FabColor}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="controls:IconLabelButton"
|
||||||
|
ApplyToDerivedTypes="True"
|
||||||
|
Class="btn-icon-secondary">
|
||||||
|
<Setter Property="IconLabelColor"
|
||||||
|
Value="{DynamicResource ButtonTextColorOpacity}" />
|
||||||
|
<Setter Property="IconLabelBorderColor"
|
||||||
|
Value="{DynamicResource ButtonBorderColor}" />
|
||||||
|
<Setter Property="IconLabelBackgroundColor"
|
||||||
|
Value="{DynamicResource BackgroundColor}" />
|
||||||
|
<Setter Property="CornerRadius"
|
||||||
|
Value="5" />
|
||||||
|
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||||
|
<VisualStateGroupList>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal" />
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Property="IconLabelColor"
|
||||||
|
Value="{DynamicResource ButtonTextColorDisabled}" />
|
||||||
|
<Setter Property="IconLabelBackgroundColor"
|
||||||
|
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||||
|
<Setter Property="IconLabelBorderColor"
|
||||||
|
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateGroupList>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<Style TargetType="Button"
|
<Style TargetType="Button"
|
||||||
Class="btn-title"
|
Class="btn-title"
|
||||||
@@ -518,4 +547,19 @@
|
|||||||
<Setter Property="Radius"
|
<Setter Property="Radius"
|
||||||
Value="15" />
|
Value="15" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Entry" Class="entry-visual-disabled">
|
||||||
|
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||||
|
<VisualStateGroupList>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Property="TextColor"
|
||||||
|
Value="{DynamicResource MutedColor}" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateGroupList>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
Value="{DynamicResource StepperForegroundColor}" />
|
Value="{DynamicResource StepperForegroundColor}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="Frame"
|
<Style TargetType="Frame"
|
||||||
|
ApplyToDerivedTypes="True"
|
||||||
Class="btn-icon-row">
|
Class="btn-icon-row">
|
||||||
<Setter Property="BackgroundColor"
|
<Setter Property="BackgroundColor"
|
||||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Essentials;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Utilities.AccountManagement
|
namespace Bit.App.Utilities.AccountManagement
|
||||||
@@ -103,11 +104,12 @@ namespace Bit.App.Utilities.AccountManagement
|
|||||||
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
||||||
|
|
||||||
var email = await _stateService.GetEmailAsync();
|
var email = await _stateService.GetEmailAsync();
|
||||||
_accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email));
|
await _stateService.SetRememberedEmailAsync(email);
|
||||||
|
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin, new HomeNavigationParams(true));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
|
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin, new HomeNavigationParams(false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +185,7 @@ namespace Bit.App.Utilities.AccountManagement
|
|||||||
await Device.InvokeOnMainThreadAsync(() =>
|
await Device.InvokeOnMainThreadAsync(() =>
|
||||||
{
|
{
|
||||||
Options.HideAccountSwitcher = false;
|
Options.HideAccountSwitcher = false;
|
||||||
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
|
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin, new HomeNavigationParams(false));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,5 +220,17 @@ namespace Bit.App.Utilities.AccountManagement
|
|||||||
_messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED);
|
_messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task PromptToSwitchToExistingAccountAsync(string userId)
|
||||||
|
{
|
||||||
|
var switchToAccount = await _platformUtilsService.ShowDialogAsync(
|
||||||
|
AppResources.SwitchToAlreadyAddedAccountConfirmation,
|
||||||
|
AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
|
||||||
|
if (switchToAccount)
|
||||||
|
{
|
||||||
|
await _stateService.SetActiveUserAsync(userId);
|
||||||
|
_messagingService.Send("switchedAccount");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/App/Utilities/AccountManagement/HomeNavigationParams.cs
Normal file
14
src/App/Utilities/AccountManagement/HomeNavigationParams.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Bit.App.Abstractions;
|
||||||
|
|
||||||
|
namespace Bit.App.Utilities.AccountManagement
|
||||||
|
{
|
||||||
|
public class HomeNavigationParams : INavigationParams
|
||||||
|
{
|
||||||
|
public HomeNavigationParams(bool shouldCheckRememberEmail)
|
||||||
|
{
|
||||||
|
ShouldCheckRememberEmail = shouldCheckRememberEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldCheckRememberEmail { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,8 +83,10 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||||
Task DeleteSendAsync(string id);
|
Task DeleteSendAsync(string id);
|
||||||
|
Task<List<PasswordlessLoginResponse>> GetAuthRequestAsync();
|
||||||
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
||||||
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
||||||
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
||||||
|
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||||
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
||||||
|
|
||||||
|
Task<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync();
|
||||||
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
||||||
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Bit.Core.Abstractions
|
|||||||
{
|
{
|
||||||
List<AccountView> AccountViews { get; }
|
List<AccountView> AccountViews { get; }
|
||||||
Task<string> GetActiveUserIdAsync();
|
Task<string> GetActiveUserIdAsync();
|
||||||
|
Task<string> GetActiveUserEmailAsync();
|
||||||
Task<bool> IsActiveAccountAsync(string userId = null);
|
Task<bool> IsActiveAccountAsync(string userId = null);
|
||||||
Task SetActiveUserAsync(string userId);
|
Task SetActiveUserAsync(string userId);
|
||||||
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();
|
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<bool> SyncDeleteFolderAsync(SyncFolderNotification notification);
|
Task<bool> SyncDeleteFolderAsync(SyncFolderNotification notification);
|
||||||
Task<bool> SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit);
|
Task<bool> SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit);
|
||||||
Task<bool> SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit);
|
Task<bool> SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit);
|
||||||
|
// Passwordless code will be moved to an independent service in future techdept
|
||||||
|
Task SyncPasswordlessLoginRequestsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,5 +112,7 @@
|
|||||||
public const string File = "\xe96e";
|
public const string File = "\xe96e";
|
||||||
public const string Paste = "\xe96f";
|
public const string Paste = "\xe96f";
|
||||||
public const string ViewCellMenu = "\xe5d3";
|
public const string ViewCellMenu = "\xe5d3";
|
||||||
|
public const string Device = "\xe986";
|
||||||
|
public const string Suitcase = "\xe98c";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
public const string iOSNotificationClearActionId = "Clear";
|
public const string iOSNotificationClearActionId = "Clear";
|
||||||
public const string NotificationData = "notificationData";
|
public const string NotificationData = "notificationData";
|
||||||
public const string NotificationDataType = "Type";
|
public const string NotificationDataType = "Type";
|
||||||
|
public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest";
|
||||||
public const int SelectFileRequestCode = 42;
|
public const int SelectFileRequestCode = 42;
|
||||||
public const int SelectFilePermissionRequestCode = 43;
|
public const int SelectFilePermissionRequestCode = 43;
|
||||||
public const int SaveFileRequestCode = 44;
|
public const int SaveFileRequestCode = 44;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Response
|
namespace Bit.Core.Models.Response
|
||||||
{
|
{
|
||||||
@@ -13,7 +15,19 @@ namespace Bit.Core.Models.Response
|
|||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public string MasterPasswordHash { get; set; }
|
public string MasterPasswordHash { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public bool RequestApproved { get; set; }
|
public DateTime? ResponseDate { get; set; }
|
||||||
|
public bool? RequestApproved { get; set; }
|
||||||
public string Origin { get; set; }
|
public string Origin { get; set; }
|
||||||
|
public string RequestAccessCode { get; set; }
|
||||||
|
public Tuple<byte[], byte[]> RequestKeyPair { get; set; }
|
||||||
|
|
||||||
|
public bool IsAnswered => RequestApproved != null && ResponseDate != null;
|
||||||
|
|
||||||
|
public bool IsExpired => CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordlessLoginsResponse
|
||||||
|
{
|
||||||
|
public List<PasswordlessLoginResponse> Data { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -536,6 +536,12 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
#region PasswordlessLogin
|
#region PasswordlessLogin
|
||||||
|
|
||||||
|
public async Task<List<PasswordlessLoginResponse>> GetAuthRequestAsync()
|
||||||
|
{
|
||||||
|
var response = await SendAsync<object, PasswordlessLoginsResponse>(HttpMethod.Get, $"/auth-requests/", null, true, true);
|
||||||
|
return response.Data;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id)
|
public Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id)
|
||||||
{
|
{
|
||||||
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
||||||
@@ -547,6 +553,11 @@ namespace Bit.Core.Services
|
|||||||
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Put, $"/auth-requests/{id}", request, true, true);
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Put, $"/auth-requests/{id}", request, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier)
|
||||||
|
{
|
||||||
|
return SendAsync<object, bool>(HttpMethod.Get, $"/devices/knowndevice/{email}/{deviceIdentifier}", null, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
@@ -790,8 +801,6 @@ namespace Bit.Core.Services
|
|||||||
if (authed
|
if (authed
|
||||||
&&
|
&&
|
||||||
(
|
(
|
||||||
(tokenError && response.StatusCode == HttpStatusCode.BadRequest)
|
|
||||||
||
|
|
||||||
(logoutOnUnauthorized && response.StatusCode == HttpStatusCode.Unauthorized)
|
(logoutOnUnauthorized && response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
||
|
||
|
||||||
response.StatusCode == HttpStatusCode.Forbidden
|
response.StatusCode == HttpStatusCode.Forbidden
|
||||||
@@ -808,6 +817,17 @@ namespace Bit.Core.Services
|
|||||||
var responseJsonString = await response.Content.ReadAsStringAsync();
|
var responseJsonString = await response.Content.ReadAsStringAsync();
|
||||||
responseJObject = JObject.Parse(responseJsonString);
|
responseJObject = JObject.Parse(responseJsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authed && tokenError
|
||||||
|
&&
|
||||||
|
response.StatusCode == HttpStatusCode.BadRequest
|
||||||
|
&&
|
||||||
|
responseJObject?["error"]?.ToString() == "invalid_grant")
|
||||||
|
{
|
||||||
|
await _logoutCallbackAsync(new Tuple<string, bool, bool>(null, false, true));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
|
return new ErrorResponse(responseJObject, response.StatusCode, tokenError);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -471,6 +471,11 @@ namespace Bit.Core.Services
|
|||||||
SelectedTwoFactorProviderType = null;
|
SelectedTwoFactorProviderType = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync()
|
||||||
|
{
|
||||||
|
return await _apiService.GetAuthRequestAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id)
|
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id)
|
||||||
{
|
{
|
||||||
return await _apiService.GetAuthRequestAsync(id);
|
return await _apiService.GetAuthRequestAsync(id);
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ namespace Bit.Core.Services
|
|||||||
return activeUserId;
|
return activeUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetActiveUserEmailAsync()
|
||||||
|
{
|
||||||
|
var activeUserId = await GetActiveUserIdAsync();
|
||||||
|
return await GetEmailAsync(activeUserId);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> IsActiveAccountAsync(string userId = null)
|
public async Task<bool> IsActiveAccountAsync(string userId = null)
|
||||||
{
|
{
|
||||||
if (userId == null)
|
if (userId == null)
|
||||||
@@ -68,7 +74,6 @@ namespace Bit.Core.Services
|
|||||||
_state.ActiveUserId = userId;
|
_state.ActiveUserId = userId;
|
||||||
|
|
||||||
// Update pre-auth settings based on now-active user
|
// Update pre-auth settings based on now-active user
|
||||||
await SetRememberedEmailAsync(await GetEmailAsync());
|
|
||||||
await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync());
|
await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync());
|
||||||
await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync());
|
await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly ISendService _sendService;
|
private readonly ISendService _sendService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly Func<Tuple<string, bool, bool>, Task> _logoutCallbackAsync;
|
private readonly Func<Tuple<string, bool, bool>, Task> _logoutCallbackAsync;
|
||||||
|
|
||||||
public SyncService(
|
public SyncService(
|
||||||
@@ -39,6 +40,7 @@ namespace Bit.Core.Services
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
ISendService sendService,
|
ISendService sendService,
|
||||||
IKeyConnectorService keyConnectorService,
|
IKeyConnectorService keyConnectorService,
|
||||||
|
ILogger logger,
|
||||||
Func<Tuple<string, bool, bool>, Task> logoutCallbackAsync)
|
Func<Tuple<string, bool, bool>, Task> logoutCallbackAsync)
|
||||||
{
|
{
|
||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
@@ -53,6 +55,7 @@ namespace Bit.Core.Services
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_sendService = sendService;
|
_sendService = sendService;
|
||||||
_keyConnectorService = keyConnectorService;
|
_keyConnectorService = keyConnectorService;
|
||||||
|
_logger = logger;
|
||||||
_logoutCallbackAsync = logoutCallbackAsync;
|
_logoutCallbackAsync = logoutCallbackAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,5 +385,45 @@ namespace Bit.Core.Services
|
|||||||
new Dictionary<string, SendData>();
|
new Dictionary<string, SendData>();
|
||||||
await _sendService.ReplaceAsync(sends);
|
await _sendService.ReplaceAsync(sends);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SyncPasswordlessLoginRequestsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = await _stateService.GetActiveUserIdAsync();
|
||||||
|
// if the user has not enabled passwordless logins ignore requests
|
||||||
|
if (!await _stateService.GetApprovePasswordlessLoginsAsync(userId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginRequests = await _apiService.GetAuthRequestAsync();
|
||||||
|
if (loginRequests == null || !loginRequests.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validLoginRequest = loginRequests.Where(l => !l.IsAnswered && !l.IsExpired)
|
||||||
|
.OrderByDescending(x => x.CreationDate)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (validLoginRequest is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _stateService.SetPasswordlessLoginNotificationAsync(new PasswordlessRequestNotification()
|
||||||
|
{
|
||||||
|
Id = validLoginRequest.Id,
|
||||||
|
UserId = userId
|
||||||
|
});
|
||||||
|
|
||||||
|
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ namespace Bit.Core.Utilities
|
|||||||
var messagingService = Resolve<IMessagingService>("messagingService");
|
var messagingService = Resolve<IMessagingService>("messagingService");
|
||||||
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||||
var cryptoService = Resolve<ICryptoService>("cryptoService");
|
var cryptoService = Resolve<ICryptoService>("cryptoService");
|
||||||
|
var logger = Resolve<ILogger>();
|
||||||
|
|
||||||
SearchService searchService = null;
|
SearchService searchService = null;
|
||||||
|
|
||||||
var tokenService = new TokenService(stateService);
|
var tokenService = new TokenService(stateService);
|
||||||
@@ -67,7 +69,7 @@ namespace Bit.Core.Utilities
|
|||||||
});
|
});
|
||||||
var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService,
|
var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService,
|
||||||
cryptoService, collectionService, organizationService, messagingService, policyService, sendService,
|
cryptoService, collectionService, organizationService, messagingService, policyService, sendService,
|
||||||
keyConnectorService, (extras) =>
|
keyConnectorService, logger, (extras) =>
|
||||||
{
|
{
|
||||||
messagingService.Send("logout", extras);
|
messagingService.Send("logout", extras);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|||||||
@@ -425,15 +425,16 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LaunchHomePage()
|
private void LaunchHomePage(bool shouldCheckRememberEmail = true)
|
||||||
{
|
{
|
||||||
var homePage = new HomePage();
|
var appOptions = new AppOptions { IosExtension = true };
|
||||||
var app = new App.App(new AppOptions { IosExtension = true });
|
var homePage = new HomePage(appOptions, shouldCheckRememberEmail);
|
||||||
|
var app = new App.App(appOptions);
|
||||||
ThemeManager.SetTheme(app.Resources);
|
ThemeManager.SetTheme(app.Resources);
|
||||||
ThemeManager.ApplyResourcesTo(homePage);
|
ThemeManager.ApplyResourcesTo(homePage);
|
||||||
if (homePage.BindingContext is HomeViewModel vm)
|
if (homePage.BindingContext is HomeViewModel vm)
|
||||||
{
|
{
|
||||||
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||||
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
||||||
@@ -456,8 +457,8 @@ namespace Bit.iOS.Autofill
|
|||||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(environmentPage);
|
var navigationPage = new NavigationPage(environmentPage);
|
||||||
@@ -475,7 +476,7 @@ namespace Bit.iOS.Autofill
|
|||||||
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(registerPage);
|
var navigationPage = new NavigationPage(registerPage);
|
||||||
@@ -495,8 +496,9 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(loginPage);
|
var navigationPage = new NavigationPage(loginPage);
|
||||||
@@ -602,7 +604,14 @@ namespace Bit.iOS.Autofill
|
|||||||
switch (navTarget)
|
switch (navTarget)
|
||||||
{
|
{
|
||||||
case NavigationTarget.HomeLogin:
|
case NavigationTarget.HomeLogin:
|
||||||
DismissViewController(false, () => LaunchHomePage());
|
if (navParams is HomeNavigationParams homeParams)
|
||||||
|
{
|
||||||
|
DismissViewController(false, () => LaunchHomePage(homeParams.ShouldCheckRememberEmail));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DismissViewController(false, () => LaunchHomePage());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.Login:
|
case NavigationTarget.Login:
|
||||||
if (navParams is LoginNavigationParams loginParams)
|
if (navParams is LoginNavigationParams loginParams)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden.autofill</string>
|
<string>com.8bit.bitwarden.autofill</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2022.10.1</string>
|
<string>2022.11.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
|
|||||||
Binary file not shown.
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
<string>com.8bit.bitwarden.find-login-action-extension</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2022.10.1</string>
|
<string>2022.11.0</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
|
|||||||
@@ -446,15 +446,16 @@ namespace Bit.iOS.Extension
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LaunchHomePage()
|
private void LaunchHomePage(bool shouldCheckRememberEmail = true)
|
||||||
{
|
{
|
||||||
var homePage = new HomePage();
|
var appOptions = new AppOptions { IosExtension = true };
|
||||||
var app = new App.App(new AppOptions { IosExtension = true });
|
var homePage = new HomePage(appOptions, shouldCheckRememberEmail);
|
||||||
|
var app = new App.App(appOptions);
|
||||||
ThemeManager.SetTheme(app.Resources);
|
ThemeManager.SetTheme(app.Resources);
|
||||||
ThemeManager.ApplyResourcesTo(homePage);
|
ThemeManager.ApplyResourcesTo(homePage);
|
||||||
if (homePage.BindingContext is HomeViewModel vm)
|
if (homePage.BindingContext is HomeViewModel vm)
|
||||||
{
|
{
|
||||||
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
|
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||||
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
|
||||||
@@ -477,8 +478,8 @@ namespace Bit.iOS.Extension
|
|||||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(environmentPage);
|
var navigationPage = new NavigationPage(environmentPage);
|
||||||
@@ -496,7 +497,7 @@ namespace Bit.iOS.Extension
|
|||||||
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
|
||||||
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(registerPage);
|
var navigationPage = new NavigationPage(registerPage);
|
||||||
@@ -516,8 +517,9 @@ namespace Bit.iOS.Extension
|
|||||||
{
|
{
|
||||||
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
|
||||||
|
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
|
||||||
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
vm.LogInSuccessAction = () => DismissLockAndContinue();
|
||||||
vm.CloseAction = () => CompleteRequest(null, null);
|
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var navigationPage = new NavigationPage(loginPage);
|
var navigationPage = new NavigationPage(loginPage);
|
||||||
|
|||||||
Binary file not shown.
@@ -15,7 +15,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2022.10.1</string>
|
<string>2022.11.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
|
|||||||
@@ -287,13 +287,13 @@ namespace Bit.iOS.ShareExtension
|
|||||||
return _app;
|
return _app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LaunchHomePage()
|
private void LaunchHomePage(bool shouldCheckRememberEmail = true)
|
||||||
{
|
{
|
||||||
var homePage = new HomePage();
|
var homePage = new HomePage(_appOptions.Value, shouldCheckRememberEmail);
|
||||||
SetupAppAndApplyResources(homePage);
|
SetupAppAndApplyResources(homePage);
|
||||||
if (homePage.BindingContext is HomeViewModel vm)
|
if (homePage.BindingContext is HomeViewModel vm)
|
||||||
{
|
{
|
||||||
vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow());
|
vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
|
||||||
vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow());
|
vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow());
|
||||||
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
|
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
|
||||||
vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow());
|
vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow());
|
||||||
@@ -311,8 +311,8 @@ namespace Bit.iOS.ShareExtension
|
|||||||
ThemeManager.ApplyResourcesTo(environmentPage);
|
ThemeManager.ApplyResourcesTo(environmentPage);
|
||||||
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage());
|
vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
|
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigateToPage(environmentPage);
|
NavigateToPage(environmentPage);
|
||||||
@@ -325,7 +325,7 @@ namespace Bit.iOS.ShareExtension
|
|||||||
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
if (registerPage.BindingContext is RegisterPageViewModel vm)
|
||||||
{
|
{
|
||||||
vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
|
vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email));
|
||||||
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage());
|
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
}
|
}
|
||||||
NavigateToPage(registerPage);
|
NavigateToPage(registerPage);
|
||||||
}
|
}
|
||||||
@@ -338,11 +338,9 @@ namespace Bit.iOS.ShareExtension
|
|||||||
{
|
{
|
||||||
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
|
vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false));
|
||||||
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
|
vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow());
|
||||||
vm.LogInSuccessAction = () =>
|
vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow());
|
||||||
{
|
vm.LogInSuccessAction = () => { DismissLockAndContinue(); };
|
||||||
DismissLockAndContinue();
|
vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage(shouldCheckRememberEmail: false));
|
||||||
};
|
|
||||||
vm.CloseAction = () => CompleteRequest();
|
|
||||||
}
|
}
|
||||||
NavigateToPage(loginPage);
|
NavigateToPage(loginPage);
|
||||||
|
|
||||||
@@ -426,7 +424,14 @@ namespace Bit.iOS.ShareExtension
|
|||||||
switch (navTarget)
|
switch (navTarget)
|
||||||
{
|
{
|
||||||
case NavigationTarget.HomeLogin:
|
case NavigationTarget.HomeLogin:
|
||||||
ExecuteLaunch(LaunchHomePage);
|
if (navParams is HomeNavigationParams homeParams)
|
||||||
|
{
|
||||||
|
ExecuteLaunch(() => LaunchHomePage(homeParams.ShouldCheckRememberEmail));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ExecuteLaunch(() => LaunchHomePage());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.Login:
|
case NavigationTarget.Login:
|
||||||
if (navParams is LoginNavigationParams loginParams)
|
if (navParams is LoginNavigationParams loginParams)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.8bit.bitwarden</string>
|
<string>com.8bit.bitwarden</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2022.10.1</string>
|
<string>2022.11.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user