1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-15 15:53:44 +00:00

Compare commits

..

23 Commits

Author SHA1 Message Date
github-actions[bot]
1b34f68794 Bumped version to 2022.11.0 (#2212)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
(cherry picked from commit 8e2e143e6f)
2022-12-01 17:45:03 +00:00
André Bispo
c9eb3f7f2d [SG-860] Add condition to execute notification tap code (#2211) 2022-12-01 12:01:44 +00:00
André Bispo
eab32ef59e [SG-831] Pull Down Sync does not retrieve pending AuthRequests (#2196)
* [SG-831] Pull to refresh forces refresh.

* [SG-831] Expose sync login request method to be used independently

* [SG-831] Change sync order

(cherry picked from commit 34fd30e157)
2022-11-17 11:09:18 -05:00
André Bispo
bb759c16ea [SG-808] Change navigation on remove account after logout by timeout (#2193) 2022-11-16 16:46:13 +00:00
André Bispo
b4c0fba5e9 [SG-816] Get all login requests and pick the most recent (#2191) 2022-11-15 18:19:26 +00:00
André Bispo
5e4192b7db [SG-813] Remove unwanted code for this release 2022-11-11 19:14:24 +00:00
Robyn MacCallum
0187676b5e [SG-813] Add missing import 2022-11-11 13:39:18 -05:00
Robyn MacCallum
3b796c6599 Add syncCompleted to check to get passwordless requests (#2186) 2022-11-11 13:25:20 -05:00
André Bispo
eeb0f61986 [SG-813] Not You? crashes app after vault logout timeout (#2184) 2022-11-11 18:20:50 +00:00
Patrick
13eff49372 Update AppResources.resx (#2181) 2022-11-11 07:50:24 -05:00
Álison Fernandes
47d3a8b345 [EC-655] Adds build variants to the mobile codebase using a CAKE script (#2161)
* Implemented CAKE build script

* cake script now deals with all of iOS's .plists

* cake now updates iOS bundleid's / Android packagename in codefiles

* iOS Bundle ID / Name should be correctly handled now + refactor

* tabs -> spaces

* Additional code files are now handled by cake

* Additional iOS codefile changes required

* Android's Autofill Label is now changed

* Removed dash from packagenames / bundleIDs

* Fixed CFBundleURLName set

* Added google-services.json to cake preprocessing

* Add CAKE to build workflow
- Android

* Add debug

* Updated cake's GitVersion.Tool

* AndroidManifest manual parsing needs to happen first

* Added Android Constants to build.cake

* [SG-747] Add Android QA build to mobile build pipeline (#2144)

* Add checkout depth

* Build and upload QA artifacts

* Remove missing .aab

* Update build.yml

* Update paths

* Update var names

* Build and upload QA artifacts

* Add in matrix to path.

* Lets not fail all the jobs if something pukes

* Add in some flow logic for QA

* We need strings in pwsh

* Remove extra quotes

* Testing, remove uneeded runs

* Test folder items

* [SG-747] Added more debug info to find problem

* [SG-747] copy signed apk to correct file name for each app variant

* [SG-747] try to fix if statement

* [SG-747] separate decrypt google services into another step with condition.

* [SG-747] fixed typo and line break

* [SG-747] added debug to check output path

* [SG-747] fix package name

* [SG-747] Fixed condition of step execution

* [SG-747] test if cases

* [SG-747] Code clean up

* [SG-747] Added FDroid and iOS steps.

* [SG-747] Removed test step

* [SG-747] Step name changes

* Update condition to be more inclusive

Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>

* [SG-747] Expand if condition to allow more build types other than QA

* [SG-747] removed execution condition

Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>

* Apply suggestions from code review

Linter suggestions

Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Micaiah Martin <mmartin@bitwarden.com>
Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>

* Base bundle ID refactor and cleaned up TODOs

- Added base vars for the bundle IDs
- Removed a TODO and explained the remaining ones
- Commented a unused var, keeping it in the code as this might be useful later

Co-authored-by: Micaiah Martin <mmartin@bitwarden.com>
Co-authored-by: Federico Andrés Maccaroni <fedemkr@gmail.com>
Co-authored-by: Todd Martin <tmartin@bitwarden.com>
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: mimartin12 <77340197+mimartin12@users.noreply.github.com>
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
(cherry picked from commit 99472d2548)
2022-11-07 13:38:56 -05:00
André Bispo
76042a2ef7 [SG-806] add try catch on initAsync (#2173) 2022-11-07 14:52:44 +00:00
André Bispo
0507fcd54a [SG-802] Add if check on notification tap. (#2172) 2022-11-07 14:52:09 +00:00
André Bispo
1a69cc0591 [SG-800] Selecting notification for inactive account on iOS while app is open does not display the request (#2166) 2022-11-04 16:59:26 +00:00
André Bispo
0866a78802 [SG-773][SG-775] Duplicate passwordless login requests (#2160)
* [SG-773] Change method call to message send

* [SG-773] Introduce lock to avoid concurrent executions of login requests

* [SG-773][SG-775] add comment

* [SSG-773][SG-775] Refactor passwordlessLoginRequest string to constant

(cherry picked from commit 9baa79e10b)
2022-10-31 15:38:29 -04:00
André Bispo
cfda5fd6ff [SG-765] Add cancel button to passwordless login modal on android (#2159)
(cherry picked from commit a8909a3ce6)
2022-10-31 15:37:28 -04:00
Carlos Gonçalves
526904d1d8 SG-786 - Fix 400 error code log outs without invalid_grant (#2156)
* SG-786 - Added validation to check if the 400 error is invalid grant

* SG 786 - Improved code quality

(cherry picked from commit ee09c0abda)
2022-10-31 13:43:13 -04:00
André Bispo
b9b9c2e5ff [SG-166] Two Step Login - Feature Branch (#2157)
* [SG-166] Update fonts to have necessary icons

* [SG-166] Add new custom view to hold a button with a font icon and a label.

* [SG-166] Two Step login flow - Mobile (#2153)

* [SG-166] Add UI elements to Home and Login pages. Change VMs to function with new UI. Add new string resources.

* [SG-166] Pass email parameter from Home to Login page.

* [SG-166] Pass email to password hint page.

* [SG-166] Remove remembered email from account switching.

* [SG-166] Add GetKnownDevice endpoint to ApiService

* [SG-166] Fix GetKnownDevice string uri

* [SG-166] Add Renderer for IconLabel control. Add RemoveFontPadding bool property.

* [SG-166] include IconLabelRenderer in Android csproj file

* [SG-166] Add new control. Add styles for the control.

* [SG-166] Add verification to start login if email is remembered

* [SG-166] Pass default email to hint page

* [SG-166] Login with device button only shows if it is a known device.

* [SG-166] Change Remember Email to Remember me. Change Check to Switch control.

* [SG-166] Add command to button for SSO Login

* Revert "[SG-166] Update fonts to have necessary icons"

This reverts commit 472b541cef.

* [SG-166] Remove IconLabel Android renderer. Add RemoveFontPadding effect.

* [SG-166] Update font with new device and suitcase icon

* [SG-166] Fix RemoveFontPadding effect

* [SG-166] Remove unused property in IconLabel

* [SG-166] Fix formatting on IconLabelButton.xaml

* [SG-166] Update padding effect to IconLabel

* [SG-166] Add control variable to run code once on create

* [SG-166] Add email validation before continue

* [SG-166] Refactor icons

* [SG-166] Update iOS Extension font

* [SG-166] Remove HomePage login btn step

* [SG-166] Make clickable area smaller

* [SG-166] Fix hint filled by default

* [SG-166] Fix IconButton font issue

* [SG-166] Fix iOS extension

* [SG-166] Move style to Base instead of platforms

* [SG-166] Fix layout for IconLabelButton

* [SG-166] Switched EventHandler for Command

* [SG-166] Removed event handler

* [SG-166] Fix LoginPage layout options

* [SG-166] Fix extensions Login null email

* [SG-166] Move remembered email logic to viewmodel

* [SG-166] Protect method and show dialog in case of error

* [SG-166] Rename of GetKnownDevice api method

* [SG-166] rename text resource key name

* [SG-166] Add close button to iOS extension

* [SG-166] Switch event handlers for commands

* [SG-166] Change commands UI thread invocation.

* [SG-166] Remove Login with device button from the UI

* [SG-166] Fixed appOptions and close button on iOS Extensions
2022-10-28 23:10:41 +01:00
github-actions[bot]
89adab6784 Autosync the updated translations (#2155)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-10-28 02:40:47 +02:00
mp-bw
77d8afcfe6 added missing id check to obj comparison (#2154) 2022-10-27 12:09:33 -04:00
Michał Chęciński
4fcb063190 Move renovate.json to .github folder (#2152) 2022-10-27 15:49:29 +02:00
mp-bw
5deba15373 Updated avatar color selection logic (#2151)
* updated avatar color selection logic

* tweaks

* more tweaks

* formatting
2022-10-26 12:34:54 -04:00
Carlos Gonçalves
505426cd6a [SG 547] Mobile username generator iOS.Extension UI changes (#2140)
* [SG-547] - Added button to generate username when using iOS extension

* [SG-547] - Missing changes from last commit

* SG-547 - Added missing interface method

* SG-547 - Added token renovation for iOS.Extension flow

* SG-547 Replaced generate buttons for icons

* SG-547 Removed unnecessary validation

* SG-547 - Fixed PR comments

* SG 547 - Missing file from last commit

* SG-547 - Fixed PR comments

* SG-547 - Renamed method
2022-10-25 21:05:15 +01:00
108 changed files with 1796 additions and 1255 deletions

View File

@@ -7,6 +7,12 @@
"commands": [
"dotnet-format"
]
},
"cake.tool": {
"version": "2.2.0",
"commands": [
"dotnet-cake"
]
}
}
}

View File

@@ -59,6 +59,10 @@ jobs:
name: Android
runs-on: windows-2022
needs: setup
strategy:
fail-fast: false
matrix:
variant: ['prod', 'qa']
steps:
- name: Setup NuGet
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
@@ -67,7 +71,7 @@ jobs:
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Work Around for broken Windows 2022 Runner Image
run: |
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
@@ -87,7 +91,6 @@ jobs:
Write-Host "components were not installed"
exit 1
}
- name: Print environment
run: |
nuget help | grep Version
@@ -98,7 +101,8 @@ jobs:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
fetch-depth: 0
- name: Decrypt secrets
env:
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
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--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" \
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
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
run: |
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
@@ -142,26 +151,35 @@ jobs:
run: dotnet test test/Core.Test/Core.Test.csproj
- name: Build Play Store publisher
if: ${{ matrix.variant == 'prod' }}
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: |
$configuration = "Release";
Write-Output "########################################"
Write-Output "##### Build $configuration Configuration"
Write-Output "########################################"
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
shell: pwsh
- name: Sign for Play Store
- name: Sign Android Build
env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
run: |
$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 "##### Sign Google Play Bundle Release Configuration"
Write-Output "########################################"
@@ -175,9 +193,8 @@ jobs:
Write-Output "##### Copy Google Play Bundle to project root"
Write-Output "########################################"
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab");
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab");
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
Copy-Item $signedAabPath $signedAabDestPath
Write-Output "########################################"
@@ -193,33 +210,41 @@ jobs:
Write-Output "##### Copy Release APK to project root"
Write-Output "########################################"
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk");
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk");
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
Copy-Item $signedApkPath $signedApkDestPath
shell: pwsh
- name: Upload Play Store .aab artifact
- name: Upload Prod .aab artifact
if: ${{ matrix.variant == 'prod' }}
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: com.x8bit.bitwarden.aab
path: ./com.x8bit.bitwarden.aab
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
with:
name: com.x8bit.bitwarden.apk
path: ./com.x8bit.bitwarden.apk
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
if: |
(github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_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/hotfix-rc'
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
&& needs.setup.outputs.rc_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/hotfix-rc' ) }}
run: |
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
CREDS_PATH="$HOME/secrets/play_creds.json"

3
.gitignore vendored
View File

@@ -208,4 +208,5 @@ FakesAssemblies/
# Other
project.lock.json
.DS_Store
src/App/Css
src/App/Css
tools

345
build.cake Normal file
View 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);

View File

@@ -156,8 +156,7 @@
<Compile Include="Services\FileService.cs" />
<Compile Include="Services\AutofillHandler.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Renderers\CollectionView\ExtendedCollectionViewRenderer.cs" />
<Compile Include="Utilities\RecyclerSwipeItemTouchCallback.cs" />
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -296,7 +295,6 @@
<Folder Include="Resources\values-v30\" />
<Folder Include="Resources\drawable-v26\" />
<Folder Include="Resources\drawable-night-v26\" />
<Folder Include="Renderers\CollectionView\" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

Binary file not shown.

View 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()
{
}
}
}

View File

@@ -20,7 +20,6 @@ using System.Net;
using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Utilities.Helpers;
using Bit.App.Controls;
#if !FDROID
using Android.Gms.Security;
@@ -75,16 +74,7 @@ namespace Bit.Droid
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
var cipherHelper = new CipherHelper(
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IEventService>("eventService"),
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
ServiceContainer.Resolve<IClipboardService>("clipboardService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService")
);
ServiceContainer.Register<ICipherHelper>("cipherHelper", cipherHelper);
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="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-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
@@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">

View File

@@ -1,87 +0,0 @@
using System;
using Android.Content;
using AndroidX.RecyclerView.Widget;
using Bit.App.Controls;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Droid.Renderers.CollectionView;
using Bit.Droid.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using static Android.Content.ClipData;
using static AndroidX.RecyclerView.Widget.RecyclerView;
[assembly: ExportRenderer(typeof(ExtendedCollectionView), typeof(ExtendedCollectionViewRenderer))]
namespace Bit.Droid.Renderers.CollectionView
{
public class CustomGroupableItemsViewAdapter<TItemsView, TItemsViewSource> : GroupableItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsView : GroupableItemsView
where TItemsViewSource : IGroupableItemsViewSource
{
protected internal CustomGroupableItemsViewAdapter(TItemsView groupableItemsView, Func<View, Context, ItemContentView> createView = null)
: base(groupableItemsView, createView)
{
}
public object GetItemAt(int position)
{
return ItemsSource.GetItem(position);
}
}
public class ExtendedCollectionViewRenderer : GroupableItemsViewRenderer<ExtendedCollectionView, CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>, IGroupableItemsViewSource>
{
ItemTouchHelper _itemTouchHelper;
public ExtendedCollectionViewRenderer(Context context) : base(context)
{
}
protected override CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource> CreateAdapter()
{
return new CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>(ItemsView);
}
protected override void SetUpNewElement(ExtendedCollectionView newElement)
{
base.SetUpNewElement(newElement);
if (newElement is null)
{
return;
}
var itemTouchCallback = new RecyclerSwipeItemTouchCallback<CipherViewCellViewModel>(ItemTouchHelper.Right, this.Context, new CipherViewModelSwipeableItem(),
viewHolder =>
{
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
&&
templatedViewHolder.View?.BindingContext is CipherViewCellViewModel vm)
{
return vm;
}
return null;
});
itemTouchCallback.OnSwipedCommand = new Command<ViewHolder>(viewHolder =>
{
ItemsViewAdapter.NotifyItemChanged(viewHolder.LayoutPosition);
ItemsView.OnSwipeCommand?.Execute(ItemsViewAdapter.GetItemAt(viewHolder.BindingAdapterPosition));
});
_itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
_itemTouchHelper.AttachToRecyclerView(this);
}
protected override void TearDownOldElement(ItemsView oldElement)
{
base.TearDownOldElement(oldElement);
if (oldElement is null)
{
return;
}
_itemTouchHelper.AttachToRecyclerView(null);
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Bit.Droid.Renderers
protected override void OnElementChanged(ElementChangedEventArgs<View> elementChangedEvent)
{
base.OnElementChanged(elementChangedEvent);
if (elementChangedEvent.NewElement is ExtendedGrid extGrid && extGrid.ApplyRipple)
if (elementChangedEvent.NewElement != null)
{
SetBackgroundResource(Resource.Drawable.list_item_bg);
}

View File

@@ -520,5 +520,11 @@ namespace Bit.Droid.Services
intent.SetData(uri);
Application.Context.StartActivity(intent);
}
public void CloseExtensionPopUp()
{
// only used by iOS
throw new NotImplementedException();
}
}
}

View File

@@ -1,156 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Util;
using Android.Views;
using AndroidX.RecyclerView.Widget;
using Bit.App.Abstractions;
using Xamarin.Forms.Platform.Android;
using FontImageSource = Xamarin.Forms.FontImageSource;
namespace Bit.Droid.Utilities
{
public class RecyclerSwipeItemTouchCallback<TItem> : ItemTouchHelper.SimpleCallback
{
private Paint _clearPaint;
private readonly ColorDrawable _background = new ColorDrawable();
private readonly Android.Content.Context _context;
private readonly ISwipeableItem<TItem> _swipeableItem;
private readonly Func<RecyclerView.ViewHolder, TItem> _viewHolderToTItem;
private Dictionary<string, Typeface> _fontFamilyTypefaceCache = new Dictionary<string, Typeface>();
public RecyclerSwipeItemTouchCallback(int swipeDir, Android.Content.Context context, ISwipeableItem<TItem> swipeableItem, Func<RecyclerView.ViewHolder, TItem> viewHolderToTItem)
: base(0, swipeDir)
{
_context = context;
_swipeableItem = swipeableItem;
_viewHolderToTItem = viewHolderToTItem;
_clearPaint = new Paint();
_clearPaint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear));
}
public ICommand OnSwipedCommand { get; set; }
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
{
return false;
}
public override void OnChildDrawOver(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, bool isCurrentlyActive)
{
var itemView = viewHolder.ItemView;
int itemHeight = itemView.Bottom - itemView.Top;
var isCanceled = (dX == 0f) && !isCurrentlyActive;
if (isCanceled)
{
ClearCanvas(c, itemView.Right + dX, itemView.Top, itemView.Right, itemView.Bottom);
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, false);
return;
}
if (!(_swipeableItem.GetSwipeIcon(_viewHolderToTItem(viewHolder)) is FontImageSource fontSource))
{
return;
}
using var paint = GetIconPaint(itemView, fontSource);
var width = (int)(paint.MeasureText(fontSource.Glyph) + .5f);
var baseline = (int)(-paint.Ascent() + .5f);
var height = (int)(baseline + paint.Descent() + .5f);
int itemTop = itemView.Top + (itemHeight - height) / 2;
int itemMargin = (itemHeight - height) / 2;
int itemBottom = itemTop + height;
_background.Color = _swipeableItem.GetBackgroundColor(_viewHolderToTItem(viewHolder)).ToAndroid();
if (dX < 0)
{
_background.SetBounds((int)(itemView.Right + dX), itemView.Top, itemView.Right, itemView.Bottom);
_background.Draw(c);
int itemLeft = itemView.Right - itemMargin - width;
int itemRight = itemView.Right - itemMargin;
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
}
else
{
_background.SetBounds((int)(itemView.Left + dX), itemView.Top, itemView.Left, itemView.Bottom);
_background.Draw(c);
int itemLeft = itemView.Left + itemMargin;
int itemRight = itemView.Left + itemMargin + width;
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
}
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
private Paint GetIconPaint(View itemView, FontImageSource fontSource)
{
var paint = new Paint
{
TextSize = TypedValue.ApplyDimension(ComplexUnitType.Dip, (float)fontSource.Size, _context.Resources.DisplayMetrics),
Color = fontSource.Color.ToAndroid(),
TextAlign = Paint.Align.Left,
AntiAlias = true,
};
if (fontSource.FontFamily != null)
{
if (!_fontFamilyTypefaceCache.TryGetValue(fontSource.FontFamily, out var typeface))
{
var font = new Xamarin.Forms.Font();
// HACK: there is no way to set the font family of Font
// and the only public extension method to get the typeface is thorugh a Xamarin.Forms.Font
// so we use reflection here to set the font family and take advantage of ToTypeface method
// Also, we need to box the font in order to use reflection to set the property because it's a struct
object fontBoxed = font;
var pinfo = typeof(Xamarin.Forms.Font)
.GetProperty(nameof(Xamarin.Forms.Font.FontFamily));
pinfo.SetValue(fontBoxed, fontSource.FontFamily, null);
typeface = ((Xamarin.Forms.Font)fontBoxed).ToTypeface();
_fontFamilyTypefaceCache.Add(fontSource.FontFamily, typeface);
}
paint.SetTypeface(typeface);
}
int alpha = Math.Abs(((int)((itemView.TranslationX / itemView.Width) * 510)));
paint.Alpha = Math.Min(alpha, 255);
return paint;
}
public override int GetSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
{
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
&&
_swipeableItem.CanSwipe(_viewHolderToTItem(viewHolder)))
{
return base.GetSwipeDirs(recyclerView, viewHolder);
}
return 0;
}
private void ClearCanvas(Canvas c, float left, float top, float right, float bottom)
{
if (c != null)
c.DrawRect(left, top, right, bottom, _clearPaint);
}
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
{
OnSwipedCommand?.Execute(viewHolder);
}
}
}

View File

@@ -9,5 +9,6 @@ namespace Bit.App.Abstractions
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
Task PromptToSwitchToExistingAccountAsync(string userId);
}
}

View File

@@ -37,5 +37,6 @@ namespace Bit.App.Abstractions
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
void CloseExtensionPopUp();
}
}

View File

@@ -1,11 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Abstractions
{
public interface ISwipeableItem<TItem>
{
bool CanSwipe(TItem item);
FontImageSource GetSwipeIcon(TItem item);
Color GetBackgroundColor(TItem item);
}
}

View File

@@ -138,8 +138,8 @@
<Folder Include="Lists\ItemViewModels\CustomFields\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Utilities\Helpers\" />
<Folder Include="Controls\DateTime\" />
<Folder Include="Controls\IconLabelButton\" />
</ItemGroup>
<ItemGroup>
@@ -430,7 +430,7 @@
<None Remove="Lists\ItemViewModels\CustomFields\" />
<None Remove="Controls\AccountSwitchingOverlay\" />
<None Remove="Utilities\AccountManagement\" />
<None Remove="Utilities\Helpers\" />
<None Remove="Controls\DateTime\" />
<None Remove="Controls\IconLabelButton\" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
@@ -33,8 +34,9 @@ namespace Bit.App
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
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 object _processingLoginRequestLock = new object();
public App(AppOptions appOptions)
{
@@ -143,9 +145,15 @@ namespace Bit.App
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)
@@ -162,7 +170,6 @@ namespace Bit.App
_pendingCheckPasswordlessLoginRequests = true;
return;
}
_pendingCheckPasswordlessLoginRequests = false;
if (await _vaultTimeoutService.IsLockedAsync())
{
@@ -182,6 +189,11 @@ namespace Bit.App
// Delay to wait for the vault page to appear
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 page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
{
@@ -196,7 +208,7 @@ namespace Bit.App
});
await _stateService.SetPasswordlessLoginNotificationAsync(null);
_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)));
}
@@ -211,13 +223,20 @@ namespace Bit.App
}
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);
if (result == AppResources.Ok)
try
{
await _stateService.SetActiveUserAsync(notification.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
if (result == AppResources.Ok)
{
await _stateService.SetActiveUserAsync(notification.UserId);
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
return true;
@@ -242,7 +261,7 @@ namespace Bit.App
}
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
if (Device.RuntimePlatform == Device.Android)
{
@@ -278,7 +297,7 @@ namespace Bit.App
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
}
if (Device.RuntimePlatform == Device.Android)
{
@@ -440,7 +459,14 @@ namespace Bit.App
switch (navTarget)
{
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;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)

View File

@@ -14,7 +14,7 @@ namespace Bit.App.Controls
{
AccountView = accountView;
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email);
}
public AccountView AccountView

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Utilities;
using SkiaSharp;
using Xamarin.Forms;
@@ -10,7 +11,8 @@ namespace Bit.App.Controls
{
public class AvatarImageSource : StreamImageSource
{
private string _data;
private readonly string _text;
private readonly string _id;
public override bool Equals(object obj)
{
@@ -21,20 +23,21 @@ namespace Bit.App.Controls
if (obj is AvatarImageSource avatar)
{
return avatar._data == _data;
return avatar._id == _id && avatar._text == _text;
}
return base.Equals(obj);
}
public override int GetHashCode() => _data?.GetHashCode() ?? -1;
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
public AvatarImageSource(string name = null, string email = null)
public AvatarImageSource(string userId = null, string name = null, string email = null)
{
_data = name;
if (string.IsNullOrWhiteSpace(_data))
_id = userId;
_text = name;
if (string.IsNullOrWhiteSpace(_text))
{
_data = email;
_text = email;
}
}
@@ -52,24 +55,24 @@ namespace Bit.App.Controls
private Stream Draw()
{
string chars;
string upperData = null;
string upperCaseText = null;
if (string.IsNullOrEmpty(_data))
if (string.IsNullOrEmpty(_text))
{
chars = "..";
}
else if (_data?.Length > 1)
else if (_text?.Length > 1)
{
upperData = _data.ToUpper();
chars = GetFirstLetters(upperData, 2);
upperCaseText = _text.ToUpper();
chars = GetFirstLetters(upperCaseText, 2);
}
else
{
chars = upperData = _data.ToUpper();
chars = upperCaseText = _text.ToUpper();
}
var bgColor = StringToColor(upperData);
var textColor = Color.White;
var bgColor = CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
var size = 50;
using (var bitmap = new SKBitmap(size * 2,
@@ -85,7 +88,7 @@ namespace Bit.App.Controls
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
Color = SKColor.Parse(bgColor)
})
{
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
@@ -97,7 +100,7 @@ namespace Bit.App.Controls
IsAntialias = true,
Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex())
Color = SKColor.Parse(bgColor)
})
{
canvas.DrawCircle(midX, midY, radius, circlePaint);
@@ -108,7 +111,7 @@ namespace Bit.App.Controls
{
IsAntialias = true,
Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()),
Color = SKColor.Parse(textColor),
TextSize = textSize,
TextAlign = SKTextAlign.Center,
Typeface = typeface

View File

@@ -5,19 +5,19 @@ namespace Bit.App.Controls
{
public interface IAvatarImageSourcePool
{
AvatarImageSource GetOrCreateAvatar(string name, string email);
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email);
}
public class AvatarImageSourcePool : IAvatarImageSourcePool
{
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email)
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email)
{
var key = $"{name}{email}";
var key = $"{userId}{name}{email}";
if (!_cache.TryGetValue(key, out var avatar))
{
avatar = new AvatarImageSource(name, email);
avatar = new AvatarImageSource(userId, name, email);
if (!_cache.TryAdd(key, avatar)
&&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.

View File

@@ -1,89 +0,0 @@
using System.Collections.Generic;
using Bit.App.Abstractions;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Enums;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CipherViewModelSwipeableItem : ISwipeableItem<CipherViewCellViewModel>
{
readonly Dictionary<CipherType, FontImageSource> _imageCache = new Dictionary<CipherType, FontImageSource>();
public bool CanSwipe(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return false;
}
return item.Cipher.Type == CipherType.Login
||
item.Cipher.Type == CipherType.Card
||
item.Cipher.Type == CipherType.SecureNote;
}
public Xamarin.Forms.Color GetBackgroundColor(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return ThemeManager.GetResourceColor("PrimaryColor");
}
if (item.Cipher.Type == CipherType.Login
&&
string.IsNullOrEmpty(item.Cipher.Login?.Password))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
if (item.Cipher.Type == CipherType.Card
&&
string.IsNullOrEmpty(item.Cipher.Card?.Number))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
if (item.Cipher.Type == CipherType.SecureNote
&&
string.IsNullOrEmpty(item.Cipher.Notes))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
return ThemeManager.GetResourceColor("PrimaryColor");
}
public FontImageSource GetSwipeIcon(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return null;
}
if (!_imageCache.TryGetValue(item.Cipher.Type, out var image))
{
image = new IconFontImageSource { Color = ThemeManager.GetResourceColor("BackgroundColor") };
switch (item.Cipher.Type)
{
case CipherType.Login:
image.Glyph = BitwardenIcons.Key;
break;
case CipherType.Card:
image.Glyph = BitwardenIcons.Hashtag;
break;
case CipherType.SecureNote:
image.Glyph = BitwardenIcons.Clone;
break;
default:
return null;
}
_imageCache.Add(item.Cipher.Type, image);
}
return image;
}
}
}

View File

@@ -1,19 +1,9 @@
using System.Windows.Input;
using Xamarin.Forms;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class ExtendedCollectionView : CollectionView
{
public static BindableProperty OnSwipeCommandProperty =
BindableProperty.Create(nameof(OnSwipeCommand), typeof(ICommand), typeof(ExtendedCollectionView));
public ICommand OnSwipeCommand
{
get => (ICommand)GetValue(OnSwipeCommandProperty);
set => SetValue(OnSwipeCommandProperty, value);
}
public string ExtraDataForLogging { get; set; }
}
}

View File

@@ -4,6 +4,5 @@ namespace Bit.App.Controls
{
public class ExtendedGrid : Grid
{
public bool ApplyRipple { get; set; } = true;
}
}

View File

@@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -16,6 +17,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

View File

@@ -1,20 +0,0 @@
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class IconFontImageSource : FontImageSource
{
public IconFontImageSource()
{
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "bwi-font";
break;
case Device.Android:
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Xamarin.Forms;
using Bit.App.Effects;
using Xamarin.Forms;
namespace Bit.App.Controls
{
@@ -17,6 +18,8 @@ namespace Bit.App.Controls
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
Effects.Add(new RemoveFontPaddingEffect());
}
}
}

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

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

View File

@@ -0,0 +1,13 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Effects
{
public class RemoveFontPaddingEffect : RoutingEffect
{
public RemoveFontPaddingEffect()
: base("Bitwarden.RemoveFontPaddingEffect")
{ }
}
}

View File

@@ -6,11 +6,12 @@ namespace Bit.App.Pages
{
private HintPageViewModel _vm;
public HintPage()
public HintPage(string email = null)
{
InitializeComponent();
_vm = BindingContext as HintPageViewModel;
_vm.Page = this;
_vm.Email = email;
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);

View File

@@ -15,6 +15,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IApiService _apiService;
private readonly ILogger _logger;
private string _email;
public HintPageViewModel()
{
@@ -34,7 +35,12 @@ namespace Bit.App.Pages
}
public ICommand SubmitCommand { get; }
public string Email { get; set; }
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
public async Task SubmitAsync()
{

View File

@@ -24,6 +24,7 @@
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
<ToolbarItem x:Name="_closeButton" Text="{u:I18n Close}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"/>
<ToolbarItem
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
@@ -32,30 +33,66 @@
<ContentPage.Resources>
<ResourceDictionary>
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="0" Padding="10, 5">
<StackLayout VerticalOptions="CenterAndExpand" Spacing="20">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center">
</Label>
<StackLayout Spacing="5">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n CreateAccount}"
Clicked="Register_Clicked" />
<Button Text="{u:I18n LogInSso}"
Clicked="LogInSso_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Margin="0,10,0,0"
Clicked="Cancel_Clicked" />
<u:InverseBoolConverter x:Key="inverseBool" />
<StackLayout x:Name="_mainLayout" x:Key="mainLayout" Spacing="30" Padding="20, 50, 20, 0">
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center"/>
<StackLayout
StyleClass="box-row">
<Label
Text="{u:I18n EmailAddress}"
StyleClass="box-label" />
<Entry
x:Name="_email"
Text="{Binding Email}"
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
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>
<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>
</ResourceDictionary>
</ContentPage.Resources>

View File

@@ -10,28 +10,35 @@ namespace Bit.App.Pages
{
public partial class HomePage : BaseContentPage
{
private bool _checkRememberedEmail;
private readonly HomeViewModel _vm;
private readonly AppOptions _appOptions;
private IBroadcasterService _broadcasterService;
public HomePage(AppOptions appOptions = null)
public HomePage(AppOptions appOptions = null, bool shouldCheckRememberEmail = true)
{
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_appOptions = appOptions;
InitializeComponent();
_vm = BindingContext as HomeViewModel;
_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.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
_vm.CloseAction = async () =>
{
await _accountListOverlay.HideAsync();
await Navigation.PopModalAsync();
};
UpdateLogo();
if (_appOptions?.IosExtension ?? false)
if (!_vm.ShowCancelButton)
{
_vm.ShowCancelButton = true;
ToolbarItems.Remove(_closeButton);
}
if (_appOptions?.HideAccountSwitcher ?? false)
{
ToolbarItems.Remove(_accountAvatar);
@@ -52,7 +59,7 @@ namespace Bit.App.Pages
if (!_appOptions?.HideAccountSwitcher ?? false)
{
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(false);
}
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
{
@@ -64,6 +71,8 @@ namespace Bit.App.Pages
});
}
});
_vm.CheckNavigateLoginStep();
}
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()
{
var page = new LoginPage(null, _appOptions);
var page = new LoginPage(_vm.Email, _appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
private void Register_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.StartRegisterAction();
}
}
private async Task StartRegisterAsync()
{
var page = new RegisterPage(this);

View File

@@ -1,8 +1,15 @@
using System;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -12,19 +19,37 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
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()
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
var logger = ServiceContainer.Resolve<ILogger>("logger");
_stateService = ServiceContainer.Resolve<IStateService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_logger = ServiceContainer.Resolve<ILogger>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
PageTitle = AppResources.Bitwarden;
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, logger)
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
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
@@ -33,11 +58,102 @@ namespace Bit.App.Pages
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 Action StartLoginAction { get; set; }
public Action StartRegisterAction { get; set; }
public Action StartSsoLoginAction { get; set; }
public Action StartEnvironmentAction { 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);
}
}
}
}

View File

@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.LoginPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
x:DataType="pages:LoginPageViewModel"
@@ -46,33 +47,13 @@
Order="Secondary" />
<ScrollView x:Name="_mainLayout" x:Key="mainLayout">
<StackLayout Spacing="20">
<StackLayout Spacing="0">
<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.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -81,6 +62,7 @@
<Label
Text="{u:I18n MasterPassword}"
StyleClass="box-label"
Padding="0, 10, 0, 0"
Grid.Row="0"
Grid.Column="0" />
<controls:MonoEntry
@@ -98,21 +80,60 @@
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Grid.Row="0"
Grid.Row="1"
Grid.Column="1"
Grid.RowSpan="2"
Grid.RowSpan="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}"
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>
</StackLayout>
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}"
<StackLayout Padding="10, 10">
<Button x:Name="_loginWithMasterPassword"
Text="{u:I18n LogInWithMasterPassword}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Clicked="Cancel_Clicked" />
<controls:IconLabelButton
HorizontalOptions="Fill"
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>
</ScrollView>

View File

@@ -3,7 +3,9 @@ using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -25,6 +27,7 @@ namespace Bit.App.Pages
_vm.Page = this;
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
_vm.UpdateTempPasswordAction =
() => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync());
_vm.CloseAction = async () =>
@@ -42,9 +45,6 @@ namespace Bit.App.Pages
_vm.Email = email;
MasterPasswordEntry = _masterPassword;
_email.ReturnType = ReturnType.Next;
_email.ReturnCommand = new Command(() => _masterPassword.Focus());
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_moreItem);
@@ -54,11 +54,6 @@ namespace Bit.App.Pages
ToolbarItems.Add(_getPasswordHint);
}
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
{
ToolbarItems.Add(_removeAccount);
}
if (_appOptions?.IosExtension ?? false)
{
_vm.ShowCancelButton = true;
@@ -78,16 +73,20 @@ namespace Bit.App.Pages
_mainContent.Content = _mainLayout;
_accountAvatar?.OnAppearing();
await _vm.InitAsync();
if (!_appOptions?.HideAccountSwitcher ?? false)
{
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
_vm.AvatarImageSource = await GetAvatarImageSourceAsync(_vm.EmailIsInSavedAccounts);
}
await _vm.InitAsync();
if (!_inputFocused)
{
RequestFocus(string.IsNullOrWhiteSpace(_vm.Email) ? _email : _masterPassword);
RequestFocus(_masterPassword);
_inputFocused = true;
}
if (Device.RuntimePlatform == Device.Android && !_vm.CanRemoveAccount)
{
ToolbarItems.Add(_removeAccount);
}
}
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)
{
if (DoOnce())
{
Navigation.PushModalAsync(new NavigationPage(new HintPage()));
_vm.ShowMasterPasswordHintAsync().FireAndForget();
}
}

View File

@@ -1,13 +1,18 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
@@ -25,12 +30,15 @@ namespace Bit.App.Pages
private readonly II18nService _i18nService;
private readonly IMessagingService _messagingService;
private readonly ILogger _logger;
private readonly IApiService _apiService;
private readonly IAppIdService _appIdService;
private readonly IAccountsManager _accountManager;
private bool _showPassword;
private bool _showCancelButton;
private string _email;
private string _masterPassword;
private bool _isEmailEnabled;
private bool _isKnownDevice;
public LoginPageViewModel()
{
@@ -43,6 +51,9 @@ namespace Bit.App.Pages
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_apiService = ServiceContainer.Resolve<IApiService>();
_appIdService = ServiceContainer.Resolve<IAppIdService>();
_accountManager = ServiceContainer.Resolve<IAccountsManager>();
PageTitle = AppResources.Bitwarden;
TogglePasswordCommand = new Command(TogglePassword);
@@ -76,7 +87,11 @@ namespace Bit.App.Pages
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
set => SetProperty(ref _email, value,
additionalPropertyNames: new string[]
{
nameof(LoggingInAsText)
});
}
public string MasterPassword
@@ -91,20 +106,29 @@ namespace Bit.App.Pages
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 Command LogInCommand { get; }
public Command TogglePasswordCommand { get; }
public ICommand MoreCommand { get; internal set; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
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 LogInSuccessAction { get; set; }
public Action UpdateTempPasswordAction { get; set; }
public Action StartSsoLoginAction { 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 IEnvironmentService environmentService => _environmentService;
protected override IDeviceActionService deviceActionService => _deviceActionService;
@@ -112,9 +136,22 @@ namespace Bit.App.Pages
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);
if (userEnvUrls?.Base == _environmentService.BaseUrl)
{
await PromptToSwitchToExistingAccountAsync(userId);
await _accountManager.PromptToSwitchToExistingAccountAsync(userId);
return;
}
}
@@ -170,7 +207,6 @@ namespace Bit.App.Pages
}
var response = await _authService.LogInAsync(Email, MasterPassword, _captchaToken);
await _stateService.SetRememberedEmailAsync(Email);
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
if (response.CaptchaNeeded)
@@ -216,19 +252,14 @@ namespace Bit.App.Pages
private async Task MoreAsync()
{
var buttons = IsEmailEnabled
var buttons = IsEmailEnabled || CanRemoveAccount
? new[] { AppResources.GetPasswordHint }
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
if (selection == AppResources.GetPasswordHint)
{
var hintNavigationPage = new NavigationPage(new HintPage());
if (IsIosExtension)
{
ThemeManager.ApplyResourcesTo(hintNavigationPage);
}
await Page.Navigation.PushModalAsync(hintNavigationPage);
await ShowMasterPasswordHintAsync();
}
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()
{
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(
AppResources.SwitchToAlreadyAddedAccountConfirmation,
AppResources.AccountAlreadyAdded, AppResources.Yes, AppResources.Cancel);
if (switchToAccount)
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _stateService.SetActiveUserAsync(userId);
_messagingService.Send("switchedAccount");
}
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
}
}

View File

@@ -14,10 +14,7 @@ namespace Bit.App.Pages
_vm.LoginRequest = loginPasswordlessDetails;
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem);
}
ToolbarItems.Add(_closeItem);
}
private async void Close_Clicked(object sender, System.EventArgs e)

View File

@@ -100,7 +100,7 @@ namespace Bit.App.Pages
private async Task UpdateRequestTime()
{
TriggerPropertyChanged(nameof(TimeOfRequestText));
if (DateTime.UtcNow > LoginRequest?.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
if (LoginRequest?.IsExpired ?? false)
{
StopRequestTimeUpdater();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
@@ -110,7 +110,7 @@ namespace Bit.App.Pages
private async Task PasswordlessLoginAsync(bool approveRequest)
{
if (LoginRequest.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
if (LoginRequest.IsExpired)
{
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
@@ -171,5 +171,7 @@ namespace Bit.App.Pages
public string DeviceType { get; set; }
public string IpAddress { get; set; }
public bool IsExpired => RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) < DateTime.UtcNow;
}
}

View File

@@ -129,7 +129,8 @@ namespace Bit.App.Pages
{
if (useCurrentActiveAccount)
{
return new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
return new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
}
return new AvatarImageSource();
}

View File

@@ -21,7 +21,7 @@
<u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
<ToolbarItem Text="{u:I18n Cancel}" Command="{Binding CloseCommand}" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Select}"
Clicked="Select_Clicked"

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Styles;
using Bit.Core.Abstractions;
@@ -18,11 +19,11 @@ namespace Bit.App.Pages
private readonly Action<string> _selectAction;
private readonly TabsPage _tabsPage;
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false, AppOptions appOptions = null)
{
_tabsPage = tabsPage;
InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
_vm = BindingContext as GeneratorPageViewModel;
_vm.Page = this;
_fromTabPage = fromTabPage;
@@ -31,6 +32,7 @@ namespace Bit.App.Pages
_vm.IsUsername = isUsernameGenerator;
_vm.EmailWebsite = emailWebsite;
_vm.EditMode = editMode;
_vm.IosExtension = appOptions?.IosExtension ?? false;
var isIos = Device.RuntimePlatform == Device.iOS;
if (selectAction != null)
{
@@ -134,14 +136,6 @@ namespace Bit.App.Pages
await _vm.SliderChangedAsync();
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
public override async Task UpdateOnThemeChanged()
{
await base.UpdateOnThemeChanged();

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@@ -21,6 +22,7 @@ namespace Bit.App.Pages
private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
private readonly IDeviceActionService _deviceActionService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
@@ -59,6 +61,7 @@ namespace Bit.App.Pages
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
@@ -89,8 +92,9 @@ namespace Bit.App.Pages
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
CloseCommand = new AsyncCommand(CloseAsync, onException: ex => _logger.Value.Exception(ex), allowsMultipleExecutions: false);
}
public List<GeneratorType> GeneratorTypeOptions { get; set; }
@@ -104,6 +108,7 @@ namespace Bit.App.Pages
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public ICommand CloseCommand { get; set; }
public string Password
{
@@ -140,6 +145,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _isUsername, value);
}
public bool IosExtension { get; set; }
public bool ShowTypePicker
{
get => _showTypePicker;
@@ -606,6 +613,7 @@ namespace Bit.App.Pages
LoadFromOptions();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
await _tokenService.PrepareTokenForDecodingAsync();
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
_usernameOptions.EmailWebsite = EmailWebsite;
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
@@ -681,6 +689,7 @@ namespace Bit.App.Pages
return;
}
_usernameOptions.EmailWebsite = EmailWebsite;
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
@@ -729,6 +738,18 @@ namespace Bit.App.Pages
}
}
public async Task CloseAsync()
{
if (IosExtension)
{
_deviceActionService.CloseExtensionPopUp();
}
else
{
await Page.Navigation.PopModalAsync();
}
}
private void LoadFromOptions()
{
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
@@ -765,6 +786,7 @@ namespace Bit.App.Pages
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
TriggerPropertyChanged(nameof(EmailWebsite));
}
private void SetOptions()

View File

@@ -90,6 +90,7 @@ namespace Bit.App.Pages
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
await _syncService.SyncPasswordlessLoginRequestsAsync();
var success = await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
if (success)

View File

@@ -6,7 +6,6 @@ using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Helpers;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@@ -27,7 +26,6 @@ namespace Bit.App.Pages
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IMessagingService _messagingService;
private readonly ICipherHelper _cipherHelper;
private readonly ILogger _logger;
private bool _showNoData;
@@ -44,7 +42,6 @@ namespace Bit.App.Pages
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
@@ -185,7 +182,7 @@ namespace Bit.App.Pages
}
if (_deviceActionService.SystemMajorVersion() < 21)
{
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
}
else
{
@@ -246,7 +243,7 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
}
}
}

View File

@@ -5,7 +5,6 @@ using System.Threading;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities.Helpers;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -26,7 +25,6 @@ namespace Bit.App.Pages
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly ICipherHelper _cipherHelper;
private CancellationTokenSource _searchCancellationTokenSource;
private readonly ILogger _logger;
@@ -45,7 +43,6 @@ namespace Bit.App.Pages
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Ciphers = new ExtendedObservableCollection<CipherView>();
@@ -197,7 +194,7 @@ namespace Bit.App.Pages
}
if (_deviceActionService.SystemMajorVersion() < 21)
{
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
}
else
{
@@ -223,7 +220,7 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
}
}
}

View File

@@ -33,9 +33,7 @@
<ContentPage.Resources>
<ResourceDictionary>
<xct:IsNotNullOrEmptyConverter x:Key="isNotNullOrEmptyConverter" />
<u:CipherTypeToSwipeActionGlyphConverter x:Key="cipherTypeToSwipeActionGlyphConverter" />
<u:CipherToSwipeBackgroundColor x:Key="cipherToSwipeBackgroundColor" />
<u:InverseBoolConverter x:Key="inverseBool" />
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
Clicked="Sync_Clicked" Order="Secondary" />
@@ -55,27 +53,6 @@
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate>
<DataTemplate x:Key="swipeableCipherTemplate"
x:DataType="pages:GroupingsPageListItem">
<SwipeView Threshold="{Binding Source={x:Reference _page}, Path=SwipeThreshold}">
<SwipeView.LeftItems>
<SwipeItems Mode="Execute">
<SwipeItem
BackgroundColor="{Binding Cipher, Converter={StaticResource cipherToSwipeBackgroundColor}}"
Command="{Binding Source={x:Reference _page}, Path=BindingContext.SwipeItemActionCommand}"
CommandParameter="{Binding .}"
IconImageSource="{Binding Cipher.Type, Converter={StaticResource cipherTypeToSwipeActionGlyphConverter}}">
</SwipeItem>
</SwipeItems>
</SwipeView.LeftItems>
<controls:CipherViewCell
Cipher="{Binding Cipher}"
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
ApplyRipple="False"
BackgroundColor="{DynamicResource BackgroundColor}"/>
</SwipeView>
</DataTemplate>
<DataTemplate x:Key="authenticatorTemplate"
x:DataType="pages:GroupingsPageTOTPListItem">
@@ -136,9 +113,8 @@
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
HeaderTemplate="{StaticResource headerTemplate}"
CipherTemplate="{StaticResource cipherTemplate}"
GroupTemplate="{StaticResource groupTemplate}"
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
SwipeableCipherTemplate="{StaticResource swipeableCipherTemplate}" />
GroupTemplate="{StaticResource groupTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
<StackLayout
@@ -189,8 +165,7 @@
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform"
ExtraDataForLogging="Groupings Page"
OnSwipeCommand="{Binding SwipeItemActionCommand}"/>
ExtraDataForLogging="Groupings Page" />
</RefreshView>
</StackLayout>
</ResourceDictionary>

View File

@@ -28,19 +28,6 @@ namespace Bit.App.Pages
private PreviousPageInfo _previousPage;
double _swipeThreshold;
public double SwipeThreshold
{
get
{
if (_swipeThreshold == default)
{
_swipeThreshold = (Content?.Width ?? 500) / 2;
}
return _swipeThreshold;
}
}
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)

View File

@@ -7,7 +7,6 @@ namespace Bit.App.Pages
public DataTemplate HeaderTemplate { get; set; }
public DataTemplate CipherTemplate { get; set; }
public DataTemplate GroupTemplate { get; set; }
public DataTemplate SwipeableCipherTemplate { get; set; }
public DataTemplate AuthenticatorTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
@@ -24,16 +23,7 @@ namespace Bit.App.Pages
if (item is GroupingsPageListItem listItem)
{
if (listItem.Cipher is null)
{
return GroupTemplate;
}
return CipherTemplate;
//return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
// ? SwipeableCipherTemplate
// : CipherTemplate;
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
}
return null;

View File

@@ -8,7 +8,6 @@ using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Helpers;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
@@ -51,9 +50,9 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly ICipherHelper _cipherHelper;
private readonly ILogger _logger;
public GroupingsPageViewModel()
@@ -67,9 +66,9 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Loading = true;
@@ -80,7 +79,6 @@ namespace Bit.App.Pages
await LoadAsync();
});
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
SwipeItemActionCommand = new AsyncCommand<IGroupingsPageListItem>(SwipeItemActionAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
@@ -171,8 +169,6 @@ namespace Bit.App.Pages
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public IAsyncCommand<IGroupingsPageListItem> SwipeItemActionCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -193,6 +189,7 @@ namespace Bit.App.Pages
if (await _stateService.GetSyncOnRefreshAsync() && Refreshing && !SyncRefreshing)
{
SyncRefreshing = true;
await _syncService.SyncPasswordlessLoginRequestsAsync();
await _syncService.FullSyncAsync(false);
return;
}
@@ -717,43 +714,7 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
}
private async Task SwipeItemActionAsync(IGroupingsPageListItem listItem)
{
if (listItem is GroupingsPageListItem groupPageListItem && groupPageListItem.Cipher is CipherView cipher)
{
switch (cipher.Type)
{
case CipherType.Login:
if (string.IsNullOrEmpty(cipher.Login?.Password) || !await _cipherHelper.CopyPasswordAsync(cipher))
{
return;
}
break;
case CipherType.Card:
if (!await _cipherHelper.CopyCardNumberAsync(cipher))
{
return;
}
break;
case CipherType.SecureNote:
await _cipherHelper.CopyNotesAsync(cipher);
break;
default:
_logger.Error($"The cipher type {cipher.Type} does not have any swipe action associated");
return;
}
try
{
Xamarin.Essentials.Vibration.Vibrate();
}
catch (Xamarin.Essentials.FeatureNotSupportedException)
{
}
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
}
}
}

View File

@@ -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>
/// Looks up a localized string similar to Get your master password hint.
/// </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>
/// Looks up a localized string similar to Log In.
/// </summary>
@@ -3434,10 +3452,11 @@ namespace Bit.App.Resources {
}
/// <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>
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount
{
public static string LoginAttemptFromXDoYouWantToSwitchToThisAccount {
get {
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>
/// Looks up a localized string similar to Log out.
/// </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>
/// Looks up a localized string similar to New custom field.
/// </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>
/// Looks up a localized string similar to This login does not have a username or password configured..
/// </summary>

View File

@@ -2319,64 +2319,64 @@
<value>هل أنت متأكد من أنك تريد تمكين التقاط الشاشة؟</value>
</data>
<data name="LogInRequested" xml:space="preserve">
<value>Login requested</value>
<value>طلب تسجيل الدخول</value>
</data>
<data name="AreYouTryingToLogIn" xml:space="preserve">
<value>Are you trying to log in?</value>
<value>هل تحاول تسجيل الدخول؟</value>
</data>
<data name="LogInAttemptByXOnY" xml:space="preserve">
<value>Login attempt by {0} on {1}</value>
<value>محاولة تسجيل الدخول بواسطة {0} في {1}</value>
</data>
<data name="DeviceType" xml:space="preserve">
<value>Device type</value>
<value>نوع الجهاز</value>
</data>
<data name="IpAddress" xml:space="preserve">
<value>IP address</value>
<value>عنوان IP</value>
</data>
<data name="Time" xml:space="preserve">
<value>Time</value>
<value>الوقت</value>
</data>
<data name="Near" xml:space="preserve">
<value>Near</value>
<value>قريب</value>
</data>
<data name="ConfirmLogIn" xml:space="preserve">
<value>Confirm login</value>
<value>تأكيد تسجيل الدخول</value>
</data>
<data name="DenyLogIn" xml:space="preserve">
<value>Deny login</value>
<value>رفض تسجيل الدخول</value>
</data>
<data name="JustNow" xml:space="preserve">
<value>Just now</value>
<value>للتو</value>
</data>
<data name="XMinutesAgo" xml:space="preserve">
<value>{0} minutes ago</value>
<value>منذ {0} دقائق</value>
</data>
<data name="LogInAccepted" xml:space="preserve">
<value>Login confirmed</value>
<value>تم تأكيد تسجيل الدخول</value>
</data>
<data name="LogInDenied" xml:space="preserve">
<value>Login denied</value>
<value>تم رفض تسجيل الدخول</value>
</data>
<data name="ApproveLoginRequests" xml:space="preserve">
<value>Approve login requests</value>
<value>الموافقة على طلبات تسجيل الدخول</value>
</data>
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
<value>Use this device to approve login requests made from other devices.</value>
<value>استخدم هذا الجهاز للموافقة على طلبات تسجيل الدخول من الأجهزة الأخرى.</value>
</data>
<data name="AllowNotifications" xml:space="preserve">
<value>Allow notifications</value>
<value>السماح بالإشعارات</value>
</data>
<data name="ReceivePushNotificationsForNewLoginRequests" xml:space="preserve">
<value>Receive push notifications for new login requests</value>
<value>تلقي إشعارات دفع لطلبات تسجيل الدخول الجديدة</value>
</data>
<data name="NoThanks" xml:space="preserve">
<value>No thanks</value>
<value>ﻻ، شكرًا</value>
</data>
<data name="ConfimLogInAttempForX" xml:space="preserve">
<value>Confirm login attempt for {0}</value>
<value>تأكيد محاولة تسجيل الدخول لـ {0}</value>
</data>
<data name="AllNotifications" xml:space="preserve">
<value>All notifications</value>
<value>جميع الإشعارات</value>
</data>
<data name="PasswordType" xml:space="preserve">
<value>نوع كلمة المرور</value>
@@ -2454,23 +2454,23 @@
<value>عشوائي</value>
</data>
<data name="AccessibilityServiceDisclosure" xml:space="preserve">
<value>Accessibility Service Disclosure</value>
<value>كشف خدمة إمكانية الوصول</value>
</data>
<data name="AccessibilityDisclosureText" xml:space="preserve">
<value>Bitwarden uses the Accessibility Service to search for login fields in apps and websites, then establish the appropriate field IDs for entering a username &amp; password when a match for the app or site is found. We do not store any of the information presented to us by the service, nor do we make any attempt to control any on-screen elements beyond text entry of credentials.</value>
<value>Bitwarden يستخدم خدمة إمكانية الوصول للبحث عن حقول تسجيل الدخول في التطبيقات ومواقع الويب، ثم يقوم بإنشاء معرفات الحقل المناسب لإدخال اسم المستخدم وكلمة المرور عند العثور على تطابق للتطبيق أو الموقع. ونحن لا نخزن أيا من المعلومات التي قدمتها لنا الخدمة، كما أننا لا نحاول السيطرة على أي عناصر على الشاشة تتجاوز إدخال نصوص وثائق التفويض.</value>
</data>
<data name="Accept" xml:space="preserve">
<value>Accept</value>
<value>قبول</value>
</data>
<data name="Decline" xml:space="preserve">
<value>Decline</value>
<value>رفض</value>
</data>
<data name="LoginRequestHasAlreadyExpired" xml:space="preserve">
<value>Login request has already expired.</value>
<value>انتهت صلاحية طلب تسجيل الدخول.</value>
</data>
<data name="LoginAttemptFromXDoYouWantToSwitchToThisAccount" xml:space="preserve">
<value>Login attempt from:
<value>محاولة تسجيل الدخول من:
{0}
Do you want to switch to this account?</value>
هل تريد التبديل إلى هذا الحساب؟</value>
</data>
</root>

View File

@@ -273,13 +273,13 @@
<comment>The log out button text (verb).</comment>
</data>
<data name="LogoutConfirmation" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выйсці?</value>
<value>Вы сапраўды хочаце выйсці?</value>
</data>
<data name="RemoveAccount" xml:space="preserve">
<value>Выдаліць уліковы запіс</value>
</data>
<data name="RemoveAccountConfirmation" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выдаліць уліковы запіс?</value>
<value>Вы сапраўды хочаце выдаліць уліковы запіс?</value>
</data>
<data name="AccountAlreadyAdded" xml:space="preserve">
<value>Уліковы запіс ужо дададзены</value>
@@ -559,7 +559,7 @@
<value>Дзеянне пасля заканчэння часу чакання сховішча</value>
</data>
<data name="VaultTimeoutLogOutConfirmation" xml:space="preserve">
<value>Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыю праз інтэрнэт пасля завяршэння часу чакання. Вы ўпэўнены, што хочаце выкарыстоўваць гэты параметр?</value>
<value>Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыю праз інтэрнэт пасля завяршэння часу чакання. Вы сапраўды хочаце выкарыстоўваць гэты параметр?</value>
</data>
<data name="LoggingIn" xml:space="preserve">
<value>Уваход...</value>
@@ -644,7 +644,7 @@
<value>Мы адправілі вам на электронную пошту падказку да асноўнага пароля.</value>
</data>
<data name="PasswordOverrideAlert" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце перазапісаць бягучы пароль?</value>
<value>Вы сапраўды хочаце перазапісаць бягучы пароль?</value>
</data>
<data name="PushNotificationAlert" xml:space="preserve">
<value>Bitwarden дазваляе аўтаматычна сінхранізаваць сховішча пры дапамозе push-апавяшчэнняў. Для максімальнай зручнасці, выберыце "Дазволіць" пры з'яўленні прапановы ўключыць push-апавяшчэнні.</value>
@@ -793,7 +793,7 @@
<value>Вы сапраўды хочаце аўтазапоўніць або паглядзець гэты элемент?</value>
</data>
<data name="BitwardenAutofillServiceMatchConfirm" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выкарыстоўваць для аўтазапаўнення гэты элемент? Ён не дакладна адпавядае "{0}".</value>
<value>Вы сапраўды хочаце выкарыстоўваць для аўтазапаўнення гэты элемент? Ён не дакладна адпавядае "{0}".</value>
</data>
<data name="MatchingItems" xml:space="preserve">
<value>Адпаведныя элементы</value>
@@ -883,7 +883,7 @@
<comment>Message shown when downloading a file</comment>
</data>
<data name="AttachmentLargeWarning" xml:space="preserve">
<value>Гэта далучэнне мае памер {0}. Вы ўпэўнены, што хочаце спампаваць яго на сваю прыладу?</value>
<value>Гэта далучэнне мае памер {0}. Вы сапраўды хочаце спампаваць яго на сваю прыладу?</value>
<comment>The placeholder will show the file size of the attachment. Ex "25 MB"</comment>
</data>
<data name="AuthenticatorKey" xml:space="preserve">
@@ -1561,7 +1561,7 @@
<value>Выхад</value>
</data>
<data name="ExitConfirmation" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выйсці з Bitwarden?</value>
<value>Вы сапраўды хочаце выйсці з Bitwarden?</value>
</data>
<data name="PINRequireMasterPasswordRestart" xml:space="preserve">
<value>Вы сапраўды хочаце, каб пасля перазапуску праграма патрабавала асноўны пароль для разблакіроўкі?</value>
@@ -1963,7 +1963,7 @@
<value>Выдаліць пароль</value>
</data>
<data name="AreYouSureRemoveSendPassword" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выдаліць пароль?</value>
<value>Вы сапраўды хочаце выдаліць пароль?</value>
</data>
<data name="RemovingSendPassword" xml:space="preserve">
<value>Выдаленне пароля</value>
@@ -2010,7 +2010,7 @@
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AreYouSureDeleteSend" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце выдаліць гэты Send?</value>
<value>Вы сапраўды хочаце выдаліць гэты Send?</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendDeleted" xml:space="preserve">
@@ -2176,7 +2176,7 @@
<value>Выдаленне вашага ўліковага запісу з'яўляецца незваротным дзеяннем</value>
</data>
<data name="DeleteAccountExplanation" xml:space="preserve">
<value>Ваш уліковы запіс і ўсе даныя сховішча будуць выдалены без магчымасці аднаўлення. Вы ўпэўнены, што хочаце працягнуць?</value>
<value>Ваш уліковы запіс і ўсе даныя сховішча будуць выдалены без магчымасці аднаўлення. Вы сапраўды хочаце працягнуць?</value>
</data>
<data name="DeletingYourAccount" xml:space="preserve">
<value>Выдаленне вашага ўліковага запісу</value>
@@ -2315,7 +2315,7 @@
<value>Дазволіць здымкі экрана</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце дазволіць здымкі экрана?</value>
<value>Вы сапраўды хочаце дазволіць здымкі экрана?</value>
</data>
<data name="LogInRequested" xml:space="preserve">
<value>Выкананы запыт уваходу</value>
@@ -2426,7 +2426,7 @@
<value>Токен доступу да API</value>
</data>
<data name="AreYouSureYouWantToOverwriteTheCurrentUsername" xml:space="preserve">
<value>Вы ўпэўнены, што хочаце перазапісаць бягучае імя карыстальніка?</value>
<value>Вы сапраўды хочаце перазапісаць бягучае імя карыстальніка?</value>
</data>
<data name="GenerateUsername" xml:space="preserve">
<value>Генерыраваць імя карыстальніка</value>

View File

@@ -919,7 +919,7 @@ Načtení proběhne automaticky.</value>
<value>Zkopírujte si TOTP ověřovací kód do schránky, pokud používáte autentizační klíč k přihlašování, před použitím automatického vyplňování.</value>
</data>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>Copy TOTP automatically</value>
<value>Automaticky kopírovat TOTP</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>Pro použití této funkce je potřebné prémiové členství.</value>
@@ -1976,7 +1976,7 @@ Načtení proběhne automaticky.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="DisableSend" xml:space="preserve">
<value>Zakažte tento Send, aby k němu nikdo neměl přístup.</value>
<value>Vypnout tento Send, aby k němu nikdo neměl přístup</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="NoSends" xml:space="preserve">
@@ -2044,7 +2044,7 @@ Načtení proběhne automaticky.</value>
<value>Vlastní</value>
</data>
<data name="ShareOnSave" xml:space="preserve">
<value>Sdílet tento Send po uložení.</value>
<value>Sdílet tento Send po uložení</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendDisabledWarning" xml:space="preserve">
@@ -2056,7 +2056,7 @@ Načtení proběhne automaticky.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="HideEmail" xml:space="preserve">
<value>Skrýt mou e-mailovou adresu před příjemci.</value>
<value>Skrýt mou e-mailovou adresu před příjemci</value>
</data>
<data name="SendOptionsPolicyInEffect" xml:space="preserve">
<value>Jedna nebo více zásad organizace ovlivňuje nastavení Send.</value>
@@ -2092,7 +2092,7 @@ Načtení proběhne automaticky.</value>
<value>Aktualizovat hlavní heslo</value>
</data>
<data name="UpdateMasterPasswordWarning" xml:space="preserve">
<value>Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</value>
<value>Administrátor v organizaci nedávno změnil vaše hlavní heslo. Pro přístup k trezoru jej nyní musíte změnit. Pokračování vás odhlásí z vaší aktuální relace a bude nutné se znovu přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny.</value>
</data>
<data name="UpdatingPassword" xml:space="preserve">
<value>Aktualizace hesla</value>
@@ -2218,10 +2218,10 @@ Načtení proběhne automaticky.</value>
<value>Zadejte ověřovací kód, který byl odeslán na %@</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Odeslat protokoly o pádech</value>
<value>Odesílat záznamy o pádech</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
<value>Pomozte zlepšovat stabilitu aplikace odesíláním hlášení o selháních.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Options are expanded, tap to collapse.</value>
@@ -2251,19 +2251,19 @@ Načtení proběhne automaticky.</value>
<value>Heslo není viditelné, klepněte pro zobrazení.</value>
</data>
<data name="FilterByVault" xml:space="preserve">
<value>Filter items by vault</value>
<value>Filtrovat položky podle trezoru</value>
</data>
<data name="AllVaults" xml:space="preserve">
<value>All vaults</value>
<value>Všechny trezory</value>
</data>
<data name="Vaults" xml:space="preserve">
<value>Vaults</value>
<value>Trezory</value>
</data>
<data name="VaultFilterDescription" xml:space="preserve">
<value>Vault: {0}</value>
<value>Trezor: {0}</value>
</data>
<data name="All" xml:space="preserve">
<value>All</value>
<value>Všechny</value>
</data>
<data name="Totp" xml:space="preserve">
<value>TOTP</value>
@@ -2290,10 +2290,10 @@ Načtení proběhne automaticky.</value>
<value>Enter key manually</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>Add TOTP</value>
<value>Přidat TOTP</value>
</data>
<data name="SetupTotp" xml:space="preserve">
<value>Set up TOTP</value>
<value>Nastavit TOTP</value>
</data>
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
<value>Once the key is successfully entered,
@@ -2303,7 +2303,7 @@ select Add TOTP to store the key safely</value>
<value></value>
</data>
<data name="NeverLockWarning" xml:space="preserve">
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
<value>Nastavení zámku na „Nikdy“ ponechá váš trezor k dispozici komukoliv s přístupem k vašemu zařízení. Používáte-li tuto možnost, měli byste zajistit, aby vaše zařízení bylo náležitě chráněno.</value>
</data>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
@@ -2387,13 +2387,13 @@ select Add TOTP to store the key safely</value>
<value>Typ uživatelského jména</value>
</data>
<data name="PlusAddressedEmail" xml:space="preserve">
<value>Plus addressed email</value>
<value>E-mailová adresa s plusem</value>
</data>
<data name="CatchAllEmail" xml:space="preserve">
<value>Catch-all email</value>
</data>
<data name="ForwardedEmailAlias" xml:space="preserve">
<value>Forwarded email alias</value>
<value>Alias přeposílaného e-mailu</value>
</data>
<data name="RandomWord" xml:space="preserve">
<value>Náhodné slovo</value>
@@ -2447,7 +2447,7 @@ select Add TOTP to store the key safely</value>
<value>Use your domain's configured catch-all inbox.</value>
</data>
<data name="ForwardedEmailDescription" xml:space="preserve">
<value>Generate an email alias with an external forwarding service.</value>
<value>Vygeneruje alias e-mailu pomocí externí přeposílací služby.</value>
</data>
<data name="Random" xml:space="preserve">
<value>Náhodně</value>

View File

@@ -1278,11 +1278,11 @@ Koodi luetaan automaattisesti.</value>
<value>Esteettömyyspalvelu voi olla hyödyllinen sellaisten sovellusten kanssa, jotka eivät tue tavallista automaattisen täytön palvelua.</value>
</data>
<data name="DatePasswordUpdated" xml:space="preserve">
<value>Salasana vaihdettiin</value>
<value>Salasana päivitettiin</value>
<comment>ex. Date this password was updated</comment>
</data>
<data name="DateUpdated" xml:space="preserve">
<value>Päivitetty</value>
<value>Päivitettiin</value>
<comment>ex. Date this item was updated</comment>
</data>
<data name="AutofillActivated" xml:space="preserve">
@@ -1701,7 +1701,7 @@ Koodi luetaan automaattisesti.</value>
<value>Liitteen tallennuksessa oli ongelma. Jos ongelma jatkuu, voit tehdä tallennuksen verkkoholvin kautta.</value>
</data>
<data name="SaveAttachmentSuccess" xml:space="preserve">
<value>Tiedostoliitteen tallennus onnistui</value>
<value>Tiedostoliite tallennettiin</value>
</data>
<data name="AutofillTileAccessibilityRequired" xml:space="preserve">
<value>Ota "Automaattisen täytön esteettömyyspalvelu" käyttöön Bitwardenin asetuksista käyttääksesi automaattisen täytön pikavalintapalkkia.</value>
@@ -1861,7 +1861,7 @@ Koodi luetaan automaattisesti.</value>
<value>Jos käytössä, esteettömyyspalvelu näyttää pikavalintapalkin laajentaakseen automaattisen täytön palvelun toiminnan kattamaan myös vanhemmat sovellukset, jotka eivät tue Android Autofill Framework -rajapintaa.</value>
</data>
<data name="PersonalOwnershipSubmitError" xml:space="preserve">
<value>Yrityksen asettaman käytännön johdosta kohteiden tallennus henkilökohtaiseen holviin ei ole mahdollista. Muuta omistusasetus organisaatiolle ja valitse käytettävissä olevista kokoelmista.</value>
<value>Yrityskäytännön johdosta kohteiden tallennus henkilökohtaiseen holviin ei ole mahdollista. Muuta omistusasetus organisaatiolle ja valitse käytettävissä olevista kokoelmista.</value>
</data>
<data name="PersonalOwnershipPolicyInEffect" xml:space="preserve">
<value>Organisaatiokäytäntö vaikuttaa omistajuusvalintoihisi.</value>
@@ -2048,7 +2048,7 @@ Koodi luetaan automaattisesti.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendDisabledWarning" xml:space="preserve">
<value>Yrityksen käytännön vuoksi voit poistaa vain olemassa olevan Sendin.</value>
<value>Yrityskäytännön vuoksi voit poistaa vain olemassa olevan Sendin.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AboutSend" xml:space="preserve">
@@ -2086,7 +2086,7 @@ Koodi luetaan automaattisesti.</value>
<value>Captcha-vahvistus epäonnistui. Yritä uudelleen.</value>
</data>
<data name="UpdatedMasterPassword" xml:space="preserve">
<value>Pääsalasana vaihdettiin</value>
<value>Pääsalasana päivitettiin</value>
</data>
<data name="UpdateMasterPassword" xml:space="preserve">
<value>Vaihda pääsalasana</value>

View File

@@ -1576,7 +1576,7 @@ La numérisation se fera automatiquement.</value>
<comment>'Nord' is the name of a specific color scheme. It should not be translated.</comment>
</data>
<data name="SolarizedDark" xml:space="preserve">
<value>Solarized Dark</value>
<value>Sombre "Solarized"</value>
<comment>'Solarized Dark' is the name of a specific color scheme. It should not be translated.</comment>
</data>
<data name="AutofillBlockedUris" xml:space="preserve">
@@ -2470,6 +2470,6 @@ sélectionnez Ajouter TOTP pour stocker la clé en toute sécurité</value>
<data name="LoginAttemptFromXDoYouWantToSwitchToThisAccount" xml:space="preserve">
<value>Tentative de connexion depuis :
{0}
Voulez-vous commuter sur ce compte ?</value>
Voulez-vous basculer vers ce compte ?</value>
</data>
</root>

View File

@@ -342,7 +342,7 @@
<comment>Reveal a hidden value (password).</comment>
</data>
<data name="ItemDeleted" xml:space="preserve">
<value>Item is verwijderd.</value>
<value>Item is verwijderd</value>
<comment>Confirmation message after successfully deleting a login.</comment>
</data>
<data name="Submit" xml:space="preserve">
@@ -2218,10 +2218,10 @@ Het scannen gebeurt automatisch.</value>
<value>Voer de verificatiecode in die we naar je e-mail is gestuurd</value>
</data>
<data name="SubmitCrashLogs" xml:space="preserve">
<value>Submit crash logs</value>
<value>Crashes rapporteren</value>
</data>
<data name="SubmitCrashLogsDescription" xml:space="preserve">
<value>Help Bitwarden improve app stability by submitting crash reports.</value>
<value>Help Bitwarden de app-stabiliteit te verbeteren door crashrapporten te versturen.</value>
</data>
<data name="OptionsExpanded" xml:space="preserve">
<value>Opties zijn uitgebreid, tik om te storten.</value>
@@ -2306,10 +2306,10 @@ kies je TOTP toevoegen om de sleutel veilig op te slaan</value>
<value>De vergrendelingsoptie "Nooit" houdt je kluis beschikbaar voor iedereen met toegang tot je apparaat. Als je deze optie gebruikt, moet je ervoor zorgen dat je je apparaat naar behoren beschermt.</value>
</data>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
<value>Een of meerdere van de ingevoerde URL's zijn ongeldig. Controleer en probeer opnieuw op te slaan.</value>
</data>
<data name="GenericErrorMessage" xml:space="preserve">
<value>We were unable to process your request. Please try again or contact us.</value>
<value>We kunnen uw aanvraag niet verwerken. Probeer het opnieuw of neem contact met ons op.</value>
</data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Schermopname toestaan</value>
@@ -2453,23 +2453,23 @@ kies je TOTP toevoegen om de sleutel veilig op te slaan</value>
<value>Willekeurig</value>
</data>
<data name="AccessibilityServiceDisclosure" xml:space="preserve">
<value>Accessibility Service Disclosure</value>
<value>Toegankelijksheidsservice-melding</value>
</data>
<data name="AccessibilityDisclosureText" xml:space="preserve">
<value>Bitwarden uses the Accessibility Service to search for login fields in apps and websites, then establish the appropriate field IDs for entering a username &amp; password when a match for the app or site is found. We do not store any of the information presented to us by the service, nor do we make any attempt to control any on-screen elements beyond text entry of credentials.</value>
<value>Bitwarden gebruikt de toegankelijkheidsservice om inlogvelden in apps en op websites te vinden, de juiste veld ID's te bepalen om de gebruikersnaam en het wachtwoord in te voeren wanneer een bijpassend paar gevonden is voor de app of site. We slaan geen informatie op die de service ons levert, ook controleren we geen elementen op het scherm naast de tekstinvoer van inloggegevens.</value>
</data>
<data name="Accept" xml:space="preserve">
<value>Accept</value>
<value>Accepteren</value>
</data>
<data name="Decline" xml:space="preserve">
<value>Decline</value>
<value>Weigeren</value>
</data>
<data name="LoginRequestHasAlreadyExpired" xml:space="preserve">
<value>Inlogverzoek is al verlopen.</value>
</data>
<data name="LoginAttemptFromXDoYouWantToSwitchToThisAccount" xml:space="preserve">
<value>Login attempt from:
<value>Inlogpoging vanaf:
{0}
Do you want to switch to this account?</value>
Wilt u naar dit account wisselen?</value>
</data>
</root>

View File

@@ -152,11 +152,11 @@
<comment>Copy some value to your clipboard.</comment>
</data>
<data name="CopyPassword" xml:space="preserve">
<value>Skopiuj hasło</value>
<value>Kopiuj hasło</value>
<comment>The button text that allows a user to copy the login's password to their clipboard.</comment>
</data>
<data name="CopyUsername" xml:space="preserve">
<value>Skopiuj nazwę użytkownika</value>
<value>Kopiuj nazwę użytkownika</value>
<comment>The button text that allows a user to copy the login's username to their clipboard.</comment>
</data>
<data name="Credits" xml:space="preserve">
@@ -179,7 +179,7 @@
<value>Edycja</value>
</data>
<data name="EditFolder" xml:space="preserve">
<value>Edytuj Folder</value>
<value>Edytuj folder</value>
</data>
<data name="Email" xml:space="preserve">
<value>Adres e-mail</value>
@@ -229,7 +229,7 @@
<value>Foldery</value>
</data>
<data name="FolderUpdated" xml:space="preserve">
<value>Folder zaktualizowany.</value>
<value>Folder został zapisany</value>
</data>
<data name="GoToWebsite" xml:space="preserve">
<value>Przejdź do strony</value>
@@ -247,7 +247,7 @@
<comment>Description message for the alert when internet connection is required to continue.</comment>
</data>
<data name="InternetConnectionRequiredTitle" xml:space="preserve">
<value>Wymagane połączenie z Internetem</value>
<value>Połączenie z Internetem jest wymagane</value>
<comment>Title for the alert when internet connection is required to continue.</comment>
</data>
<data name="InvalidMasterPassword" xml:space="preserve">
@@ -296,7 +296,7 @@
<comment>Text to define that there are more options things to see.</comment>
</data>
<data name="MyVault" xml:space="preserve">
<value>Sejf</value>
<value>Mój sejf</value>
<comment>The title for the vault page.</comment>
</data>
<data name="Authenticator" xml:space="preserve">
@@ -342,7 +342,7 @@
<comment>Reveal a hidden value (password).</comment>
</data>
<data name="ItemDeleted" xml:space="preserve">
<value>Element został usunięty.</value>
<value>Element został usunięty</value>
<comment>Confirmation message after successfully deleting a login.</comment>
</data>
<data name="Submit" xml:space="preserve">
@@ -364,7 +364,7 @@
<comment>Label for a uri/url.</comment>
</data>
<data name="UseFingerprintToUnlock" xml:space="preserve">
<value>Odblokuj za pomocą odcisku palca</value>
<value>Odblokuj odciskiem palca</value>
</data>
<data name="Username" xml:space="preserve">
<value>Nazwa użytkownika</value>
@@ -514,7 +514,7 @@
<value>Odcisk palca</value>
</data>
<data name="GeneratePassword" xml:space="preserve">
<value>Generuj hasło</value>
<value>Wygeneruj hasło</value>
</data>
<data name="GetPasswordHint" xml:space="preserve">
<value>Uzyskaj podpowiedź do hasła głównego</value>
@@ -604,7 +604,7 @@
<value>Nigdy</value>
</data>
<data name="NewItemCreated" xml:space="preserve">
<value>Element został utworzony.</value>
<value>Element został dodany</value>
</data>
<data name="NoFavorites" xml:space="preserve">
<value>Brak ulubionych w sejfie.</value>
@@ -657,7 +657,7 @@
<value>Wesprzyj nas pozytywną opinią!</value>
</data>
<data name="RegeneratePassword" xml:space="preserve">
<value>Wygeneruj hasło ponownie</value>
<value>Wygeneruj ponownie hasło</value>
</data>
<data name="RetypeMasterPassword" xml:space="preserve">
<value>Wpisz ponownie hasło główne</value>
@@ -681,7 +681,7 @@
<value>Informacje o elemencie</value>
</data>
<data name="ItemUpdated" xml:space="preserve">
<value>Element został zaktualizowany.</value>
<value>Element został zapisany</value>
</data>
<data name="Submitting" xml:space="preserve">
<value>Wysyłanie...</value>
@@ -692,10 +692,10 @@
<comment>Message shown when interacting with the server</comment>
</data>
<data name="SyncingComplete" xml:space="preserve">
<value>Synchronizacja została zakończona.</value>
<value>Synchronizacja została zakończona</value>
</data>
<data name="SyncingFailed" xml:space="preserve">
<value>Synchronizacja nie powiodła się.</value>
<value>Synchronizacja nie powiodła się</value>
</data>
<data name="SyncVaultNow" xml:space="preserve">
<value>Synchronizuj sejf</value>
@@ -856,18 +856,18 @@
<comment>For 2FA</comment>
</data>
<data name="VerificationEmailSent" xml:space="preserve">
<value>Kod weryfikacyjny został wysłany.</value>
<value>Kod weryfikacyjny został wysłany</value>
<comment>For 2FA</comment>
</data>
<data name="YubiKeyInstruction" xml:space="preserve">
<value>Przyłóż klucz YubiKey NEO z tyłu urządzenia lub włóż klucz do portu USB urządzenia, a następnie dotknij jego przycisku.</value>
</data>
<data name="YubiKeyTitle" xml:space="preserve">
<value>Klucz bezpieczeństwa YubiKey NEO</value>
<value>Klucz bezpieczeństwa YubiKey</value>
<comment>"YubiKey" is the product name and should not be translated.</comment>
</data>
<data name="AddNewAttachment" xml:space="preserve">
<value>Dodaj załącznik</value>
<value>Dodaj nowy załącznik</value>
</data>
<data name="Attachments" xml:space="preserve">
<value>Załączniki</value>
@@ -919,7 +919,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Jeśli dane logowania posiadają dołączony klucz uwierzytelniający TOTP, kod weryfikacyjny jest automatycznie kopiowany do schowka przy każdym autouzupełnieniu danych logowania.</value>
</data>
<data name="CopyTotpAutomatically" xml:space="preserve">
<value>Kopiuj TOTP automatycznie</value>
<value>Kopiuj kod TOTP automatycznie</value>
</data>
<data name="PremiumRequired" xml:space="preserve">
<value>Konto Premium jest wymagane, aby skorzystać z tej funkcji.</value>
@@ -996,7 +996,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Pola niestandardowe</value>
</data>
<data name="CopyNumber" xml:space="preserve">
<value>Kopiuj numer karty</value>
<value>Kopiuj numer</value>
</data>
<data name="CopySecurityCode" xml:space="preserve">
<value>Kopiuj kod zabezpieczający</value>
@@ -1256,7 +1256,7 @@ Skanowanie nastąpi automatycznie.</value>
<comment>URI match detection for auto-fill.</comment>
</data>
<data name="YesAndSave" xml:space="preserve">
<value>Tak i Zapisz</value>
<value>Tak i zapisz</value>
</data>
<data name="AutofillAndSave" xml:space="preserve">
<value>Autouzupełnij i zapisz</value>
@@ -1278,7 +1278,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Usługa ułatwienia dostępu może być pomocna w sytuacji, gdy aplikacje nie obsługują standardowej usługi autouzupełniania.</value>
</data>
<data name="DatePasswordUpdated" xml:space="preserve">
<value>Aktualizacja hasła</value>
<value>Hasło zostało zaktualizowane</value>
<comment>ex. Date this password was updated</comment>
</data>
<data name="DateUpdated" xml:space="preserve">
@@ -1366,7 +1366,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Wartość</value>
</data>
<data name="PasswordHistory" xml:space="preserve">
<value>Historia haseł</value>
<value>Historia hasła</value>
</data>
<data name="Types" xml:space="preserve">
<value>Rodzaje</value>
@@ -1726,7 +1726,7 @@ Skanowanie nastąpi automatycznie.</value>
<comment>Message shown when interacting with the server</comment>
</data>
<data name="ItemRestored" xml:space="preserve">
<value>Element został przywrócony.</value>
<value>Element został przywrócony</value>
<comment>Confirmation message after successfully restoring a soft-deleted item</comment>
</data>
<data name="Trash" xml:space="preserve">
@@ -2006,7 +2006,7 @@ Skanowanie nastąpi automatycznie.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AddSend" xml:space="preserve">
<value>Dodaj wysyłkę</value>
<value>Nowa wysyłka</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AreYouSureDeleteSend" xml:space="preserve">
@@ -2014,15 +2014,15 @@ Skanowanie nastąpi automatycznie.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendDeleted" xml:space="preserve">
<value>Wysyłka została usunięta.</value>
<value>Wysyłka została usunięta</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendUpdated" xml:space="preserve">
<value>Wysyłka została zaktualizowana.</value>
<value>Wysyłka została zapisana</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="NewSendCreated" xml:space="preserve">
<value>Wysyłka została utworzona.</value>
<value>Wysyłka została utworzona</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="OneDay" xml:space="preserve">
@@ -2185,7 +2185,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Konto zostało trwale usunięte</value>
</data>
<data name="InvalidVerificationCode" xml:space="preserve">
<value>Kod weryfikacyjny jest nieprawidłowy.</value>
<value>Kod weryfikacyjny jest nieprawidłowy</value>
</data>
<data name="RequestOTP" xml:space="preserve">
<value>Poproś o jednorazowe hasło</value>
@@ -2242,7 +2242,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Znaki specjalne (!@#$%^&amp;*)</value>
</data>
<data name="TapToGoBack" xml:space="preserve">
<value>Dotknij, aby wrócić</value>
<value>Kliknij, aby wrócić</value>
</data>
<data name="PasswordIsVisibleTapToHide" xml:space="preserve">
<value>Hasło jest widoczne. Kliknij, aby je ukryć.</value>
@@ -2287,7 +2287,7 @@ Skanowanie nastąpi automatycznie.</value>
<value>Klucz uwierzytelniający</value>
</data>
<data name="EnterKeyManually" xml:space="preserve">
<value>Wprowadź klucz ręcznie</value>
<value>Wpisz klucz ręcznie</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>Dodaj TOTP</value>
@@ -2312,10 +2312,10 @@ wybierz Dodaj TOTP, aby bezpiecznie przechowywać klucz</value>
<value>Nie mogliśmy przetworzyć żądania. Spróbuj ponownie lub skontaktuj się z nami.</value>
</data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Pozwól na robienie zrzutów ekranu</value>
<value>Wykonywanie zrzutów ekranu</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Jesteś pewien, że chcesz włączyć możliwość robienia zrzutów ekranu?</value>
<value>Czy na pewno chcesz zezwolić na wykonywanie zrzutów ekranu?</value>
</data>
<data name="LogInRequested" xml:space="preserve">
<value>Wysłano prośbę o logowanie</value>
@@ -2327,7 +2327,7 @@ wybierz Dodaj TOTP, aby bezpiecznie przechowywać klucz</value>
<value>Próba logowania przez {0} na {1}</value>
</data>
<data name="DeviceType" xml:space="preserve">
<value>Typ urządzenia</value>
<value>Rodzaj urządzenia</value>
</data>
<data name="IpAddress" xml:space="preserve">
<value>Adres IP</value>
@@ -2348,13 +2348,13 @@ wybierz Dodaj TOTP, aby bezpiecznie przechowywać klucz</value>
<value>Teraz</value>
</data>
<data name="XMinutesAgo" xml:space="preserve">
<value>{0} minut temu</value>
<value>{0} min temu</value>
</data>
<data name="LogInAccepted" xml:space="preserve">
<value>Logowanie potwierdzone</value>
<value>Logowanie zostało potwierdzone</value>
</data>
<data name="LogInDenied" xml:space="preserve">
<value>Logowanie odrzucone</value>
<value>Logowanie zostało odrzucone</value>
</data>
<data name="ApproveLoginRequests" xml:space="preserve">
<value>Zatwierdź prośby o logowanie</value>

View File

@@ -1762,7 +1762,7 @@ Scanning will happen automatically.</value>
<value>Syncing vault with pull down gesture.</value>
</data>
<data name="LogInSso" xml:space="preserve">
<value>Enterprise Single Sign-On</value>
<value>Enterprise single sign-on</value>
</data>
<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>
@@ -2473,4 +2473,22 @@ select Add TOTP to store the key safely</value>
{0}
Do you want to switch to this account?</value>
</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>

View File

@@ -236,7 +236,7 @@
<comment>The button text that allows user to launch the website to their web browser.</comment>
</data>
<data name="HelpAndFeedback" xml:space="preserve">
<value>Hjälp och återkoppling</value>
<value>Hjälp &amp; Feedback</value>
</data>
<data name="Hide" xml:space="preserve">
<value>Dölj</value>
@@ -1308,7 +1308,7 @@ Skanningen sker automatiskt.</value>
<value>1. Gå till iOS "Inställningar"-app</value>
</data>
<data name="AutofillTurnOn2" xml:space="preserve">
<value>2. Tryck på "Lösenord &amp; konton"</value>
<value>2. Tryck på "Lösenord"</value>
</data>
<data name="AutofillTurnOn3" xml:space="preserve">
<value>3. Tryck på "Autofyll lösenord"</value>
@@ -1996,11 +1996,11 @@ Skanningen sker automatiskt.</value>
<value>Dela länk</value>
</data>
<data name="SendLink" xml:space="preserve">
<value>Skicka länk</value>
<value>Send link</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SearchSends" xml:space="preserve">
<value>Sök blan Sends</value>
<value>Search Sends</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="EditSend" xml:space="preserve">

View File

@@ -474,7 +474,7 @@
<value>开启自动同步</value>
</data>
<data name="EnterEmailForHint" xml:space="preserve">
<value>请输入您账户的 Email 地址来接收主密码提示。</value>
<value>请输入您账户的电子邮件地址来接收主密码提示。</value>
</data>
<data name="ExntesionReenable" xml:space="preserve">
<value>重新启用应用程序扩展</value>
@@ -708,7 +708,7 @@
<value>两步登录</value>
</data>
<data name="TwoStepLoginConfirmation" xml:space="preserve">
<value>两步登录要求您从其他设备(例如安全钥匙、验证器应用、短信、电话或者电子邮件)来验证的登录,这能使您的账户更加安全。两步登录可以在 bitwarden.com 网页版密码库启用。您现在要访问这个网站吗?</value>
<value>两步登录要求您从其他设备(例如安全钥匙、验证器应用、短信、电话或者电子邮件)来验证的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码中设置。您现在要访问这个网站吗?</value>
</data>
<data name="UnlockWith" xml:space="preserve">
<value>使用 {0} 解锁</value>
@@ -831,7 +831,7 @@
<comment>For 2FA whenever there are no available providers on this device.</comment>
</data>
<data name="NoTwoStepAvailable" xml:space="preserve">
<value>此账户已启用两步登录,但此设备不支持任何已配置的两步提供程序。请使用受支持的设备和/或添加其他跨设备支持的提供程序(例如验证器应用)。</value>
<value>此账户已设置两步登录,但此设备不支持任何已配置的两步提供程序。请使用受支持的设备和/或添加其他跨设备支持的提供程序(例如验证器应用)。</value>
</data>
<data name="RecoveryCodeTitle" xml:space="preserve">
<value>恢复代码</value>
@@ -937,7 +937,7 @@
<value>文件</value>
</data>
<data name="NoFileChosen" xml:space="preserve">
<value>未选定任何文件</value>
<value>未选文件</value>
</data>
<data name="NoAttachments" xml:space="preserve">
<value>没有附件。</value>
@@ -1892,7 +1892,7 @@
<value>您想要发送的文本。</value>
</data>
<data name="HideTextByDefault" xml:space="preserve">
<value>访问 Send 时,默认隐藏文本</value>
<value>访问 Send 时,默认隐藏文本内容</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="TypeFile" xml:space="preserve">
@@ -2006,7 +2006,7 @@
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AddSend" xml:space="preserve">
<value>新增 Send</value>
<value>创建 Send</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AreYouSureDeleteSend" xml:space="preserve">
@@ -2071,7 +2071,7 @@
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="PasswordPrompt" xml:space="preserve">
<value>重新提示主密码</value>
<value>重新询问主密码</value>
</data>
<data name="PasswordConfirmation" xml:space="preserve">
<value>确认主密码</value>
@@ -2107,7 +2107,7 @@
<value>{0} 正在使用客户管理加密的 SSO。继续操作将删除您的账户主密码并要求 SSO 登录。</value>
</data>
<data name="RemoveMasterPasswordWarning2" xml:space="preserve">
<value>如果您不想移除您的账号主密码,您可以退出这个组织。</value>
<value>如果您不想移除您的主密码,您可以退出这个组织。</value>
</data>
<data name="LeaveOrganization" xml:space="preserve">
<value>退出组织</value>

View File

@@ -157,7 +157,7 @@ namespace Bit.App.Services
};
_pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData);
_messagingService.Value.Send("passwordlessLoginRequest", passwordlessLoginMessage);
_messagingService.Value.Send(Constants.PasswordlessLoginRequestKey, passwordlessLoginMessage);
break;
default:
break;
@@ -228,7 +228,9 @@ namespace Bit.App.Services
if (data is PasswordlessNotificationData passwordlessNotificationData)
{
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);
_messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);

View File

@@ -80,6 +80,7 @@
Value="{DynamicResource StepperForegroundColor}" />
</Style>
<Style TargetType="Frame"
ApplyToDerivedTypes="True"
Class="btn-icon-row">
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColor}" />

View File

@@ -130,7 +130,36 @@
<Setter Property="BackgroundColor"
Value="{DynamicResource FabColor}" />
</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 -->
<Style TargetType="Button"
Class="btn-title"
@@ -518,4 +547,19 @@
<Setter Property="Radius"
Value="15" />
</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>

View File

@@ -93,6 +93,7 @@
Value="{DynamicResource StepperForegroundColor}" />
</Style>
<Style TargetType="Frame"
ApplyToDerivedTypes="True"
Class="btn-icon-row">
<Setter Property="BackgroundColor"
Value="{DynamicResource ButtonBackgroundColor}" />

View File

@@ -7,6 +7,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Utilities.AccountManagement
@@ -103,11 +104,12 @@ namespace Bit.App.Utilities.AccountManagement
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
var email = await _stateService.GetEmailAsync();
_accountsManagerHost.Navigate(NavigationTarget.Login, new LoginNavigationParams(email));
await _stateService.SetRememberedEmailAsync(email);
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin, new HomeNavigationParams(true));
}
else
{
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin);
_accountsManagerHost.Navigate(NavigationTarget.HomeLogin, new HomeNavigationParams(false));
}
}
}
@@ -183,7 +185,7 @@ namespace Bit.App.Utilities.AccountManagement
await Device.InvokeOnMainThreadAsync(() =>
{
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);
});
}
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");
}
}
}
}

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

View File

@@ -24,6 +24,132 @@ namespace Bit.App.Utilities
{
public static class AppHelpers
{
public static async Task<string> CipherListOptions(ContentPage page, CipherView cipher, IPasswordRepromptService passwordRepromptService)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
var options = new List<string> { AppResources.View };
if (!cipher.IsDeleted)
{
options.Add(AppResources.Edit);
}
if (cipher.Type == Core.Enums.CipherType.Login)
{
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
{
options.Add(AppResources.CopyUsername);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Password) && cipher.ViewPassword)
{
options.Add(AppResources.CopyPassword);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Totp))
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var canAccessPremium = await stateService.CanAccessPremiumAsync();
if (canAccessPremium || cipher.OrganizationUseTotp)
{
options.Add(AppResources.CopyTotp);
}
}
if (cipher.Login.CanLaunch)
{
options.Add(AppResources.Launch);
}
}
else if (cipher.Type == Core.Enums.CipherType.Card)
{
if (!string.IsNullOrWhiteSpace(cipher.Card.Number))
{
options.Add(AppResources.CopyNumber);
}
if (!string.IsNullOrWhiteSpace(cipher.Card.Code))
{
options.Add(AppResources.CopySecurityCode);
}
}
else if (cipher.Type == Core.Enums.CipherType.SecureNote)
{
if (!string.IsNullOrWhiteSpace(cipher.Notes))
{
options.Add(AppResources.CopyNotes);
}
}
var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, options.ToArray());
if (await vaultTimeoutService.IsLockedAsync())
{
platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
}
else if (selection == AppResources.View)
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(cipher.Id)));
}
else if (selection == AppResources.Edit)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(cipher.Id)));
}
}
else if (selection == AppResources.CopyUsername)
{
await clipboardService.CopyTextAsync(cipher.Login.Username);
platformUtilsService.ShowToastForCopiedValue(AppResources.Username);
}
else if (selection == AppResources.CopyPassword)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Login.Password);
platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
}
}
else if (selection == AppResources.CopyTotp)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (!string.IsNullOrWhiteSpace(totp))
{
await clipboardService.CopyTextAsync(totp);
platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
}
else if (selection == AppResources.Launch)
{
platformUtilsService.LaunchUri(cipher.Login.LaunchUri);
}
else if (selection == AppResources.CopyNumber)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Number);
platformUtilsService.ShowToastForCopiedValue(AppResources.Number);
}
}
else if (selection == AppResources.CopySecurityCode)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Code);
platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id);
}
}
else if (selection == AppResources.CopyNotes)
{
await clipboardService.CopyTextAsync(cipher.Notes);
platformUtilsService.ShowToastForCopiedValue(AppResources.Notes);
}
return selection;
}
public static async Task<string> SendListOptions(ContentPage page, SendView send)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");

View File

@@ -1,56 +0,0 @@
using Bit.Core.Models.View;
using Xamarin.CommunityToolkit.Converters;
using Xamarin.Forms;
namespace Bit.App.Utilities
{
public class CipherToSwipeBackgroundColor : BaseConverter<CipherView, Color>
{
public override Color ConvertFrom(CipherView value)
{
if (value is null)
{
return Color.Default;
}
Color GetDisabledColor()
{
if (App.Current.Resources.TryGetValue("ButtonBackgroundColorDisabled", out var disabledColor))
{
return (Color)disabledColor;
}
return Color.LightGray;
}
if (value.Type == Core.Enums.CipherType.Login
&&
value.Login?.Password is null)
{
return GetDisabledColor();
}
if (value.Type == Core.Enums.CipherType.Card
&&
value.Card?.Number is null)
{
return GetDisabledColor();
}
if (value.Type == Core.Enums.CipherType.SecureNote
&&
value.Notes is null)
{
return GetDisabledColor();
}
if (App.Current.Resources.TryGetValue("PrimaryColor", out var enabledColor))
{
return (Color)enabledColor;
}
return Color.Default;
}
public override CipherView ConvertBackTo(Color value) => throw new System.NotImplementedException();
}
}

View File

@@ -1,40 +0,0 @@
using System;
using System.Globalization;
using Bit.App.Controls;
using Bit.Core;
using Bit.Core.Enums;
using Xamarin.Forms;
namespace Bit.App.Utilities
{
public class CipherTypeToSwipeActionGlyphConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var fontImageSource = new IconFontImageSource();
if (value is CipherType cipherType)
{
if (Application.Current.Resources.TryGetValue("TextColor", out var textColor))
{
fontImageSource.Color = (Color)textColor;
}
switch (cipherType)
{
case CipherType.Login:
fontImageSource.Glyph = BitwardenIcons.Key;
break;
case CipherType.Card:
fontImageSource.Glyph = BitwardenIcons.Hashtag;
break;
case CipherType.SecureNote:
fontImageSource.Glyph = BitwardenIcons.Clone;
break;
}
}
return fontImageSource;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}
}

View File

@@ -1,195 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Pages;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Utilities.Helpers
{
public class CipherHelper : ICipherHelper
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IEventService _eventService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IClipboardService _clipboardService;
private readonly IPasswordRepromptService _passwordRepromptService;
public CipherHelper(IPlatformUtilsService platformUtilsService,
IEventService eventService,
IVaultTimeoutService vaultTimeoutService,
IClipboardService clipboardService,
IPasswordRepromptService passwordRepromptService)
{
_platformUtilsService = platformUtilsService;
_eventService = eventService;
_vaultTimeoutService = vaultTimeoutService;
_clipboardService = clipboardService;
_passwordRepromptService = passwordRepromptService;
}
public async Task<string> ShowCipherOptionsAsync(Page page, CipherView cipher)
{
var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, await GetCipherOptionsAsync(cipher));
if (await _vaultTimeoutService.IsLockedAsync())
{
_platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
}
else if (selection == AppResources.View)
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherDetailsPage(cipher.Id)));
}
else if (selection == AppResources.Edit)
{
if (await RepromptPasswordIfNeededAsync(cipher))
{
await page.Navigation.PushModalAsync(new NavigationPage(new CipherAddEditPage(cipher.Id)));
}
}
else if (selection == AppResources.CopyUsername)
{
await CopyUsernameAsync(cipher);
}
else if (selection == AppResources.CopyPassword)
{
await CopyPasswordAsync(cipher);
}
else if (selection == AppResources.CopyTotp)
{
if (await RepromptPasswordIfNeededAsync(cipher))
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (!string.IsNullOrWhiteSpace(totp))
{
await _clipboardService.CopyTextAsync(totp);
_platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
}
else if (selection == AppResources.Launch)
{
_platformUtilsService.LaunchUri(cipher.Login.LaunchUri);
}
else if (selection == AppResources.CopyNumber)
{
await CopyCardNumberAsync(cipher);
}
else if (selection == AppResources.CopySecurityCode)
{
if (await RepromptPasswordIfNeededAsync(cipher))
{
await _clipboardService.CopyTextAsync(cipher.Card.Code);
_platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode);
_eventService.CollectAsync(EventType.Cipher_ClientCopiedCardCode, cipher.Id).FireAndForget();
}
}
else if (selection == AppResources.CopyNotes)
{
await CopyNotesAsync(cipher);
}
return selection;
}
public async Task CopyUsernameAsync(CipherView cipher)
{
await _clipboardService.CopyTextAsync(cipher.Login.Username);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Username);
}
public async Task<bool> CopyPasswordAsync(CipherView cipher)
{
if (await RepromptPasswordIfNeededAsync(cipher))
{
await _clipboardService.CopyTextAsync(cipher.Login.Password);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
_eventService.CollectAsync(EventType.Cipher_ClientCopiedPassword, cipher.Id).FireAndForget();
return true;
}
return false;
}
public async Task<bool> CopyCardNumberAsync(CipherView cipher)
{
if (await RepromptPasswordIfNeededAsync(cipher))
{
await _clipboardService.CopyTextAsync(cipher.Card.Number);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Number);
return true;
}
return false;
}
public async Task CopyNotesAsync(CipherView cipher)
{
await _clipboardService.CopyTextAsync(cipher.Notes);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Notes);
}
private async Task<string[]> GetCipherOptionsAsync(CipherView cipher)
{
var options = new List<string> { AppResources.View };
if (!cipher.IsDeleted)
{
options.Add(AppResources.Edit);
}
if (cipher.Type == CipherType.Login)
{
if (!string.IsNullOrWhiteSpace(cipher.Login.Username))
{
options.Add(AppResources.CopyUsername);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Password) && cipher.ViewPassword)
{
options.Add(AppResources.CopyPassword);
}
if (!string.IsNullOrWhiteSpace(cipher.Login.Totp))
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var canAccessPremium = await stateService.CanAccessPremiumAsync();
if (canAccessPremium || cipher.OrganizationUseTotp)
{
options.Add(AppResources.CopyTotp);
}
}
if (cipher.Login.CanLaunch)
{
options.Add(AppResources.Launch);
}
}
else if (cipher.Type == CipherType.Card)
{
if (!string.IsNullOrWhiteSpace(cipher.Card.Number))
{
options.Add(AppResources.CopyNumber);
}
if (!string.IsNullOrWhiteSpace(cipher.Card.Code))
{
options.Add(AppResources.CopySecurityCode);
}
}
else if (cipher.Type == CipherType.SecureNote)
{
if (!string.IsNullOrWhiteSpace(cipher.Notes))
{
options.Add(AppResources.CopyNotes);
}
}
return options.ToArray();
}
private async Task<bool> RepromptPasswordIfNeededAsync(CipherView cipher)
{
return cipher.Reprompt == CipherRepromptType.None || await _passwordRepromptService.ShowPasswordPromptAsync();
}
}
}

View File

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using Bit.Core.Models.View;
using Xamarin.Forms;
namespace Bit.App.Utilities.Helpers
{
public interface ICipherHelper
{
Task<string> ShowCipherOptionsAsync(Page page, CipherView cipher);
Task CopyUsernameAsync(CipherView cipher);
Task<bool> CopyPasswordAsync(CipherView cipher);
Task<bool> CopyCardNumberAsync(CipherView cipher);
Task CopyNotesAsync(CipherView cipher);
}
}

View File

@@ -83,8 +83,10 @@ namespace Bit.Core.Abstractions
Task<SendResponse> PutSendAsync(string id, SendRequest request);
Task<SendResponse> PutSendRemovePasswordAsync(string id);
Task DeleteSendAsync(string id);
Task<List<PasswordlessLoginResponse>> GetAuthRequestAsync();
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
}
}

View File

@@ -27,6 +27,7 @@ namespace Bit.Core.Abstractions
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<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync();
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);

View File

@@ -13,6 +13,7 @@ namespace Bit.Core.Abstractions
{
List<AccountView> AccountViews { get; }
Task<string> GetActiveUserIdAsync();
Task<string> GetActiveUserEmailAsync();
Task<bool> IsActiveAccountAsync(string userId = null);
Task SetActiveUserAsync(string userId);
Task CheckExtensionActiveUserAndSwitchIfNeededAsync();

View File

@@ -14,5 +14,7 @@ namespace Bit.Core.Abstractions
Task<bool> SyncDeleteFolderAsync(SyncFolderNotification notification);
Task<bool> SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit);
Task<bool> SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit);
// Passwordless code will be moved to an independent service in future techdept
Task SyncPasswordlessLoginRequestsAsync();
}
}

View File

@@ -28,5 +28,6 @@ namespace Bit.Core.Abstractions
Task SetTwoFactorTokenAsync(string token, string email);
bool TokenNeedsRefresh(int minutes = 5);
int TokenSecondsRemaining();
Task PrepareTokenForDecodingAsync();
}
}

View File

@@ -112,5 +112,7 @@
public const string File = "\xe96e";
public const string Paste = "\xe96f";
public const string ViewCellMenu = "\xe5d3";
public const string Device = "\xe986";
public const string Suitcase = "\xe98c";
}
}

View File

@@ -37,6 +37,7 @@
public const string iOSNotificationClearActionId = "Clear";
public const string NotificationData = "notificationData";
public const string NotificationDataType = "Type";
public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest";
public const int SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43;
public const int SaveFileRequestCode = 44;

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Response
{
@@ -13,7 +15,19 @@ namespace Bit.Core.Models.Response
public string Key { get; set; }
public string MasterPasswordHash { 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 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; }
}
}

View File

@@ -536,6 +536,12 @@ namespace Bit.Core.Services
#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)
{
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);
}
public Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier)
{
return SendAsync<object, bool>(HttpMethod.Get, $"/devices/knowndevice/{email}/{deviceIdentifier}", null, false, true);
}
#endregion
#region Helpers
@@ -790,8 +801,6 @@ namespace Bit.Core.Services
if (authed
&&
(
(tokenError && response.StatusCode == HttpStatusCode.BadRequest)
||
(logoutOnUnauthorized && response.StatusCode == HttpStatusCode.Unauthorized)
||
response.StatusCode == HttpStatusCode.Forbidden
@@ -808,6 +817,17 @@ namespace Bit.Core.Services
var responseJsonString = await response.Content.ReadAsStringAsync();
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);
}
catch

View File

@@ -471,6 +471,11 @@ namespace Bit.Core.Services
SelectedTwoFactorProviderType = null;
}
public async Task<List<PasswordlessLoginResponse>> GetPasswordlessLoginRequestsAsync()
{
return await _apiService.GetAuthRequestAsync();
}
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id)
{
return await _apiService.GetAuthRequestAsync(id);

View File

@@ -46,6 +46,12 @@ namespace Bit.Core.Services
return activeUserId;
}
public async Task<string> GetActiveUserEmailAsync()
{
var activeUserId = await GetActiveUserIdAsync();
return await GetEmailAsync(activeUserId);
}
public async Task<bool> IsActiveAccountAsync(string userId = null)
{
if (userId == null)
@@ -68,7 +74,6 @@ namespace Bit.Core.Services
_state.ActiveUserId = userId;
// Update pre-auth settings based on now-active user
await SetRememberedEmailAsync(await GetEmailAsync());
await SetRememberedOrgIdentifierAsync(await GetRememberedOrgIdentifierAsync());
await SetPreAuthEnvironmentUrlsAsync(await GetEnvironmentUrlsAsync());
}

View File

@@ -24,6 +24,7 @@ namespace Bit.Core.Services
private readonly IPolicyService _policyService;
private readonly ISendService _sendService;
private readonly IKeyConnectorService _keyConnectorService;
private readonly ILogger _logger;
private readonly Func<Tuple<string, bool, bool>, Task> _logoutCallbackAsync;
public SyncService(
@@ -39,6 +40,7 @@ namespace Bit.Core.Services
IPolicyService policyService,
ISendService sendService,
IKeyConnectorService keyConnectorService,
ILogger logger,
Func<Tuple<string, bool, bool>, Task> logoutCallbackAsync)
{
_stateService = stateService;
@@ -53,6 +55,7 @@ namespace Bit.Core.Services
_policyService = policyService;
_sendService = sendService;
_keyConnectorService = keyConnectorService;
_logger = logger;
_logoutCallbackAsync = logoutCallbackAsync;
}
@@ -382,5 +385,45 @@ namespace Bit.Core.Services
new Dictionary<string, SendData>();
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);
}
}
}
}

View File

@@ -44,6 +44,11 @@ namespace Bit.Core.Services
return _accessTokenForDecoding;
}
public async Task PrepareTokenForDecodingAsync()
{
_accessTokenForDecoding = await _stateService.GetAccessTokenAsync();
}
public async Task SetRefreshTokenAsync(string refreshToken)
{
await _stateService.SetRefreshTokenAsync(refreshToken, await SkipTokenStorage());

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
@@ -264,5 +265,36 @@ namespace Bit.Core.Utilities
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
}
public static string TextColorFromBgColor(string hexColor, int threshold = 166)
{
if (new ColorConverter().ConvertFromString(hexColor) is Color bgColor)
{
var luminance = bgColor.R * 0.299 + bgColor.G * 0.587 + bgColor.B * 0.114;
return luminance > threshold ? "#ff000000" : "#ffffffff";
}
return "#ff000000";
}
public static string StringToColor(string str, string fallback)
{
if (str == null)
{
return fallback;
}
var hash = 0;
for (var i = 0; i < str.Length; i++)
{
hash = str[i] + ((hash << 5) - hash);
}
var color = "#FF";
for (var i = 0; i < 3; i++)
{
var value = (hash >> (i * 8)) & 0xff;
color += Convert.ToString(value, 16).PadLeft(2, '0');
}
return color;
}
}
}

View File

@@ -29,6 +29,8 @@ namespace Bit.Core.Utilities
var messagingService = Resolve<IMessagingService>("messagingService");
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
var cryptoService = Resolve<ICryptoService>("cryptoService");
var logger = Resolve<ILogger>();
SearchService searchService = null;
var tokenService = new TokenService(stateService);
@@ -67,7 +69,7 @@ namespace Bit.Core.Utilities
});
var syncService = new SyncService(stateService, apiService, settingsService, folderService, cipherService,
cryptoService, collectionService, organizationService, messagingService, policyService, sendService,
keyConnectorService, (extras) =>
keyConnectorService, logger, (extras) =>
{
messagingService.Send("logout", extras);
return Task.CompletedTask;

View File

@@ -425,15 +425,16 @@ namespace Bit.iOS.Autofill
}
}
private void LaunchHomePage()
private void LaunchHomePage(bool shouldCheckRememberEmail = true)
{
var homePage = new HomePage();
var app = new App.App(new AppOptions { IosExtension = true });
var appOptions = new AppOptions { IosExtension = true };
var homePage = new HomePage(appOptions, shouldCheckRememberEmail);
var app = new App.App(appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(homePage);
if (homePage.BindingContext is HomeViewModel vm)
{
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow());
vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow());
@@ -456,8 +457,8 @@ namespace Bit.iOS.Autofill
ThemeManager.ApplyResourcesTo(environmentPage);
if (environmentPage.BindingContext is EnvironmentPageViewModel vm)
{
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
}
var navigationPage = new NavigationPage(environmentPage);
@@ -475,7 +476,7 @@ namespace Bit.iOS.Autofill
if (registerPage.BindingContext is RegisterPageViewModel vm)
{
vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email));
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
}
var navigationPage = new NavigationPage(registerPage);
@@ -495,8 +496,9 @@ namespace Bit.iOS.Autofill
{
vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false));
vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow());
vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow());
vm.LogInSuccessAction = () => DismissLockAndContinue();
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage());
vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage(shouldCheckRememberEmail: false));
}
var navigationPage = new NavigationPage(loginPage);
@@ -602,7 +604,14 @@ namespace Bit.iOS.Autofill
switch (navTarget)
{
case NavigationTarget.HomeLogin:
DismissViewController(false, () => LaunchHomePage());
if (navParams is HomeNavigationParams homeParams)
{
DismissViewController(false, () => LaunchHomePage(homeParams.ShouldCheckRememberEmail));
}
else
{
DismissViewController(false, () => LaunchHomePage());
}
break;
case NavigationTarget.Login:
if (navParams is LoginNavigationParams loginParams)

View File

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

View File

@@ -5,7 +5,6 @@ using Bit.App.Models;
using Bit.App.Pages;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
@@ -55,7 +54,7 @@ namespace Bit.iOS.Core.Controllers
public abstract Action Cancel { get; }
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
AppResources.MasterPassword, useButton: true);
AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One);
public string BiometricIntegrityKey { get; set; }
@@ -161,13 +160,7 @@ namespace Bit.iOS.Core.Controllers
{
MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad;
}
MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal);
MasterPasswordCell.Button.TouchUpInside += (sender, e) =>
{
MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry;
MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal);
};
MasterPasswordCell.ConfigureToggleSecureTextCell();
}
if (TableView != null)

View File

@@ -14,7 +14,6 @@ using Bit.Core.Enums;
using Bit.App.Pages;
using Bit.App.Models;
using Xamarin.Forms;
using Bit.Core;
namespace Bit.iOS.Core.Controllers
{
@@ -52,7 +51,7 @@ namespace Bit.iOS.Core.Controllers
public abstract Action Cancel { get; }
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
AppResources.MasterPassword, useButton: true);
AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One);
public string BiometricIntegrityKey { get; set; }
@@ -155,12 +154,7 @@ namespace Bit.iOS.Core.Controllers
{
MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad;
}
MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal);
MasterPasswordCell.Button.TouchUpInside += (sender, e) => {
MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry;
MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal);
};
MasterPasswordCell.ConfigureToggleSecureTextCell();
}
TableView.RowHeight = UITableView.AutomaticDimension;

View File

@@ -1,18 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AuthenticationServices;
using Bit.App.Models;
using Bit.App.Pages;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Bit.iOS.Core.Models;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using Foundation;
using UIKit;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Models;
using System.Threading.Tasks;
using AuthenticationServices;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Bit.Core.Exceptions;
using Xamarin.Forms;
namespace Bit.iOS.Core.Controllers
{
@@ -29,10 +34,8 @@ namespace Bit.iOS.Core.Controllers
public AppExtensionContext Context { get; set; }
public FormEntryTableViewCell NameCell { get; set; } = new FormEntryTableViewCell(AppResources.Name);
public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username);
public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password);
public UITableViewCell GeneratePasswordCell { get; set; } = new ExtendedUITableViewCell(
UITableViewCellStyle.Subtitle, "GeneratePasswordCell");
public FormEntryTableViewCell UsernameCell { get; set; } = new FormEntryTableViewCell(AppResources.Username, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One);
public FormEntryTableViewCell PasswordCell { get; set; } = new FormEntryTableViewCell(AppResources.Password, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.Two);
public FormEntryTableViewCell UriCell { get; set; } = new FormEntryTableViewCell(AppResources.URI);
public SwitchTableViewCell FavoriteCell { get; set; } = new SwitchTableViewCell(AppResources.Favorite);
public FormEntryTableViewCell NotesCell { get; set; } = new FormEntryTableViewCell(
@@ -67,6 +70,12 @@ namespace Bit.iOS.Core.Controllers
UsernameCell.TextField.AutocorrectionType = UITextAutocorrectionType.No;
UsernameCell.TextField.SpellCheckingType = UITextSpellCheckingType.No;
UsernameCell.TextField.ReturnKeyType = UIReturnKeyType.Next;
UsernameCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
UsernameCell.Button.SetTitle(BitwardenIcons.Generate, UIControlState.Normal);
UsernameCell.Button.TouchUpInside += (sender, e) =>
{
LaunchUsernameGeneratorFlow();
};
UsernameCell.TextField.ShouldReturn += (UITextField tf) =>
{
PasswordCell.TextField.BecomeFirstResponder();
@@ -75,17 +84,20 @@ namespace Bit.iOS.Core.Controllers
PasswordCell.TextField.SecureTextEntry = true;
PasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Next;
PasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
PasswordCell.Button.SetTitle(BitwardenIcons.Generate, UIControlState.Normal);
PasswordCell.Button.TouchUpInside += (sender, e) =>
{
PerformSegue("passwordGeneratorSegue", this);
};
PasswordCell.ConfigureToggleSecureTextCell(true);
PasswordCell.TextField.ShouldReturn += (UITextField tf) =>
{
UriCell.TextField.BecomeFirstResponder();
return true;
};
GeneratePasswordCell.TextLabel.Text = AppResources.GeneratePassword;
GeneratePasswordCell.TextLabel.TextColor = GeneratePasswordCell.TextLabel.TintColor =
ThemeHelpers.TextColor;
GeneratePasswordCell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
UriCell.TextField.Text = Context?.UrlString ?? string.Empty;
UriCell.TextField.KeyboardType = UIKeyboardType.Url;
UriCell.TextField.ReturnKeyType = UIReturnKeyType.Next;
@@ -210,6 +222,26 @@ namespace Bit.iOS.Core.Controllers
AppResources.InternetConnectionRequiredMessage, AppResources.Ok);
}
private void LaunchUsernameGeneratorFlow()
{
var appOptions = new AppOptions { IosExtension = true };
var app = new App.App(appOptions);
var generatorPage = new GeneratorPage(false, selectAction: async (username) =>
{
UsernameCell.TextField.Text = username;
DismissViewController(false, null);
}, isUsernameGenerator: true, emailWebsite: NameCell.TextField.Text, appOptions: appOptions);
ThemeManager.SetTheme(app.Resources);
ThemeManager.ApplyResourcesTo(generatorPage);
var navigationPage = new NavigationPage(generatorPage);
var generatorController = navigationPage.CreateViewController();
generatorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
PresentViewController(generatorController, true, null);
}
public class TableSource : ExtendedUITableViewSource
{
private LoginAddViewController _controller;
@@ -235,10 +267,6 @@ namespace Bit.iOS.Core.Controllers
{
return _controller.PasswordCell;
}
else if (indexPath.Row == 3)
{
return _controller.GeneratePasswordCell;
}
}
else if (indexPath.Section == 1)
{
@@ -277,7 +305,7 @@ namespace Bit.iOS.Core.Controllers
{
if (section == 0)
{
return 4;
return 3;
}
else if (section == 1)
{
@@ -317,11 +345,6 @@ namespace Bit.iOS.Core.Controllers
tableView.DeselectRow(indexPath, true);
tableView.EndEditing(true);
if (indexPath.Section == 0 && indexPath.Row == 3)
{
_controller.PerformSegue("passwordGeneratorSegue", this);
}
var cell = tableView.CellAt(indexPath);
if (cell == null)
{

View File

@@ -379,5 +379,10 @@ namespace Bit.iOS.Core.Services
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
UIApplication.SharedApplication.OpenUrl(url);
}
public void CloseExtensionPopUp()
{
GetPresentedViewController().DismissViewController(true, null);
}
}
}

View File

@@ -32,8 +32,9 @@ namespace Bit.iOS.Core.Utilities
{
throw new NullReferenceException(nameof(_stateService));
}
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
{
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);

View File

@@ -9,7 +9,6 @@ using Bit.App.Resources;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Utilities.Helpers;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
@@ -230,15 +229,6 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
var cipherHelper = new CipherHelper(
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IEventService>("eventService"),
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
ServiceContainer.Resolve<IClipboardService>("clipboardService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService")
);
ServiceContainer.Register<ICipherHelper>("cipherHelper", cipherHelper);
if (postBootstrapFunc != null)
{
await postBootstrapFunc.Invoke();

View File

@@ -1,7 +1,7 @@
using Bit.iOS.Core.Controllers;
using System;
using Bit.Core;
using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Utilities;
using System;
using System.Drawing;
using UIKit;
namespace Bit.iOS.Core.Views
@@ -12,14 +12,14 @@ namespace Bit.iOS.Core.Views
public UITextField TextField { get; set; }
public UITextView TextView { get; set; }
public UIButton Button { get; set; }
public UIButton SecondButton { get; set; }
public event EventHandler ValueChanged;
public FormEntryTableViewCell(
string labelName = null,
bool useTextView = false,
nfloat? height = null,
bool useButton = false,
ButtonsConfig buttonsConfig = ButtonsConfig.None,
bool useLabelAsPlaceholder = false,
float leadingConstant = 15f)
: base(UITableViewCellStyle.Default, nameof(FormEntryTableViewCell))
@@ -85,7 +85,6 @@ namespace Bit.iOS.Core.Views
ValueChanged?.Invoke(sender, e);
};
}
else
{
TextField = new UITextField
@@ -110,9 +109,10 @@ namespace Bit.iOS.Core.Views
}
ContentView.Add(TextField);
ContentView.AddConstraints(new NSLayoutConstraint[] {
NSLayoutConstraint.Create(TextField, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, ContentView, NSLayoutAttribute.Leading, 1f, leadingConstant),
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, useButton ? 55f : 15f),
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, GetTextFieldToContainerTrailingConstant(buttonsConfig)),
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Bottom, 1f, 10f)
});
@@ -148,18 +148,9 @@ namespace Bit.iOS.Core.Views
});
}
if (useButton)
if(buttonsConfig != ButtonsConfig.None)
{
Button = new UIButton(UIButtonType.System);
Button.Frame = ContentView.Bounds;
Button.TranslatesAutoresizingMaskIntoConstraints = false;
Button.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal);
ContentView.Add(Button);
ContentView.BottomAnchor.ConstraintEqualTo(Button.BottomAnchor, 10f).Active = true;
ContentView.TrailingAnchor.ConstraintEqualTo(Button.TrailingAnchor, 10f).Active = true;
Button.LeadingAnchor.ConstraintEqualTo(TextField.TrailingAnchor, 10f).Active = true;
AddButtons(buttonsConfig);
}
}
@@ -174,5 +165,74 @@ namespace Bit.iOS.Core.Views
TextField.BecomeFirstResponder();
}
}
public void ConfigureToggleSecureTextCell(bool useSecondaryButton = false)
{
var button = useSecondaryButton ? SecondButton : Button;
button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal);
button.TouchUpInside += (sender, e) =>
{
TextField.SecureTextEntry = !TextField.SecureTextEntry;
button.SetTitle(TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal);
};
}
private void AddButtons(ButtonsConfig buttonsConfig)
{
Button = new UIButton(UIButtonType.System);
Button.TranslatesAutoresizingMaskIntoConstraints = false;
Button.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal);
ContentView.Add(Button);
ContentView.BottomAnchor.ConstraintEqualTo(Button.BottomAnchor, 10f).Active = true;
switch (buttonsConfig)
{
case ButtonsConfig.One:
ContentView.AddConstraints(new NSLayoutConstraint[] {
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Button, NSLayoutAttribute.Trailing, 1f, 10f),
NSLayoutConstraint.Create(Button, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 10f)
});
break;
case ButtonsConfig.Two:
SecondButton = new UIButton(UIButtonType.System);
SecondButton.TranslatesAutoresizingMaskIntoConstraints = false;
SecondButton.SetTitleColor(ThemeHelpers.PrimaryColor, UIControlState.Normal);
ContentView.Add(SecondButton);
ContentView.AddConstraints(new NSLayoutConstraint[] {
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, SecondButton, NSLayoutAttribute.Bottom, 1f, 10f),
NSLayoutConstraint.Create(SecondButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, TextField, NSLayoutAttribute.Trailing, 1f, 9f),
NSLayoutConstraint.Create(Button, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, SecondButton, NSLayoutAttribute.Trailing, 1f, 10f),
NSLayoutConstraint.Create(ContentView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, Button, NSLayoutAttribute.Trailing, 1f, 10f)
});
break;
}
}
private float GetTextFieldToContainerTrailingConstant(ButtonsConfig buttonsConfig)
{
switch (buttonsConfig)
{
case ButtonsConfig.None:
return 15f;
case ButtonsConfig.One:
return 55f;
case ButtonsConfig.Two:
return 95f;
default:
throw new ArgumentOutOfRangeException();
}
}
public enum ButtonsConfig : byte
{
None = 0,
One = 1,
Two = 2
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More