diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c35a886d..35cc5bcfd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: java-version: ${{ env.JAVA_VERSION }} - name: Configure Ruby - uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0 with: bundler-cache: true @@ -106,7 +106,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Configure Ruby - uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0 with: bundler-cache: true @@ -396,7 +396,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Configure Ruby - uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0 with: bundler-cache: true @@ -464,10 +464,17 @@ jobs: - name: Increment version run: | DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER)) + VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}" bundle exec fastlane setBuildVersionInfo \ - versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \ + versionCode:$VERSION_CODE \ versionName:${{ inputs.version-name || '' }} + regex='versionName = "([^"]+)"' + if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then + VERSION_NAME="${BASH_REMATCH[1]}" + fi + echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY + echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY - name: Generate F-Droid artifacts env: FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }} @@ -528,11 +535,11 @@ jobs: if-no-files-found: error - name: Install Firebase app distribution plugin - if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }} + if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }} run: bundle exec fastlane add_plugin firebase_app_distribution - name: Publish release F-Droid artifacts to Firebase - if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }} + if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }} env: APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json run: | diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 68c6b3474..76a725dac 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -2,7 +2,7 @@ name: Crowdin Sync on: workflow_dispatch: - inputs: { } + inputs: {} schedule: - cron: '0 0 * * 5' @@ -28,10 +28,17 @@ jobs: keyvault: "bitwarden-ci" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" + - name: Generate GH App token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Download translations uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} with: config: crowdin.yml diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 000000000..079110e37 --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,127 @@ +name: Create GitHub Release + +on: + workflow_dispatch: + inputs: + version-name: + description: 'Version Name - E.g. "2024.11.1"' + required: true + type: string + version-number: + description: 'Version Number - E.g. "123456"' + required: true + type: string + artifact_run_id: + description: 'GitHub Action Run ID containing artifacts' + required: true + type: string + draft: + description: 'Create as draft release' + type: boolean + default: true + prerelease: + description: 'Mark as pre-release' + type: boolean + make_latest: + description: 'Set as the latest release' + type: boolean + branch-protection-type: + description: 'Branch protection type' + type: choice + options: + - Branch Name + - GitHub API + default: Branch Name +env: + ARTIFACTS_PATH: artifacts +jobs: + create-release: + runs-on: ubuntu-24.04 + permissions: + contents: write + actions: read + + steps: + - name: Check out repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Get branch from workflow run + id: get_release_branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }} + BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }} + run: | + release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch) + + case "$BRANCH_PROTECTION_TYPE" in + "Branch Name") + if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then + echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches." + exit 1 + fi + ;; + "GitHub API") + #NOTE requires token with "administration:read" scope + if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then + echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope." + exit 1 + fi + ;; + *) + echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE" + exit 1 + ;; + esac + + echo "release_branch=$release_branch" >> $GITHUB_OUTPUT + + - name: Download artifacts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }} + run: | + gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH + file_count=$(find $ARTIFACTS_PATH -type f | wc -l) + echo "Downloaded $file_count file(s)." + if [ "$file_count" -gt 0 ]; then + echo "Downloaded files:" + find $ARTIFACTS_PATH -type f + fi + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9 + with: + tag_name: ${{ inputs.version-name }} + name: "v${{ inputs.version-name }} (${{ inputs.version-number }})" + prerelease: ${{ inputs.prerelease }} + draft: ${{ inputs.draft }} + make_latest: ${{ inputs.make_latest }} + target_commitish: ${{ steps.get_release_branch.outputs.release_branch }} + generate_release_notes: true + files: | + artifacts/**/* + + - name: Update Release Description + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_ID: ${{ steps.create_release.outputs.id }} + RELEASE_URL: ${{ steps.create_release.outputs.url }} + ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }} + run: | + # Get current release body + current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body) + + # Append build source to the end + updated_body="${current_body} + **Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID" + + # Update release + gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \ + -f body="$updated_body" + + echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY + echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml new file mode 100644 index 000000000..06a31e5e8 --- /dev/null +++ b/.github/workflows/release-branch.yml @@ -0,0 +1,56 @@ +name: Cut Release Branch + +on: + workflow_dispatch: + inputs: + release_type: + description: 'Release Type' + required: true + type: choice + options: + - RC + - Hotfix + rc_prefix_date: + description: 'RC - Prefix with date. E.g. 2024.11-rc1' + type: boolean + default: true + +jobs: + create-release-branch: + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Check out repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Create RC Branch + if: inputs.release_type == 'RC' + env: + RC_PREFIX_DATE: ${{ inputs.rc_prefix_date }} + run: | + if [ "$RC_PREFIX_DATE" = "true" ]; then + current_date=$(date +'%Y.%m') + branch_name="release/${current_date}-rc${{ github.run_number }}" + else + branch_name="release/rc${{ github.run_number }}" + fi + git switch main + git switch -c $branch_name + git push origin $branch_name + echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY + + - name: Create Hotfix Branch + if: inputs.release_type == 'Hotfix' + run: | + latest_tag=$(git describe --tags --abbrev=0) + if [ -z "$latest_tag" ]; then + echo "::error::No tags found in the repository" + exit 1 + fi + branch_name="release/hotfix-${latest_tag}" + git switch -c $branch_name $latest_tag + git push origin $branch_name + echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index aa34b3933..ac7b07e30 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -33,7 +33,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 + uses: checkmarx/ast-github-action@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -48,7 +48,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2 with: sarif_file: cx_result.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffab7a68e..74a5623c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: ${{ runner.os }}-build- - name: Configure Ruby - uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 + uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0 with: bundler-cache: true diff --git a/Gemfile.lock b/Gemfile.lock index b0516967f..a3fe5fadf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,8 +10,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.996.0) - aws-sdk-core (3.211.0) + aws-partitions (1.1003.0) + aws-sdk-core (3.212.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -19,7 +19,7 @@ GEM aws-sdk-kms (1.95.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.169.0) + aws-sdk-s3 (1.170.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -32,7 +32,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - date (3.3.4) + date (3.4.0) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) @@ -162,7 +162,7 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.4) + json (2.8.1) jwt (2.9.3) base64 mini_magick (4.13.2) @@ -172,7 +172,7 @@ GEM nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) public_suffix (6.0.1) @@ -199,7 +199,7 @@ GEM terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - time (0.4.0) + time (0.4.1) date trailblazer-option (0.1.2) tty-cursor (0.7.1) @@ -209,7 +209,7 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.26.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/README.md b/README.md index 353b4a103..5be22abd9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Compatibility - **Minimum SDK**: 29 -- **Target SDK**: 34 +- **Target SDK**: 35 - **Device Types Supported**: Phone and Tablet - **Orientations Supported**: Portrait and Landscape diff --git a/app/schemas/com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase/6.json b/app/schemas/com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase/6.json new file mode 100644 index 000000000..8d506a645 --- /dev/null +++ b/app/schemas/com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase/6.json @@ -0,0 +1,256 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "ee158c483edfe5102504670f3d9845d4", + "entities": [ + { + "tableName": "ciphers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cipherType", + "columnName": "cipher_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cipherJson", + "columnName": "cipher_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ciphers_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "collections", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shouldHidePasswords", + "columnName": "should_hide_passwords", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "externalId", + "columnName": "external_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isReadOnly", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canManage", + "columnName": "manage", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_collections_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "domains", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domainsJson", + "columnName": "domains_json", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "user_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "revisionDate", + "columnName": "revision_date", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_folders_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "sends", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sendType", + "columnName": "send_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sendJson", + "columnName": "send_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sends_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee158c483edfe5102504670f3d9845d4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt b/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt index 50617c2ff..175487965 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt @@ -15,7 +15,6 @@ import androidx.core.os.LocaleListCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager @@ -39,9 +38,6 @@ class MainActivity : AppCompatActivity() { private val mainViewModel: MainViewModel by viewModels() - @Inject - lateinit var accessibilityActivityManager: AccessibilityActivityManager - @Inject lateinit var autofillActivityManager: AutofillActivityManager diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt index a2c1a3d2a..5d4afa7a7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/PendingAuthRequestJson.kt @@ -7,13 +7,21 @@ import kotlinx.serialization.Serializable * Container for the user's API tokens. * * @property requestId The ID of the pending Auth Request. - * @property requestPrivateKey The private of the pending Auth Request. + * @property requestPrivateKey The private key of the pending Auth Request. + * @property requestAccessCode The access code of the pending Auth Request. + * @property requestFingerprint The fingerprint of the pending Auth Request. */ @Serializable data class PendingAuthRequestJson( - @SerialName("Id") + @SerialName("id") val requestId: String, - @SerialName("PrivateKey") + @SerialName("privateKey") val requestPrivateKey: String, + + @SerialName("accessCode") + val requestAccessCode: String, + + @SerialName("fingerprint") + val requestFingerprint: String, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAccountsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAccountsApi.kt index 86e3e087d..2a327fc13 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAccountsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAccountsApi.kt @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountReque import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.HTTP import retrofit2.http.POST @@ -18,43 +19,43 @@ interface AuthenticatedAccountsApi { * Converts the currently active account to a key-connector account. */ @POST("/accounts/convert-to-key-connector") - suspend fun convertToKeyConnector(): Result + suspend fun convertToKeyConnector(): NetworkResult /** * Creates the keys for the current account. */ @POST("/accounts/keys") - suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result + suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult /** * Deletes the current account. */ @HTTP(method = "DELETE", path = "/accounts", hasBody = true) - suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result + suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult @POST("/accounts/request-otp") - suspend fun requestOtp(): Result + suspend fun requestOtp(): NetworkResult @POST("/accounts/verify-otp") suspend fun verifyOtp( @Body body: VerifyOtpRequestJson, - ): Result + ): NetworkResult /** * Resets the temporary password. */ @HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true) - suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result + suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult /** * Resets the password. */ @HTTP(method = "POST", path = "/accounts/password", hasBody = true) - suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result + suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult /** * Sets the password. */ @POST("/accounts/set-password") - suspend fun setPassword(@Body body: SetPasswordRequestJson): Result + suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAuthRequestsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAuthRequestsApi.kt index 52b711f3f..d4e076c5c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAuthRequestsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedAuthRequestsApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header @@ -22,7 +23,7 @@ interface AuthenticatedAuthRequestsApi { suspend fun createAdminAuthRequest( @Header("Device-Identifier") deviceIdentifier: String, @Body body: AuthRequestRequestJson, - ): Result + ): NetworkResult /** * Updates an authentication request. @@ -31,13 +32,13 @@ interface AuthenticatedAuthRequestsApi { suspend fun updateAuthRequest( @Path("id") userId: String, @Body body: AuthRequestUpdateRequestJson, - ): Result + ): NetworkResult /** * Gets a list of auth requests for this device. */ @GET("/auth-requests") - suspend fun getAuthRequests(): Result + suspend fun getAuthRequests(): NetworkResult /** * Retrieves an existing authentication request by ID. @@ -45,5 +46,5 @@ interface AuthenticatedAuthRequestsApi { @GET("/auth-requests/{requestId}") suspend fun getAuthRequest( @Path("requestId") requestId: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt index 642464d00..56fb2b62d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedDevicesApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import androidx.annotation.Keep import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.PUT import retrofit2.http.Path @@ -16,5 +17,5 @@ interface AuthenticatedDevicesApi { suspend fun updateTrustedDeviceKeys( @Path(value = "appId") appId: String, @Body request: TrustedDeviceKeysRequestJson, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedKeyConnectorApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedKeyConnectorApi.kt index f296d7696..c06809e3e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedKeyConnectorApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedKeyConnectorApi.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import androidx.annotation.Keep import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Url @@ -15,5 +16,5 @@ interface AuthenticatedKeyConnectorApi { suspend fun storeMasterKeyToKeyConnector( @Url url: String, @Body body: KeyConnectorMasterKeyRequestJson, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedOrganizationApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedOrganizationApi.kt index 0fd01787c..23b13f082 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedOrganizationApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthenticatedOrganizationApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PUT @@ -20,7 +21,7 @@ interface AuthenticatedOrganizationApi { @Path("orgId") organizationId: String, @Path("userId") userId: String, @Body body: OrganizationResetPasswordEnrollRequestJson, - ): Result + ): NetworkResult /** * Checks whether this organization auto enrolls users in password reset. @@ -28,7 +29,7 @@ interface AuthenticatedOrganizationApi { @GET("/organizations/{identifier}/auto-enroll-status") suspend fun getOrganizationAutoEnrollResponse( @Path("identifier") organizationIdentifier: String, - ): Result + ): NetworkResult /** * Gets the public and private keys for this organization. @@ -36,5 +37,5 @@ interface AuthenticatedOrganizationApi { @GET("/organizations/{id}/keys") suspend fun getOrganizationKeys( @Path("id") organizationId: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/HaveIBeenPwnedApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/HaveIBeenPwnedApi.kt index 27d8d89b1..b67ddb2d4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/HaveIBeenPwnedApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/HaveIBeenPwnedApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Path @@ -14,5 +15,5 @@ interface HaveIBeenPwnedApi { suspend fun fetchBreachedPasswords( @Path("hashPrefix") hashPrefix: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAccountsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAccountsApi.kt index 505586e98..3015d4f87 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAccountsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAccountsApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION import retrofit2.http.Body import retrofit2.http.Header @@ -15,16 +16,16 @@ interface UnauthenticatedAccountsApi { @POST("/accounts/password-hint") suspend fun passwordHintRequest( @Body body: PasswordHintRequestJson, - ): Result + ): NetworkResult @POST("/two-factor/send-email-login") suspend fun resendVerificationCodeEmail( @Body body: ResendEmailRequestJson, - ): Result + ): NetworkResult @POST("/accounts/set-key-connector-key") suspend fun setKeyConnectorKey( @Body body: KeyConnectorKeyRequestJson, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAuthRequestsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAuthRequestsApi.kt index 36530e05f..eb61ca878 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAuthRequestsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedAuthRequestsApi.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header @@ -21,7 +22,7 @@ interface UnauthenticatedAuthRequestsApi { suspend fun createAuthRequest( @Header("Device-Identifier") deviceIdentifier: String, @Body body: AuthRequestRequestJson, - ): Result + ): NetworkResult /** * Queries for updates to a given auth request. @@ -30,5 +31,5 @@ interface UnauthenticatedAuthRequestsApi { suspend fun getAuthRequestUpdate( @Path("requestId") requestId: String, @Query("code") accessCode: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt index 3190f6248..ad00ab928 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedDevicesApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.GET import retrofit2.http.Header @@ -11,5 +12,5 @@ interface UnauthenticatedDevicesApi { suspend fun getIsKnownDevice( @Header(value = "X-Request-Email") emailAddress: String, @Header(value = "X-Device-Identifier") deviceId: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt index 343a86f6a..1be39c3fe 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import kotlinx.serialization.json.JsonPrimitive import retrofit2.Call import retrofit2.http.Body @@ -46,12 +47,12 @@ interface UnauthenticatedIdentityApi { @Field(value = "twoFactorProvider") twoFactorMethod: String?, @Field(value = "twoFactorRemember") twoFactorRemember: String?, @Field(value = "authRequest") authRequestId: String?, - ): Result + ): NetworkResult @GET("/sso/prevalidate") suspend fun prevalidateSso( @Query("domainHint") organizationIdentifier: String, - ): Result + ): NetworkResult /** * This call needs to be synchronous so we need it to return a [Call] directly. The identity @@ -66,23 +67,25 @@ interface UnauthenticatedIdentityApi { ): Call @POST("/accounts/prelogin") - suspend fun preLogin(@Body body: PreLoginRequestJson): Result + suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult @POST("/accounts/register") - suspend fun register(@Body body: RegisterRequestJson): Result + suspend fun register( + @Body body: RegisterRequestJson, + ): NetworkResult @POST("/accounts/register/finish") suspend fun registerFinish( @Body body: RegisterFinishRequestJson, - ): Result + ): NetworkResult @POST("/accounts/register/send-verification-email") suspend fun sendVerificationEmail( @Body body: SendVerificationEmailRequestJson, - ): Result + ): NetworkResult @POST("/accounts/register/verification-email-clicked") suspend fun verifyEmailToken( @Body body: VerifyEmailTokenRequestJson, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedKeyConnectorApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedKeyConnectorApi.kt index ecf803ccd..2d35a79ee 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedKeyConnectorApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedKeyConnectorApi.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api import androidx.annotation.Keep import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION import retrofit2.http.Body import retrofit2.http.GET @@ -20,11 +21,11 @@ interface UnauthenticatedKeyConnectorApi { @Url url: String, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, @Body body: KeyConnectorMasterKeyRequestJson, - ): Result + ): NetworkResult @GET suspend fun getMasterKeyFromKeyConnector( @Url url: String, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedOrganizationApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedOrganizationApi.kt index ec26cb922..d9764fdd7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedOrganizationApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedOrganizationApi.kt @@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomain import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.Body import retrofit2.http.POST @@ -17,7 +18,7 @@ interface UnauthenticatedOrganizationApi { @POST("/organizations/domain/sso/details") suspend fun getClaimedDomainOrganizationDetails( @Body body: OrganizationDomainSsoDetailsRequestJson, - ): Result + ): NetworkResult /** * Checks for the verfied organization domains of an email for SSO purposes. @@ -25,5 +26,5 @@ interface UnauthenticatedOrganizationApi { @POST("/organizations/domain/sso/verified") suspend fun getVerifiedOrganizationDomainsByEmail( @Body body: VerifiedOrganizationDomainSsoDetailsRequest, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt index 4904198fa..6e411d776 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AccountsServiceImpl.kt @@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJs import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import kotlinx.serialization.json.Json /** @@ -37,18 +38,22 @@ class AccountsServiceImpl( * Converts the currently active account to a key-connector account. */ override suspend fun convertToKeyConnector(): Result = - authenticatedAccountsApi.convertToKeyConnector() + authenticatedAccountsApi + .convertToKeyConnector() + .toResult() override suspend fun createAccountKeys( publicKey: String, encryptedPrivateKey: String, ): Result = - authenticatedAccountsApi.createAccountKeys( - body = CreateAccountKeysRequest( - publicKey = publicKey, - encryptedPrivateKey = encryptedPrivateKey, - ), - ) + authenticatedAccountsApi + .createAccountKeys( + body = CreateAccountKeysRequest( + publicKey = publicKey, + encryptedPrivateKey = encryptedPrivateKey, + ), + ) + .toResult() override suspend fun deleteAccount( masterPasswordHash: String?, @@ -61,9 +66,8 @@ class AccountsServiceImpl( oneTimePassword = oneTimePassword, ), ) - .map { - DeleteAccountResponseJson.Success - } + .toResult() + .map { DeleteAccountResponseJson.Success } .recoverCatching { throwable -> throwable .toBitwardenError() @@ -75,20 +79,25 @@ class AccountsServiceImpl( } override suspend fun requestOneTimePasscode(): Result = - authenticatedAccountsApi.requestOtp() + authenticatedAccountsApi + .requestOtp() + .toResult() override suspend fun verifyOneTimePasscode(passcode: String): Result = - authenticatedAccountsApi.verifyOtp( - VerifyOtpRequestJson( - oneTimePasscode = passcode, - ), - ) + authenticatedAccountsApi + .verifyOtp( + VerifyOtpRequestJson( + oneTimePasscode = passcode, + ), + ) + .toResult() override suspend fun requestPasswordHint( email: String, ): Result = unauthenticatedAccountsApi .passwordHintRequest(PasswordHintRequestJson(email)) + .toResult() .map { PasswordHintResponseJson.Success } .recoverCatching { throwable -> throwable @@ -101,54 +110,70 @@ class AccountsServiceImpl( } override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result = - unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body) + unauthenticatedAccountsApi + .resendVerificationCodeEmail(body = body) + .toResult() - override suspend fun resetPassword(body: ResetPasswordRequestJson): Result { - return if (body.currentPasswordHash == null) { - authenticatedAccountsApi.resetTempPassword(body = body) + override suspend fun resetPassword(body: ResetPasswordRequestJson): Result = + if (body.currentPasswordHash == null) { + authenticatedAccountsApi + .resetTempPassword(body = body) + .toResult() } else { - authenticatedAccountsApi.resetPassword(body = body) + authenticatedAccountsApi + .resetPassword(body = body) + .toResult() } - } override suspend fun setKeyConnectorKey( accessToken: String, body: KeyConnectorKeyRequestJson, - ): Result = unauthenticatedAccountsApi.setKeyConnectorKey( - body = body, - bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", - ) + ): Result = + unauthenticatedAccountsApi + .setKeyConnectorKey( + body = body, + bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", + ) + .toResult() override suspend fun setPassword( body: SetPasswordRequestJson, - ): Result = authenticatedAccountsApi.setPassword(body) + ): Result = authenticatedAccountsApi + .setPassword(body) + .toResult() override suspend fun getMasterKeyFromKeyConnector( url: String, accessToken: String, ): Result = - unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector( - url = "$url/user-keys", - bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", - ) + unauthenticatedKeyConnectorApi + .getMasterKeyFromKeyConnector( + url = "$url/user-keys", + bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", + ) + .toResult() override suspend fun storeMasterKeyToKeyConnector( url: String, masterKey: String, ): Result = - authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector( - url = "$url/user-keys", - body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), - ) + authenticatedKeyConnectorApi + .storeMasterKeyToKeyConnector( + url = "$url/user-keys", + body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), + ) + .toResult() override suspend fun storeMasterKeyToKeyConnector( url: String, accessToken: String, masterKey: String, ): Result = - unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector( - url = "$url/user-keys", - bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", - body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), - ) + unauthenticatedKeyConnectorApi + .storeMasterKeyToKeyConnector( + url = "$url/user-keys", + bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", + body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), + ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt index a6c4f5782..e9ccbc963 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt @@ -3,17 +3,22 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult class AuthRequestsServiceImpl( private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi, ) : AuthRequestsService { override suspend fun getAuthRequests(): Result = - authenticatedAuthRequestsApi.getAuthRequests() + authenticatedAuthRequestsApi + .getAuthRequests() + .toResult() override suspend fun getAuthRequest( requestId: String, ): Result = - authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId) + authenticatedAuthRequestsApi + .getAuthRequest(requestId = requestId) + .toResult() override suspend fun updateAuthRequest( requestId: String, @@ -22,13 +27,15 @@ class AuthRequestsServiceImpl( deviceId: String, isApproved: Boolean, ): Result = - authenticatedAuthRequestsApi.updateAuthRequest( - userId = requestId, - body = AuthRequestUpdateRequestJson( - key = key, - masterPasswordHash = masterPasswordHash, - deviceId = deviceId, - isApproved = isApproved, - ), - ) + authenticatedAuthRequestsApi + .updateAuthRequest( + userId = requestId, + body = AuthRequestUpdateRequestJson( + key = key, + masterPasswordHash = masterPasswordHash, + deviceId = deviceId, + isApproved = isApproved, + ), + ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt index d9efc6336..05f55a291 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevic import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult class DevicesServiceImpl( private val authenticatedDevicesApi: AuthenticatedDevicesApi, @@ -13,22 +14,26 @@ class DevicesServiceImpl( override suspend fun getIsKnownDevice( emailAddress: String, deviceId: String, - ): Result = unauthenticatedDevicesApi.getIsKnownDevice( - emailAddress = emailAddress.base64UrlEncode(), - deviceId = deviceId, - ) + ): Result = unauthenticatedDevicesApi + .getIsKnownDevice( + emailAddress = emailAddress.base64UrlEncode(), + deviceId = deviceId, + ) + .toResult() override suspend fun trustDevice( appId: String, encryptedUserKey: String, encryptedDevicePublicKey: String, encryptedDevicePrivateKey: String, - ): Result = authenticatedDevicesApi.updateTrustedDeviceKeys( - appId = appId, - request = TrustedDeviceKeysRequestJson( - encryptedUserKey = encryptedUserKey, - encryptedDevicePublicKey = encryptedDevicePublicKey, - encryptedDevicePrivateKey = encryptedDevicePrivateKey, - ), - ) + ): Result = authenticatedDevicesApi + .updateTrustedDeviceKeys( + appId = appId, + request = TrustedDeviceKeysRequestJson( + encryptedUserKey = encryptedUserKey, + encryptedDevicePublicKey = encryptedDevicePublicKey, + encryptedDevicePrivateKey = encryptedDevicePrivateKey, + ), + ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/HaveIBeenPwnedServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/HaveIBeenPwnedServiceImpl.kt index 44d744541..cabd28383 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/HaveIBeenPwnedServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/HaveIBeenPwnedServiceImpl.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import java.security.MessageDigest class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService { @@ -17,6 +18,7 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP return api .fetchBreachedPasswords(hashPrefix = hashPrefix) + .toResult() .mapCatching { responseBody -> responseBody.string() // First split the response by newline: each hashed password is on a new line. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt index 09bd21c38..5d73cace0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson @@ -68,7 +69,7 @@ interface IdentityService { */ suspend fun sendVerificationEmail( body: SendVerificationEmailRequestJson, - ): Result + ): Result /** * Register a new account to Bitwarden using email verification flow. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt index 7a32b598a..cf0efd887 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt @@ -11,13 +11,15 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode -import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult +import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider import kotlinx.serialization.json.Json @@ -28,12 +30,15 @@ class IdentityServiceImpl( ) : IdentityService { override suspend fun preLogin(email: String): Result = - unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email)) + unauthenticatedIdentityApi + .preLogin(PreLoginRequestJson(email = email)) + .toResult() @Suppress("MagicNumber") override suspend fun register(body: RegisterRequestJson): Result = unauthenticatedIdentityApi .register(body) + .toResult() .recoverCatching { throwable -> val bitwardenError = throwable.toBitwardenError() bitwardenError @@ -75,6 +80,7 @@ class IdentityServiceImpl( captchaResponse = captchaToken, authRequestId = authModel.authRequestId, ) + .toResult() .recoverCatching { throwable -> val bitwardenError = throwable.toBitwardenError() bitwardenError.parseErrorBodyOrNull( @@ -95,6 +101,7 @@ class IdentityServiceImpl( .prevalidateSso( organizationIdentifier = organizationIdentifier, ) + .toResult() override fun refreshTokenSynchronously( refreshToken: String, @@ -104,7 +111,8 @@ class IdentityServiceImpl( grantType = "refresh_token", refreshToken = refreshToken, ) - .executeForResult() + .executeForNetworkResult() + .toResult() @Suppress("MagicNumber") override suspend fun registerFinish( @@ -112,6 +120,7 @@ class IdentityServiceImpl( ): Result = unauthenticatedIdentityApi .registerFinish(body) + .toResult() .recoverCatching { throwable -> val bitwardenError = throwable.toBitwardenError() bitwardenError @@ -124,10 +133,20 @@ class IdentityServiceImpl( override suspend fun sendVerificationEmail( body: SendVerificationEmailRequestJson, - ): Result { + ): Result { return unauthenticatedIdentityApi .sendVerificationEmail(body = body) - .map { it?.content } + .toResult() + .map { SendVerificationEmailResponseJson.Success(it?.content) } + .recoverCatching { throwable -> + throwable + .toBitwardenError() + .parseErrorBodyOrNull( + code = 400, + json = json, + ) + ?: throw throwable + } } override suspend fun verifyEmailRegistrationToken( @@ -136,9 +155,8 @@ class IdentityServiceImpl( .verifyEmailToken( body = body, ) - .map { - VerifyEmailTokenResponseJson.Valid - } + .toResult() + .map { VerifyEmailTokenResponseJson.Valid } .recoverCatching { throwable -> val bitwardenError = throwable.toBitwardenError() bitwardenError diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt index 14cfb2b2c..0b0c9e51f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthR import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.platform.util.asFailure /** @@ -24,17 +25,19 @@ class NewAuthRequestServiceImpl( ): Result = when (authRequestType) { AuthRequestTypeJson.LOGIN_WITH_DEVICE -> { - unauthenticatedAuthRequestsApi.createAuthRequest( - deviceIdentifier = deviceId, - body = AuthRequestRequestJson( - email = email, - publicKey = publicKey, - deviceId = deviceId, - accessCode = accessCode, - fingerprint = fingerprint, - type = authRequestType, - ), - ) + unauthenticatedAuthRequestsApi + .createAuthRequest( + deviceIdentifier = deviceId, + body = AuthRequestRequestJson( + email = email, + publicKey = publicKey, + deviceId = deviceId, + accessCode = accessCode, + fingerprint = fingerprint, + type = authRequestType, + ), + ) + .toResult() } AuthRequestTypeJson.UNLOCK -> { @@ -43,17 +46,19 @@ class NewAuthRequestServiceImpl( } AuthRequestTypeJson.ADMIN_APPROVAL -> { - authenticatedAuthRequestsApi.createAdminAuthRequest( - deviceIdentifier = deviceId, - body = AuthRequestRequestJson( - email = email, - publicKey = publicKey, - deviceId = deviceId, - accessCode = accessCode, - fingerprint = fingerprint, - type = authRequestType, - ), - ) + authenticatedAuthRequestsApi + .createAdminAuthRequest( + deviceIdentifier = deviceId, + body = AuthRequestRequestJson( + email = email, + publicKey = publicKey, + deviceId = deviceId, + accessCode = accessCode, + fingerprint = fingerprint, + type = authRequestType, + ), + ) + .toResult() } } @@ -63,11 +68,15 @@ class NewAuthRequestServiceImpl( isSso: Boolean, ): Result = if (isSso) { - authenticatedAuthRequestsApi.getAuthRequest(requestId) + authenticatedAuthRequestsApi + .getAuthRequest(requestId = requestId) + .toResult() } else { - unauthenticatedAuthRequestsApi.getAuthRequestUpdate( - requestId = requestId, - accessCode = accessCode, - ) + unauthenticatedAuthRequestsApi + .getAuthRequestUpdate( + requestId = requestId, + accessCode = accessCode, + ) + .toResult() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/OrganizationServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/OrganizationServiceImpl.kt index f4d857da9..c01a18840 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/OrganizationServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/OrganizationServiceImpl.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysRe import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult /** * Default implementation of [OrganizationService]. @@ -31,6 +32,7 @@ class OrganizationServiceImpl( resetPasswordKey = resetPasswordKey, ), ) + .toResult() override suspend fun getOrganizationDomainSsoDetails( email: String, @@ -40,6 +42,7 @@ class OrganizationServiceImpl( email = email, ), ) + .toResult() override suspend fun getOrganizationAutoEnrollStatus( organizationIdentifier: String, @@ -47,6 +50,7 @@ class OrganizationServiceImpl( .getOrganizationAutoEnrollResponse( organizationIdentifier = organizationIdentifier, ) + .toResult() override suspend fun getOrganizationKeys( organizationId: String, @@ -54,6 +58,7 @@ class OrganizationServiceImpl( .getOrganizationKeys( organizationId = organizationId, ) + .toResult() override suspend fun getVerifiedOrganizationDomainSsoDetails( email: String, @@ -63,4 +68,5 @@ class OrganizationServiceImpl( email = email, ), ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt index 732a63e14..81f67900c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerImpl.kt @@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult import com.x8bit.bitwarden.data.auth.manager.util.isSso import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson import com.x8bit.bitwarden.data.platform.util.asFailure +import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import kotlinx.coroutines.currentCoroutineContext @@ -65,7 +66,7 @@ class AuthRequestManagerImpl( email: String, authRequestType: AuthRequestType, ): Flow = flow { - val initialResult = createNewAuthRequest( + val initialResult = createNewAuthRequestIfNecessary( email = email, authRequestType = authRequestType.toAuthRequestTypeJson(), ) @@ -74,7 +75,6 @@ class AuthRequestManagerImpl( emit(CreateAuthRequestResult.Error) return@flow } - val authRequestResponse = initialResult.authRequestResponse var authRequest = initialResult.authRequest emit(CreateAuthRequestResult.Update(authRequest)) @@ -84,7 +84,7 @@ class AuthRequestManagerImpl( newAuthRequestService .getAuthRequestUpdate( requestId = authRequest.id, - accessCode = authRequestResponse.accessCode, + accessCode = initialResult.accessCode, isSso = authRequestType.isSso, ) .map { request -> @@ -112,7 +112,8 @@ class AuthRequestManagerImpl( emit( CreateAuthRequestResult.Success( authRequest = updateAuthRequest, - authRequestResponse = authRequestResponse, + privateKey = initialResult.privateKey, + accessCode = initialResult.accessCode, ), ) } @@ -354,6 +355,52 @@ class AuthRequestManagerImpl( ) } + /** + * Creates a new auth request for the given email and returns a [NewAuthRequestData]. + * If the auth request type is [AuthRequestTypeJson.ADMIN_APPROVAL], check for a + * pending auth request and return it if it exists we should return that request. + */ + private suspend fun createNewAuthRequestIfNecessary( + email: String, + authRequestType: AuthRequestTypeJson, + ): Result { + return if (authRequestType == AuthRequestTypeJson.ADMIN_APPROVAL) { + authDiskSource + .getPendingAuthRequest(requireNotNull(activeUserId)) + ?.let { pendingAuthRequest -> + authRequestsService + .getAuthRequest(pendingAuthRequest.requestId) + .map { + NewAuthRequestData( + authRequest = AuthRequest( + id = it.id, + publicKey = it.publicKey, + platform = it.platform, + ipAddress = it.ipAddress, + key = it.key, + masterPasswordHash = it.masterPasswordHash, + creationDate = it.creationDate, + responseDate = it.responseDate, + requestApproved = it.requestApproved ?: false, + originUrl = it.originUrl, + fingerprint = pendingAuthRequest.requestFingerprint, + ), + privateKey = pendingAuthRequest.requestPrivateKey, + accessCode = pendingAuthRequest.requestAccessCode, + ) + .asSuccess() + } + .getOrNull() + } + ?: createNewAuthRequest(email = email, authRequestType = authRequestType) + } else { + createNewAuthRequest( + email = email, + authRequestType = authRequestType, + ) + } + } + /** * Attempts to create a new auth request for the given email and returns a [NewAuthRequestData] * with the [AuthRequest] and [AuthRequestResponse]. @@ -381,6 +428,8 @@ class AuthRequestManagerImpl( pendingAuthRequest = PendingAuthRequestJson( requestId = it.id, requestPrivateKey = authRequestResponse.privateKey, + requestAccessCode = authRequestResponse.accessCode, + requestFingerprint = authRequestResponse.fingerprint, ), ) } @@ -400,7 +449,13 @@ class AuthRequestManagerImpl( fingerprint = authRequestResponse.fingerprint, ) } - .map { NewAuthRequestData(it, authRequestResponse) } + .map { + NewAuthRequestData( + authRequest = it, + privateKey = authRequestResponse.privateKey, + accessCode = authRequestResponse.accessCode, + ) + } } private suspend fun getFingerprintPhrase( @@ -420,5 +475,6 @@ class AuthRequestManagerImpl( */ private data class NewAuthRequestData( val authRequest: AuthRequest, - val authRequestResponse: AuthRequestResponse, + val privateKey: String, + val accessCode: String, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/CreateAuthRequestResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/CreateAuthRequestResult.kt index c424bea1d..f73de5d69 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/CreateAuthRequestResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/model/CreateAuthRequestResult.kt @@ -1,7 +1,5 @@ package com.x8bit.bitwarden.data.auth.manager.model -import com.bitwarden.core.AuthRequestResponse - /** * Models result of creating a new login approval request. */ @@ -18,7 +16,8 @@ sealed class CreateAuthRequestResult { */ data class Success( val authRequest: AuthRequest, - val authRequestResponse: AuthRequestResponse, + val privateKey: String, + val accessCode: String, ) : CreateAuthRequestResult() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index bc76f5131..ca0521540 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository import com.bitwarden.core.AuthRequestMethod import com.bitwarden.core.InitUserCryptoMethod -import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.crypto.HashPurpose import com.bitwarden.crypto.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -23,6 +22,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJs import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod @@ -114,7 +114,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError @@ -1249,41 +1248,17 @@ class AuthRepositoryImpl( ?.activeAccount ?.profile ?: return ValidatePinResult.Error - val privateKey = authDiskSource - .getPrivateKey(userId = activeAccount.userId) - ?: return ValidatePinResult.Error val pinProtectedUserKey = authDiskSource .getPinProtectedUserKey(userId = activeAccount.userId) ?: return ValidatePinResult.Error - - // HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead - // try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise - // the PIN is incorrect. return vaultSdkSource - .initializeCrypto( + .validatePin( userId = activeAccount.userId, - request = InitUserCryptoRequest( - kdfParams = activeAccount.toSdkParams(), - email = activeAccount.email, - privateKey = privateKey, - method = InitUserCryptoMethod.Pin( - pin = pin, - pinProtectedUserKey = pinProtectedUserKey, - ), - ), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) .fold( - onSuccess = { - when (it) { - InitializeCryptoResult.Success -> { - ValidatePinResult.Success(isValid = true) - } - - is InitializeCryptoResult.AuthenticationError -> { - ValidatePinResult.Success(isValid = false) - } - } - }, + onSuccess = { ValidatePinResult.Success(isValid = it) }, onFailure = { ValidatePinResult.Error }, ) } @@ -1302,13 +1277,21 @@ class AuthRepositoryImpl( .sendVerificationEmail( SendVerificationEmailRequestJson( email = email, - name = name, + name = name.takeUnless { it.isBlank() }, receiveMarketingEmails = receiveMarketingEmails, ), ) .fold( onSuccess = { - SendVerificationEmailResult.Success(it) + when (it) { + is SendVerificationEmailResponseJson.Invalid -> { + SendVerificationEmailResult.Error(it.message) + } + + is SendVerificationEmailResponseJson.Success -> { + SendVerificationEmailResult.Success(it.emailVerificationToken) + } + } }, onFailure = { SendVerificationEmailResult.Error(null) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/JwtTokenUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/JwtTokenUtils.kt index 68a02363d..987390eb5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/JwtTokenUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/JwtTokenUtils.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository.util import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull import kotlinx.serialization.json.Json +import timber.log.Timber /** * Internal, generally basic [Json] instance for JWT parsing purposes. @@ -17,17 +18,24 @@ private val json: Json by lazy { /** * Parses a [JwtTokenDataJson] from the given [jwtToken], or `null` if this parsing is not possible. */ -@Suppress("MagicNumber") +@Suppress("MagicNumber", "TooGenericExceptionCaught") fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? { val parts = jwtToken.split(".") - if (parts.size != 3) return null + if (parts.size != 3) { + Timber.e(IllegalArgumentException("Incorrect number of parts"), "Invalid JWT Token") + return null + } val dataJson = parts[1] - val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: return null + val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: run { + Timber.e(IllegalArgumentException("Unable to decode"), "Invalid JWT Token") + return null + } return try { json.decodeFromString(decodedDataJson) - } catch (_: Throwable) { + } catch (throwable: Throwable) { + Timber.e(throwable, "Failed to decode JwtTokenDataJson") null } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt index 5c4106866..1f0911d57 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt @@ -22,8 +22,7 @@ class BitwardenAccessibilityService : AccessibilityService() { lateinit var processor: BitwardenAccessibilityProcessor override fun onAccessibilityEvent(event: AccessibilityEvent) { - if (rootInActiveWindow?.packageName != event.packageName) return - processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source) + processor.processAccessibilityEvent(event = event) { rootInActiveWindow } } override fun onInterrupt() = Unit diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt index b33b229fd..ceed9f19f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di import android.content.Context import android.content.pm.PackageManager import android.os.PowerManager +import android.view.accessibility.AccessibilityManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager @@ -55,8 +56,12 @@ object AccessibilityModule { @Singleton @Provides - fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager = - AccessibilityEnabledManagerImpl() + fun providesAccessibilityEnabledManager( + accessibilityManager: AccessibilityManager, + ): AccessibilityEnabledManager = + AccessibilityEnabledManagerImpl( + accessibilityManager = accessibilityManager, + ) @Singleton @Provides @@ -110,6 +115,12 @@ object AccessibilityModule { @ApplicationContext context: Context, ): PackageManager = context.packageManager + @Singleton + @Provides + fun provideAccessibilityManager( + @ApplicationContext context: Context, + ): AccessibilityManager = context.getSystemService(AccessibilityManager::class.java) + @Singleton @Provides fun providesPowerManager( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/ActivityAccessibilityModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/ActivityAccessibilityModule.kt deleted file mode 100644 index 636a6d15d..000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/ActivityAccessibilityModule.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.x8bit.bitwarden.data.autofill.accessibility.di - -import android.content.Context -import androidx.lifecycle.LifecycleCoroutineScope -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager -import com.x8bit.bitwarden.data.platform.manager.AppStateManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.android.scopes.ActivityScoped - -/** - * Provides dependencies within the accessibility package scoped to the activity. - */ -@Module -@InstallIn(ActivityComponent::class) -object ActivityAccessibilityModule { - @ActivityScoped - @Provides - fun providesAccessibilityActivityManager( - @ApplicationContext context: Context, - accessibilityEnabledManager: AccessibilityEnabledManager, - appStateManager: AppStateManager, - lifecycleScope: LifecycleCoroutineScope, - ): AccessibilityActivityManager = - AccessibilityActivityManagerImpl( - context = context, - accessibilityEnabledManager = accessibilityEnabledManager, - appStateManager = appStateManager, - lifecycleScope = lifecycleScope, - ) -} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManager.kt deleted file mode 100644 index 191382544..000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.x8bit.bitwarden.data.autofill.accessibility.manager - -import android.app.Activity - -/** - * A helper for dealing with accessibility configuration that must be scoped to a specific - * [Activity]. In particular, this should be injected into an [Activity] to ensure that the - * [AccessibilityEnabledManager] reports correct values. - */ -interface AccessibilityActivityManager diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerImpl.kt deleted file mode 100644 index 0777298b6..000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.x8bit.bitwarden.data.autofill.accessibility.manager - -import android.content.Context -import androidx.lifecycle.LifecycleCoroutineScope -import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled -import com.x8bit.bitwarden.data.platform.manager.AppStateManager -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -/** - * The default implementation of the [AccessibilityActivityManager]. - */ -class AccessibilityActivityManagerImpl( - private val context: Context, - private val accessibilityEnabledManager: AccessibilityEnabledManager, - appStateManager: AppStateManager, - lifecycleScope: LifecycleCoroutineScope, -) : AccessibilityActivityManager { - init { - appStateManager - .appForegroundStateFlow - .onEach { - accessibilityEnabledManager.isAccessibilityEnabled = - context.isAccessibilityServiceEnabled - } - .launchIn(lifecycleScope) - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt index 4ca327247..5c498c347 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManager.kt @@ -7,15 +7,7 @@ import kotlinx.coroutines.flow.StateFlow */ interface AccessibilityEnabledManager { /** - * Whether or not the accessibility service should be considered enabled. - * - * Note that changing this does not enable or disable autofill; it is only an indicator that - * this has occurred elsewhere. - */ - var isAccessibilityEnabled: Boolean - - /** - * Emits updates that track [isAccessibilityEnabled] values. + * Emits updates that track whether the accessibility autofill service is enabled.. */ val isAccessibilityEnabledStateFlow: StateFlow } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt index c3f434521..c9e53d0bf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerImpl.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager +import android.view.accessibility.AccessibilityManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -7,14 +8,18 @@ import kotlinx.coroutines.flow.asStateFlow /** * The default implementation of [AccessibilityEnabledManager]. */ -class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager { +class AccessibilityEnabledManagerImpl( + accessibilityManager: AccessibilityManager, +) : AccessibilityEnabledManager { private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false) - override var isAccessibilityEnabled: Boolean - get() = mutableIsAccessibilityEnabledStateFlow.value - set(value) { - mutableIsAccessibilityEnabledStateFlow.value = value - } + init { + accessibilityManager.addAccessibilityStateChangeListener( + AccessibilityManager.AccessibilityStateChangeListener { isEnabled -> + mutableIsAccessibilityEnabledStateFlow.value = isEnabled + }, + ) + } override val isAccessibilityEnabledStateFlow: StateFlow get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/FillableFields.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/FillableFields.kt index 24aeebcc7..57ac21067 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/FillableFields.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/FillableFields.kt @@ -8,4 +8,6 @@ import android.view.accessibility.AccessibilityNodeInfo data class FillableFields( val usernameField: AccessibilityNodeInfo?, val passwordFields: List, -) +) { + val hasFields: Boolean = usernameField != null || passwordFields.isNotEmpty() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt index a75227804..a08e868a3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor +import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo /** @@ -7,7 +8,12 @@ import android.view.accessibility.AccessibilityNodeInfo */ interface BitwardenAccessibilityProcessor { /** - * Processes the [AccessibilityNodeInfo] for autofill options. + * Processes the [AccessibilityEvent] for autofill options and grant access to the current + * [AccessibilityNodeInfo] via the [rootAccessibilityNodeInfoProvider] (note that calling the + * `rootAccessibilityNodeInfoProvider` is expensive). */ - fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) + fun processAccessibilityEvent( + event: AccessibilityEvent, + rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt index f80675c65..fafcbcea5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor import android.content.Context import android.os.PowerManager +import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.widget.Toast import com.x8bit.bitwarden.R @@ -25,37 +26,48 @@ class BitwardenAccessibilityProcessorImpl( private val launcherPackageNameManager: LauncherPackageNameManager, private val powerManager: PowerManager, ) : BitwardenAccessibilityProcessor { - override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) { - val rootNode = rootAccessibilityNodeInfo ?: return + override fun processAccessibilityEvent( + event: AccessibilityEvent, + rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?, + ) { + val eventNode = event.source ?: return // Ignore the event when the phone is inactive if (!powerManager.isInteractive) return // We skip if the system package - if (rootNode.isSystemPackage) return - // We skip any package that is a launcher or unsupported - if (rootNode.shouldSkipPackage || - launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName } - ) { - // Clear the action since this event needs to be ignored completely - accessibilityAutofillManager.accessibilityAction = null + if (eventNode.isSystemPackage) return + // We skip any package that is unsupported + if (eventNode.shouldSkipPackage) return + // We skip any package that is a launcher + if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) { return } // Only process the event if the tile was clicked val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return + // We only call for the root node once after all other checks + // have passed because it is significant performance hit + if (rootAccessibilityNodeInfoProvider()?.packageName != event.packageName) return + + // Clear the action since we are now acting on it accessibilityAutofillManager.accessibilityAction = null when (accessibilityAction) { is AccessibilityAction.AttemptFill -> { - handleAttemptFill(rootNode = rootNode, attemptFill = accessibilityAction) + handleAttemptFill(rootNode = eventNode, attemptFill = accessibilityAction) } - AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = rootNode) + AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = eventNode) } } private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) { accessibilityParser .parseForUriOrPackageName(rootNode = rootNode) + ?.takeIf { + accessibilityParser + .parseForFillableFields(rootNode = rootNode, uri = it) + .hasFields + } ?.let { uri -> context.startActivity( createAutofillSelectionIntent( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/api/DigitalAssetLinkApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/api/DigitalAssetLinkApi.kt index 594b5b236..dcf7b55fb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/api/DigitalAssetLinkApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/api/DigitalAssetLinkApi.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.GET import retrofit2.http.Url @@ -15,5 +16,5 @@ interface DigitalAssetLinkApi { @GET suspend fun getDigitalAssetLinks( @Url url: String, - ): Result> + ): NetworkResult> } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/service/DigitalAssetLinkServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/service/DigitalAssetLinkServiceImpl.kt index 4a48c5871..6ff84088b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/service/DigitalAssetLinkServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/fido2/datasource/network/service/DigitalAssetLinkServiceImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api.DigitalAssetLinkApi import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult /** * Primary implementation of [DigitalAssetLinkService]. @@ -18,4 +19,5 @@ class DigitalAssetLinkServiceImpl( .getDigitalAssetLinks( url = "$scheme$relyingParty/.well-known/assetlinks.json", ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index 9ed7f86e6..5f602f0eb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -74,6 +74,11 @@ interface SettingsDiskSource { */ var lastDatabaseSchemeChangeInstant: Instant? + /** + * Emits updates that track [lastDatabaseSchemeChangeInstant]. + */ + val lastDatabaseSchemeChangeInstantFlow: Flow + /** * Clears all the settings data for the given user. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index b65e022fb..3144ac84e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -75,6 +75,8 @@ class SettingsDiskSourceImpl( private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow() + private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow() + private val mutableScreenCaptureAllowedFlowMap = mutableMapOf>() @@ -158,7 +160,14 @@ class SettingsDiskSourceImpl( override var lastDatabaseSchemeChangeInstant: Instant? get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) } - set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli()) + set(value) { + putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli()) + mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value) + } + + override val lastDatabaseSchemeChangeInstantFlow: Flow + get() = mutableLastDatabaseSchemeChangeInstantFlow + .onSubscription { emit(lastDatabaseSchemeChangeInstant) } override fun clearData(userId: String) { storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/ConfigApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/ConfigApi.kt index 7b26a56df..8a381b330 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/ConfigApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/ConfigApi.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.api import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.http.GET /** @@ -9,5 +10,5 @@ import retrofit2.http.GET interface ConfigApi { @GET("config") - suspend fun getConfig(): Result + suspend fun getConfig(): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/EventApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/EventApi.kt index 6b5072a33..6ff713ae4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/EventApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/EventApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson import retrofit2.http.Body import retrofit2.http.POST @@ -9,5 +10,7 @@ import retrofit2.http.POST */ interface EventApi { @POST("/collect") - suspend fun collectOrganizationEvents(@Body events: List): Result + suspend fun collectOrganizationEvents( + @Body events: List, + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/PushApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/PushApi.kt index b6567163c..ab5216ec6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/PushApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/api/PushApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest import retrofit2.http.Body import retrofit2.http.PUT @@ -13,5 +14,5 @@ interface PushApi { suspend fun putDeviceToken( @Path("appId") appId: String, @Body body: PushTokenRequest, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCall.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCall.kt similarity index 57% rename from app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCall.kt rename to app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCall.kt index 1555e42a3..8385f0c83 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCall.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCall.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.core -import com.x8bit.bitwarden.data.platform.util.asFailure -import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import okhttp3.Request import okio.IOException import okio.Timeout @@ -18,33 +17,36 @@ import java.lang.reflect.Type private const val NO_CONTENT_RESPONSE_CODE: Int = 204 /** - * A [Call] for wrapping a network request into a [Result]. + * A [Call] for wrapping a network request into a [NetworkResult]. */ @Suppress("TooManyFunctions") -class ResultCall( +class NetworkResultCall( private val backingCall: Call, private val successType: Type, -) : Call> { +) : Call> { override fun cancel(): Unit = backingCall.cancel() - override fun clone(): Call> = ResultCall(backingCall, successType) + override fun clone(): Call> = NetworkResultCall(backingCall, successType) - override fun enqueue(callback: Callback>): Unit = backingCall.enqueue( + override fun enqueue(callback: Callback>): Unit = backingCall.enqueue( object : Callback { override fun onResponse(call: Call, response: Response) { - callback.onResponse(this@ResultCall, Response.success(response.toResult())) + callback.onResponse( + this@NetworkResultCall, + Response.success(response.toNetworkResult()), + ) } override fun onFailure(call: Call, t: Throwable) { - callback.onResponse(this@ResultCall, Response.success(t.toFailure())) + callback.onResponse(this@NetworkResultCall, Response.success(t.toFailure())) } }, ) @Suppress("TooGenericExceptionCaught") - override fun execute(): Response> = + override fun execute(): Response> = try { - Response.success(backingCall.execute().toResult()) + Response.success(backingCall.execute().toNetworkResult()) } catch (ioException: IOException) { Response.success(ioException.toFailure()) } catch (runtimeException: RuntimeException) { @@ -60,20 +62,18 @@ class ResultCall( override fun timeout(): Timeout = backingCall.timeout() /** - * Synchronously send the request and return its response as a [Result]. + * Synchronously send the request and return its response as a [NetworkResult]. */ - fun executeForResult(): Result = requireNotNull(execute().body()) + fun executeForResult(): NetworkResult = requireNotNull(execute().body()) - private fun Throwable.toFailure(): Result = - this - .also { - // We rebuild the URL without query params, we do not want to log those - val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" } - Timber.w(it, "Network Error: $url") - } - .asFailure() + private fun Throwable.toFailure(): NetworkResult { + // We rebuild the URL without query params, we do not want to log those + val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" } + Timber.w(this, "Network Error: $url") + return NetworkResult.Failure(this) + } - private fun Response.toResult(): Result = + private fun Response.toNetworkResult(): NetworkResult = if (!this.isSuccessful) { HttpException(this).toFailure() } else { @@ -81,11 +81,11 @@ class ResultCall( @Suppress("UNCHECKED_CAST") when { // We got a nonnull T as the body, just return it. - body != null -> body.asSuccess() + body != null -> NetworkResult.Success(body) // We expected the body to be null since the successType is Unit, just return Unit. - successType == Unit::class.java -> (Unit as T).asSuccess() + successType == Unit::class.java -> NetworkResult.Success(Unit as T) // We allow null for 204's, just return null. - this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess() + this.code() == NO_CONTENT_RESPONSE_CODE -> NetworkResult.Success(null as T) // All other null bodies result in an error. else -> IllegalStateException("Unexpected null body!").toFailure() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapter.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapter.kt new file mode 100644 index 000000000..4a07b7fea --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapter.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.core + +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +/** + * A [CallAdapter] for wrapping network requests into [NetworkResult]. + */ +class NetworkResultCallAdapter( + private val successType: Type, +) : CallAdapter>> { + + override fun responseType(): Type = successType + override fun adapt(call: Call): Call> = NetworkResultCall(call, successType) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterFactory.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterFactory.kt similarity index 68% rename from app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterFactory.kt rename to app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterFactory.kt index 4b808aa42..000986661 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterFactory.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterFactory.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.core +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Retrofit @@ -7,9 +8,9 @@ import java.lang.reflect.ParameterizedType import java.lang.reflect.Type /** - * A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result]. + * A [CallAdapter.Factory] for wrapping network requests into [NetworkResult]. */ -class ResultCallAdapterFactory : CallAdapter.Factory() { +class NetworkResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array, @@ -18,13 +19,13 @@ class ResultCallAdapterFactory : CallAdapter.Factory() { check(returnType is ParameterizedType) { "$returnType must be parameterized" } val containerType = getParameterUpperBound(0, returnType) - if (getRawType(containerType) != Result::class.java) return null + if (getRawType(containerType) != NetworkResult::class.java) return null check(containerType is ParameterizedType) { "$containerType must be parameterized" } val requestType = getParameterUpperBound(0, containerType) return if (getRawType(returnType) == Call::class.java) { - ResultCallAdapter(successType = requestType) + NetworkResultCallAdapter(successType = requestType) } else { null } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapter.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapter.kt deleted file mode 100644 index 88e758d18..000000000 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.x8bit.bitwarden.data.platform.datasource.network.core - -import retrofit2.Call -import retrofit2.CallAdapter -import java.lang.reflect.Type - -/** - * A [CallAdapter] for wrapping network requests into [kotlin.Result]. - */ -class ResultCallAdapter( - private val successType: Type, -) : CallAdapter>> { - - override fun responseType(): Type = successType - override fun adapt(call: Call): Call> = ResultCall(call, successType) -} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/model/NetworkResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/model/NetworkResult.kt new file mode 100644 index 000000000..54e919acb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/model/NetworkResult.kt @@ -0,0 +1,21 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.model + +import androidx.annotation.Keep + +/** + * A wrapper class for a network result for type [T]. If the network request is successful, the + * response will be a [Success] containing the data. If the network request is a failure, the + * response will be a [Failure] containing the [Throwable]. + */ +@Keep +sealed class NetworkResult { + /** + * A successful network result with the relevant [T] data. + */ + data class Success(val value: T) : NetworkResult() + + /** + * A failed network result with the relevant [throwable] error. + */ + data class Failure(val throwable: Throwable) : NetworkResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt index e681f484c..7ee544457 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt @@ -1,7 +1,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.retrofit import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator -import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory +import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCallAdapterFactory import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors @@ -105,7 +105,7 @@ class RetrofitsImpl( private val baseRetrofitBuilder: Retrofit.Builder by lazy { Retrofit.Builder() .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) - .addCallAdapterFactory(ResultCallAdapterFactory()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory()) .client(baseOkHttpClient) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/ConfigServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/ConfigServiceImpl.kt index f90b01c6c..947a2ca7f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/ConfigServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/ConfigServiceImpl.kt @@ -2,7 +2,8 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService { - override suspend fun getConfig(): Result = configApi.getConfig() + override suspend fun getConfig(): Result = configApi.getConfig().toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/EventServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/EventServiceImpl.kt index 65a459509..77e13a453 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/EventServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/EventServiceImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service import com.x8bit.bitwarden.data.platform.datasource.network.api.EventApi import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult /** * The default implementation of the [EventService]. @@ -11,5 +12,5 @@ class EventServiceImpl( ) : EventService { override suspend fun sendOrganizationEvents( events: List, - ): Result = eventApi.collectOrganizationEvents(events = events) + ): Result = eventApi.collectOrganizationEvents(events = events).toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/PushServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/PushServiceImpl.kt index 9916029c6..e83b01a3a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/PushServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/service/PushServiceImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service import com.x8bit.bitwarden.data.platform.datasource.network.api.PushApi import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult class PushServiceImpl( private val pushApi: PushApi, @@ -15,4 +16,5 @@ class PushServiceImpl( appId = appId, body = body, ) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensions.kt index d7a061f3d..b5c4e69cb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensions.kt @@ -1,21 +1,22 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util -import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCall +import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCall +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import retrofit2.Call /** - * Synchronously executes the [Call] and returns the [Result]. + * Synchronously executes the [Call] and returns the [NetworkResult]. */ -inline fun Call.executeForResult(): Result = +inline fun Call.executeForNetworkResult(): NetworkResult = this - .toResultCall() + .toNetworkResultCall() .executeForResult() /** - * Wraps the existing [Call] in a [ResultCall]. + * Wraps the existing [Call] in a [NetworkResultCall]. */ -inline fun Call.toResultCall(): ResultCall = - ResultCall( +inline fun Call.toNetworkResultCall(): NetworkResultCall = + NetworkResultCall( backingCall = this, successType = T::class.java, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensions.kt new file mode 100644 index 000000000..7f4e983bb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensions.kt @@ -0,0 +1,14 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.util + +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult +import com.x8bit.bitwarden.data.platform.util.asFailure +import com.x8bit.bitwarden.data.platform.util.asSuccess + +/** + * Converts the [NetworkResult] to a [Result]. + */ +fun NetworkResult.toResult(): Result = + when (this) { + is NetworkResult.Failure -> this.throwable.asFailure() + is NetworkResult.Success -> this.value.asSuccess() + } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManager.kt index a88cde55c..e936d7ec2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManager.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.manager +import kotlinx.coroutines.flow.Flow import java.time.Instant /** @@ -14,4 +15,9 @@ interface DatabaseSchemeManager { * that a scheme change to any database will update this value and trigger a sync. */ var lastDatabaseSchemeChangeInstant: Instant? + + /** + * A flow of the last database schema change instant. + */ + val lastDatabaseSchemeChangeInstantFlow: Flow } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerImpl.kt index 2c0fb256e..5b5d7430e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerImpl.kt @@ -1,6 +1,10 @@ package com.x8bit.bitwarden.data.platform.manager import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn import java.time.Instant /** @@ -8,10 +12,23 @@ import java.time.Instant */ class DatabaseSchemeManagerImpl( val settingsDiskSource: SettingsDiskSource, + val dispatcherManager: DispatcherManager, ) : DatabaseSchemeManager { + + private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + override var lastDatabaseSchemeChangeInstant: Instant? get() = settingsDiskSource.lastDatabaseSchemeChangeInstant set(value) { settingsDiskSource.lastDatabaseSchemeChangeInstant = value } + + override val lastDatabaseSchemeChangeInstantFlow = + settingsDiskSource + .lastDatabaseSchemeChangeInstantFlow + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Eagerly, + initialValue = settingsDiskSource.lastDatabaseSchemeChangeInstant, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt index d899a6bc9..406a799b4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow +import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState @@ -30,6 +31,7 @@ class FirstTimeActionManagerImpl @Inject constructor( private val settingsDiskSource: SettingsDiskSource, private val vaultDiskSource: VaultDiskSource, private val featureFlagManager: FeatureFlagManager, + private val autofillEnabledManager: AutofillEnabledManager, ) : FirstTimeActionManager { private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) @@ -78,7 +80,7 @@ class FirstTimeActionManagerImpl @Inject constructor( .filterNotNull() .flatMapLatest { // Can be expanded to support multiple autofill settings - settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = it) + getShowAutofillSettingBadgeFlowInternal(userId = it) .map { showAutofillBadge -> listOfNotNull(showAutofillBadge) } @@ -128,7 +130,7 @@ class FirstTimeActionManagerImpl @Inject constructor( listOf( getShowImportLoginsFlowInternal(userId = activeUserId), settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId), - settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = activeUserId), + getShowAutofillSettingBadgeFlowInternal(userId = activeUserId), getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId), ), ) { @@ -165,7 +167,7 @@ class FirstTimeActionManagerImpl @Inject constructor( FirstTimeState( showImportLoginsCard = authDiskSource.getShowImportLogins(it), showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it), - showSetupAutofillCard = settingsDiskSource.getShowAutoFillSettingBadge(it), + showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it), showImportLoginsCardInSettings = settingsDiskSource .getShowImportLoginsSettingBadge(it), ) @@ -236,4 +238,23 @@ class FirstTimeActionManagerImpl @Inject constructor( showImportLogins ?: false && ciphers.isEmpty() } } + + /** + * Internal implementation to get a flow of the showAutofill value which takes + * into account if autofill is already enabled globally. + */ + private fun getShowAutofillSettingBadgeFlowInternal(userId: String): Flow { + return settingsDiskSource + .getShowAutoFillSettingBadgeFlow(userId) + .combine( + autofillEnabledManager.isAutofillEnabledStateFlow, + ) { showAutofill, autofillEnabled -> + showAutofill ?: false && !autofillEnabled + } + } + + private fun getShowAutofillSettingBadgeInternal(userId: String): Boolean { + return settingsDiskSource.getShowAutoFillSettingBadge(userId) ?: false && + !autofillEnabledManager.isAutofillEnabled + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index b962ccb8f..f09e31143 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -6,6 +6,7 @@ import androidx.core.content.getSystemService import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource @@ -293,19 +294,23 @@ object PlatformManagerModule { vaultDiskSource: VaultDiskSource, dispatcherManager: DispatcherManager, featureFlagManager: FeatureFlagManager, + autofillEnabledManager: AutofillEnabledManager, ): FirstTimeActionManager = FirstTimeActionManagerImpl( authDiskSource = authDiskSource, settingsDiskSource = settingsDiskSource, vaultDiskSource = vaultDiskSource, dispatcherManager = dispatcherManager, featureFlagManager = featureFlagManager, + autofillEnabledManager = autofillEnabledManager, ) @Provides @Singleton fun provideDatabaseSchemeManager( settingsDiskSource: SettingsDiskSource, + dispatcherManager: DispatcherManager, ): DatabaseSchemeManager = DatabaseSchemeManagerImpl( settingsDiskSource = settingsDiskSource, + dispatcherManager = dispatcherManager, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/processor/AuthenticatorBridgeProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/processor/AuthenticatorBridgeProcessorImpl.kt index 29df0ed42..97e7c11f1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/processor/AuthenticatorBridgeProcessorImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/processor/AuthenticatorBridgeProcessorImpl.kt @@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber /** * Default implementation of [AuthenticatorBridgeProcessor]. @@ -93,7 +94,13 @@ class AuthenticatorBridgeProcessorImpl( } override fun syncAccounts() { - val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return + val symmetricEncryptionKey = symmetricEncryptionKeyData ?: run { + Timber.e( + t = IllegalStateException(), + message = "Unable to sync accounts when symmetricEncryptionKeyData is null.", + ) + return + } scope.launch { // Encrypt the shared account data with the symmetric key: val encryptedSharedAccountData = authenticatorBridgeRepository @@ -110,14 +117,31 @@ class AuthenticatorBridgeProcessorImpl( } override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean { - val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return false + val symmetricEncryptionKey = symmetricEncryptionKeyData ?: run { + Timber.e( + t = IllegalStateException(), + message = "Unable to start add TOTP item flow when " + + "symmetricEncryptionKeyData is null.", + ) + return false + } val intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext) val totpData = data.decrypt(symmetricEncryptionKey) + .onFailure { + Timber.e(t = it, message = "Unable to decrypt TOTP data.") + return false + } .getOrNull() ?.totpUri ?.toUri() ?.getTotpDataOrNull() - ?: return false + ?: run { + Timber.e( + t = IllegalStateException(), + message = "Unable to parse TOTP URI.", + ) + return false + } addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData applicationContext.startActivity(intent) return true diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt index 7784700e8..6b1a51775 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt @@ -14,9 +14,16 @@ inline fun Result.flatMap(transform: (T) -> Result): Result = /** * Returns the given receiver of type [T] as a "success" [Result]. + * + * Note that this will never double wrap the `Result` and we return the original value if [T] is + * already an instance of `Result` */ -fun T.asSuccess(): Result = +fun T.asSuccess(): Result = if (this is Result<*>) { + @Suppress("UNCHECKED_CAST") + this as Result +} else { Result.success(this) +} /** * Returns the given [Throwable] as a "failure" [Result]. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt index 204fe787e..9631f1441 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -125,7 +124,6 @@ class VaultDiskSourceImpl( override fun getDomains(userId: String): Flow = domainsDao .getDomains(userId) - .filterNotNull() .map { entity -> withContext(dispatcherManager.default) { entity?.domainsJson?.let { json.decodeFromString(it) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt index 19dcdc75d..8c7ca5cbf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt @@ -26,7 +26,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity FolderEntity::class, SendEntity::class, ], - version = 5, + version = 6, exportSchema = true, ) @TypeConverters(ZonedDateTimeTypeConverter::class) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/CollectionEntity.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/CollectionEntity.kt index 5370642f9..15aaf69dd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/CollectionEntity.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/CollectionEntity.kt @@ -32,5 +32,5 @@ data class CollectionEntity( val isReadOnly: Boolean, @ColumnInfo(name = "manage") - val canManage: Boolean, + val canManage: Boolean?, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt index 42adf069a..ab9a21d21 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/AzureApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.Header @@ -21,5 +22,5 @@ interface AzureApi { @Header("x-ms-date") date: String, @Header("x-ms-version") version: String?, @Body body: RequestBody, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt index 84c6c35cd..47f39f3bc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/CiphersApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest @@ -26,7 +27,7 @@ interface CiphersApi { * Create a cipher. */ @POST("ciphers") - suspend fun createCipher(@Body body: CipherJsonRequest): Result + suspend fun createCipher(@Body body: CipherJsonRequest): NetworkResult /** * Create a cipher that belongs to an organization. @@ -34,7 +35,7 @@ interface CiphersApi { @POST("ciphers/create") suspend fun createCipherInOrganization( @Body body: CreateCipherInOrganizationJsonRequest, - ): Result + ): NetworkResult /** * Associates an attachment with a cipher. @@ -43,7 +44,7 @@ interface CiphersApi { suspend fun createAttachment( @Path("cipherId") cipherId: String, @Body body: AttachmentJsonRequest, - ): Result + ): NetworkResult /** * Uploads the attachment associated with a cipher. @@ -53,7 +54,7 @@ interface CiphersApi { @Path("cipherId") cipherId: String, @Path("attachmentId") attachmentId: String, @Body body: MultipartBody, - ): Result + ): NetworkResult /** * Updates a cipher. @@ -62,7 +63,7 @@ interface CiphersApi { suspend fun updateCipher( @Path("cipherId") cipherId: String, @Body body: CipherJsonRequest, - ): Result + ): NetworkResult /** * Shares a cipher. @@ -71,7 +72,7 @@ interface CiphersApi { suspend fun shareCipher( @Path("cipherId") cipherId: String, @Body body: ShareCipherJsonRequest, - ): Result + ): NetworkResult /** * Shares an attachment. @@ -82,7 +83,7 @@ interface CiphersApi { @Path("attachmentId") attachmentId: String, @Query("organizationId") organizationId: String?, @Body body: MultipartBody, - ): Result + ): NetworkResult /** * Updates a cipher's collections. @@ -91,7 +92,7 @@ interface CiphersApi { suspend fun updateCipherCollections( @Path("cipherId") cipherId: String, @Body body: UpdateCipherCollectionsJsonRequest, - ): Result + ): NetworkResult /** * Hard deletes a cipher. @@ -99,7 +100,7 @@ interface CiphersApi { @DELETE("ciphers/{cipherId}") suspend fun hardDeleteCipher( @Path("cipherId") cipherId: String, - ): Result + ): NetworkResult /** * Soft deletes a cipher. @@ -107,7 +108,7 @@ interface CiphersApi { @PUT("ciphers/{cipherId}/delete") suspend fun softDeleteCipher( @Path("cipherId") cipherId: String, - ): Result + ): NetworkResult /** * Deletes an attachment from a cipher. @@ -116,7 +117,7 @@ interface CiphersApi { suspend fun deleteCipherAttachment( @Path("cipherId") cipherId: String, @Path("attachmentId") attachmentId: String, - ): Result + ): NetworkResult /** * Restores a cipher. @@ -124,7 +125,7 @@ interface CiphersApi { @PUT("ciphers/{cipherId}/restore") suspend fun restoreCipher( @Path("cipherId") cipherId: String, - ): Result + ): NetworkResult /** * Gets a cipher. @@ -132,7 +133,7 @@ interface CiphersApi { @GET("ciphers/{cipherId}") suspend fun getCipher( @Path("cipherId") cipherId: String, - ): Result + ): NetworkResult /** * Gets a cipher attachment. @@ -141,11 +142,11 @@ interface CiphersApi { suspend fun getCipherAttachment( @Path("cipherId") cipherId: String, @Path("attachmentId") attachmentId: String, - ): Result + ): NetworkResult /** * Indicates if the active user has unassigned ciphers. */ @GET("ciphers/has-unassigned-ciphers") - suspend fun hasUnassignedCiphers(): Result + suspend fun hasUnassignedCiphers(): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/DownloadApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/DownloadApi.kt index ae7bd4471..8a1b41f0c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/DownloadApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/DownloadApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import okhttp3.ResponseBody import retrofit2.http.GET import retrofit2.http.Streaming @@ -16,5 +17,5 @@ interface DownloadApi { @Streaming suspend fun getDataStream( @Url url: String, - ): Result + ): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt index 92097ade2..e301f1c58 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/FoldersApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import retrofit2.http.Body @@ -18,7 +19,7 @@ interface FoldersApi { * Create a folder. */ @POST("folders") - suspend fun createFolder(@Body body: FolderJsonRequest): Result + suspend fun createFolder(@Body body: FolderJsonRequest): NetworkResult /** * Gets a folder. @@ -26,7 +27,7 @@ interface FoldersApi { @GET("folders/{folderId}") suspend fun getFolder( @Path("folderId") folderId: String, - ): Result + ): NetworkResult /** * Updates a folder. @@ -35,11 +36,11 @@ interface FoldersApi { suspend fun updateFolder( @Path("folderId") folderId: String, @Body body: FolderJsonRequest, - ): Result + ): NetworkResult /** * Deletes a folder. */ @DELETE("folders/{folderId}") - suspend fun deleteFolder(@Path("folderId") folderId: String): Result + suspend fun deleteFolder(@Path("folderId") folderId: String): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt index 102ff6a5e..fc5fe2cee 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SendsApi.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api import androidx.annotation.Keep +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -22,13 +23,15 @@ interface SendsApi { * Create a text send. */ @POST("sends") - suspend fun createTextSend(@Body body: SendJsonRequest): Result + suspend fun createTextSend(@Body body: SendJsonRequest): NetworkResult /** * Create a file send. */ @POST("sends/file/v2") - suspend fun createFileSend(@Body body: SendJsonRequest): Result + suspend fun createFileSend( + @Body body: SendJsonRequest, + ): NetworkResult /** * Updates a send. @@ -37,7 +40,7 @@ interface SendsApi { suspend fun updateSend( @Path("sendId") sendId: String, @Body body: SendJsonRequest, - ): Result + ): NetworkResult /** * Uploads the file associated with a send. @@ -47,23 +50,25 @@ interface SendsApi { @Path("sendId") sendId: String, @Path("fileId") fileId: String, @Body body: MultipartBody, - ): Result + ): NetworkResult /** * Deletes a send. */ @DELETE("sends/{sendId}") - suspend fun deleteSend(@Path("sendId") sendId: String): Result + suspend fun deleteSend(@Path("sendId") sendId: String): NetworkResult /** * Deletes a send. */ @PUT("sends/{sendId}/remove-password") - suspend fun removeSendPassword(@Path("sendId") sendId: String): Result + suspend fun removeSendPassword( + @Path("sendId") sendId: String, + ): NetworkResult /** * Gets a send. */ @GET("sends/{sendId}") - suspend fun getSend(@Path("sendId") sendId: String): Result + suspend fun getSend(@Path("sendId") sendId: String): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SyncApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SyncApi.kt index 1f1c7ece7..dcc2a3c72 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SyncApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/api/SyncApi.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.api +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import retrofit2.http.GET @@ -13,8 +14,8 @@ interface SyncApi { * @return A [SyncResponseJson] containing the vault response model. */ @GET("sync") - suspend fun sync(): Result + suspend fun sync(): NetworkResult @GET("/accounts/revision-date") - suspend fun getAccountRevisionDateMillis(): Result + suspend fun getAccountRevisionDateMillis(): NetworkResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt index d8aec4b85..046747407 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseJson.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.data.vault.datasource.network.model import kotlinx.serialization.Contextual +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames import kotlinx.serialization.json.JsonObject import java.time.ZonedDateTime @@ -21,6 +23,7 @@ private const val DEFAULT_FIDO_2_KEY_CURVE = "P-256" * @property domains A domains object associated with the vault data. * @property sends A list of send objects associated with the vault data (nullable). */ +@OptIn(ExperimentalSerializationApi::class) @Serializable data class SyncResponseJson( @SerialName("folders") @@ -30,6 +33,7 @@ data class SyncResponseJson( val collections: List?, @SerialName("profile") + @JsonNames("Profile") val profile: Profile, @SerialName("ciphers") @@ -39,6 +43,7 @@ data class SyncResponseJson( val policies: List?, @SerialName("domains") + @JsonNames("Domains") val domains: Domains?, @SerialName("sends") @@ -971,6 +976,6 @@ data class SyncResponseJson( val id: String, @SerialName("manage") - val canManage: Boolean, + val canManage: Boolean?, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt index 7610fa77e..b4a61c39e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceImpl.kt @@ -4,6 +4,7 @@ import androidx.core.net.toUri import com.bitwarden.vault.Attachment import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi @@ -34,20 +35,26 @@ class CiphersServiceImpl( private val clock: Clock, ) : CiphersService { override suspend fun createCipher(body: CipherJsonRequest): Result = - ciphersApi.createCipher(body = body) + ciphersApi + .createCipher(body = body) + .toResult() override suspend fun createCipherInOrganization( body: CreateCipherInOrganizationJsonRequest, - ): Result = ciphersApi.createCipherInOrganization(body = body) + ): Result = ciphersApi + .createCipherInOrganization(body = body) + .toResult() override suspend fun createAttachment( cipherId: String, body: AttachmentJsonRequest, ): Result = - ciphersApi.createAttachment( - cipherId = cipherId, - body = body, - ) + ciphersApi + .createAttachment( + cipherId = cipherId, + body = body, + ) + .toResult() override suspend fun uploadAttachment( attachmentJsonResponse: AttachmentJsonResponse, @@ -82,6 +89,7 @@ class CiphersServiceImpl( ) } } + .toResult() .map { cipher } } @@ -94,6 +102,7 @@ class CiphersServiceImpl( cipherId = cipherId, body = body, ) + .toResult() .map { UpdateCipherResponseJson.Success(cipher = it) } .recoverCatching { throwable -> throwable @@ -115,77 +124,97 @@ class CiphersServiceImpl( ?: return IllegalStateException("Attachment must have ID").asFailure() val attachmentKey = attachment.key ?: return IllegalStateException("Attachment must have Key").asFailure() - return ciphersApi.shareAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - organizationId = organizationId, - body = this - .createMultipartBodyBuilder( - encryptedFile = encryptedFile, - filename = attachment.fileName, - ) - .addPart( - part = MultipartBody.Part.createFormData( - name = "key", - value = attachmentKey, - ), - ) - .build(), - ) + return ciphersApi + .shareAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + organizationId = organizationId, + body = this + .createMultipartBodyBuilder( + encryptedFile = encryptedFile, + filename = attachment.fileName, + ) + .addPart( + part = MultipartBody.Part.createFormData( + name = "key", + value = attachmentKey, + ), + ) + .build(), + ) + .toResult() } override suspend fun shareCipher( cipherId: String, body: ShareCipherJsonRequest, ): Result = - ciphersApi.shareCipher( - cipherId = cipherId, - body = body, - ) + ciphersApi + .shareCipher( + cipherId = cipherId, + body = body, + ) + .toResult() override suspend fun updateCipherCollections( cipherId: String, body: UpdateCipherCollectionsJsonRequest, ): Result = - ciphersApi.updateCipherCollections( - cipherId = cipherId, - body = body, - ) + ciphersApi + .updateCipherCollections( + cipherId = cipherId, + body = body, + ) + .toResult() override suspend fun hardDeleteCipher(cipherId: String): Result = - ciphersApi.hardDeleteCipher(cipherId = cipherId) + ciphersApi + .hardDeleteCipher(cipherId = cipherId) + .toResult() override suspend fun softDeleteCipher(cipherId: String): Result = - ciphersApi.softDeleteCipher(cipherId = cipherId) + ciphersApi + .softDeleteCipher(cipherId = cipherId) + .toResult() override suspend fun deleteCipherAttachment( cipherId: String, attachmentId: String, ): Result = - ciphersApi.deleteCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - ) + ciphersApi + .deleteCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + ) + .toResult() override suspend fun restoreCipher(cipherId: String): Result = - ciphersApi.restoreCipher(cipherId = cipherId) + ciphersApi + .restoreCipher(cipherId = cipherId) + .toResult() override suspend fun getCipher( cipherId: String, ): Result = - ciphersApi.getCipher(cipherId = cipherId) + ciphersApi + .getCipher(cipherId = cipherId) + .toResult() override suspend fun getCipherAttachment( cipherId: String, attachmentId: String, ): Result = - ciphersApi.getCipherAttachment( - cipherId = cipherId, - attachmentId = attachmentId, - ) + ciphersApi + .getCipherAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + ) + .toResult() override suspend fun hasUnassignedCiphers(): Result = - ciphersApi.hasUnassignedCiphers() + ciphersApi + .hasUnassignedCiphers() + .toResult() private fun createMultipartBodyBuilder( encryptedFile: File, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/DownloadServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/DownloadServiceImpl.kt index 50e915362..cea6da4d6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/DownloadServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/DownloadServiceImpl.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.vault.datasource.network.api.DownloadApi import okhttp3.ResponseBody @@ -12,5 +13,7 @@ class DownloadServiceImpl( override suspend fun getDataStream( url: String, ): Result = - downloadApi.getDataStream(url = url) + downloadApi + .getDataStream(url = url) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt index 572e87500..c1d7090fc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/FolderServiceImpl.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.vault.datasource.network.api.FoldersApi import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -13,7 +14,9 @@ class FolderServiceImpl( private val json: Json, ) : FolderService { override suspend fun createFolder(body: FolderJsonRequest): Result = - foldersApi.createFolder(body = body) + foldersApi + .createFolder(body = body) + .toResult() override suspend fun updateFolder( folderId: String, @@ -24,6 +27,7 @@ class FolderServiceImpl( folderId = folderId, body = body, ) + .toResult() .map { UpdateFolderResponseJson.Success(folder = it) } .recoverCatching { throwable -> throwable @@ -36,10 +40,13 @@ class FolderServiceImpl( } override suspend fun deleteFolder(folderId: String): Result = - foldersApi.deleteFolder(folderId = folderId) + foldersApi + .deleteFolder(folderId = folderId) + .toResult() override suspend fun getFolder( folderId: String, ): Result = foldersApi .getFolder(folderId = folderId) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt index d8104447d..63647394c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SendsServiceImpl.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service import androidx.core.net.toUri import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse @@ -34,7 +35,9 @@ class SendsServiceImpl( override suspend fun createTextSend( body: SendJsonRequest, ): Result = - sendsApi.createTextSend(body = body) + sendsApi + .createTextSend(body = body) + .toResult() .map { CreateSendJsonResponse.Success(send = it) } .recoverCatching { throwable -> throwable.toBitwardenError() @@ -48,7 +51,9 @@ class SendsServiceImpl( override suspend fun createFileSend( body: SendJsonRequest, ): Result = - sendsApi.createFileSend(body = body) + sendsApi + .createFileSend(body = body) + .toResult() .map { CreateFileSendResponse.Success(it) } .recoverCatching { throwable -> throwable.toBitwardenError() @@ -68,6 +73,7 @@ class SendsServiceImpl( sendId = sendId, body = body, ) + .toResult() .map { UpdateSendResponseJson.Success(send = it) } .recoverCatching { throwable -> throwable @@ -118,16 +124,20 @@ class SendsServiceImpl( ) } } + .toResult() .onFailure { sendsApi.deleteSend(send.id) } .map { send } } override suspend fun deleteSend(sendId: String): Result = - sendsApi.deleteSend(sendId = sendId) + sendsApi + .deleteSend(sendId = sendId) + .toResult() override suspend fun removeSendPassword(sendId: String): Result = sendsApi .removeSendPassword(sendId = sendId) + .toResult() .map { UpdateSendResponseJson.Success(send = it) } .recoverCatching { throwable -> throwable @@ -142,5 +152,7 @@ class SendsServiceImpl( override suspend fun getSend( sendId: String, ): Result = - sendsApi.getSend(sendId = sendId) + sendsApi + .getSend(sendId = sendId) + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceImpl.kt index 57e478dd7..bfa8a0f3c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/network/service/SyncServiceImpl.kt @@ -1,13 +1,18 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service +import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson class SyncServiceImpl( private val syncApi: SyncApi, ) : SyncService { - override suspend fun sync(): Result = syncApi.sync() + override suspend fun sync(): Result = syncApi + .sync() + .toResult() override suspend fun getAccountRevisionDateMillis(): Result = - syncApi.getAccountRevisionDateMillis() + syncApi + .getAccountRevisionDateMillis() + .toResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt index 4e26eb042..120f50176 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt @@ -94,6 +94,15 @@ interface VaultSdkSource { encryptedPin: String, ): Result + /** + * Validate the user pin using the [pinProtectedUserKey]. + */ + suspend fun validatePin( + userId: String, + pin: String, + pinProtectedUserKey: String, + ): Result + /** * Gets the key for an auth request that is required to approve or decline it. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt index 2c33c7450..95de2ec67 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt @@ -109,6 +109,17 @@ class VaultSdkSourceImpl( .derivePinUserKey(encryptedPin = encryptedPin) } + override suspend fun validatePin( + userId: String, + pin: String, + pinProtectedUserKey: String, + ): Result = + runCatchingWithLogs { + getClient(userId = userId) + .auth() + .validatePin(pin = pin, pinProtectedUserKey = pinProtectedUserKey) + } + override suspend fun getAuthRequestKey( publicKey: String, userId: String, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 12ac59c11..7c60bbcec 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -99,6 +99,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest @@ -323,6 +324,12 @@ class VaultRepositoryImpl( .syncFolderUpsertFlow .onEach(::syncFolderIfNecessary) .launchIn(ioScope) + + databaseSchemeManager + .lastDatabaseSchemeChangeInstantFlow + .filterNotNull() + .onEach { sync() } + .launchIn(ioScope) } private fun clearUnlockedData() { diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensions.kt index 42d9e373e..1d2283f33 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensions.kt @@ -17,7 +17,7 @@ fun SyncResponseJson.Collection.toEncryptedSdkCollection(): Collection = externalId = this.externalId, hidePasswords = this.shouldHidePasswords, readOnly = this.isReadOnly, - manage = this.canManage, + manage = this.canManage ?: !this.isReadOnly, ) /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt index 9d18975fa..3398db221 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.rememberScrollState @@ -134,14 +133,13 @@ fun SetupAutoFillScreen( }, ) }, - ) { innerPadding -> + ) { SetupAutoFillContent( state = state, onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) }, onContinueClick = handler.onContinueClick, onTurnOnLaterClick = handler.onTurnOnLaterClick, modifier = Modifier - .padding(innerPadding) .verticalScroll(rememberScrollState()) .fillMaxSize(), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt index b45b83512..2d23678e7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -62,11 +61,9 @@ fun SetupCompleteScreen( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> + ) { SetupCompleteContent( - modifier = Modifier - .padding(innerPadding) - .verticalScroll(rememberScrollState()), + modifier = Modifier.verticalScroll(rememberScrollState()), onContinue = setupCompleteAction, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt index 8348bb5e1..c3e748901 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt @@ -128,15 +128,13 @@ fun SetupUnlockScreen( }, ) }, - ) { innerPadding -> + ) { SetupUnlockScreenContent( state = state, showBiometricsPrompt = showBiometricsPrompt, handler = handler, biometricsManager = biometricsManager, - modifier = Modifier - .padding(paddingValues = innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt index 1e61245af..458e2a454 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt @@ -76,6 +76,11 @@ class SetupUnlockViewModel @Inject constructor( } private fun handleCloseClick() { + // If the user has enabled biometric or PIN lock, but then closes the screen we + // want to dismiss the action card. + if (state.isContinueButtonEnabled) { + firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false) + } sendEvent(SetupUnlockEvent.NavigateBack) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailScreen.kt index 8848b9ea2..376758126 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailScreen.kt @@ -91,10 +91,9 @@ fun CheckEmailScreen( onNavigationIconClick = handler.onBackClick, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .imePadding() .fillMaxSize() .verticalScroll(rememberScrollState()), @@ -298,7 +297,7 @@ private fun CheckEmailLegacyContent( ), highlights = listOf( ClickableTextHighlight( - textToHighlight = stringResource(id = R.string.log_in), + textToHighlight = stringResource(id = R.string.log_in_verb), onTextClick = onLoginClick, ), ), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index 102a162fa..1b6306f17 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -158,10 +157,9 @@ fun CompleteRegistrationScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .imePadding() .fillMaxSize() .verticalScroll(rememberScrollState()), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt index abdc86b3f..7835542d4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt @@ -42,14 +42,20 @@ fun PasswordStrengthIndicator( currentCharacterCount: Int, minimumCharacterCount: Int? = null, ) { + val minimumRequirementMet = (minimumCharacterCount == null) || + (currentCharacterCount >= minimumCharacterCount) val widthPercent by animateFloatAsState( - targetValue = when (state) { - PasswordStrengthState.NONE -> 0f - PasswordStrengthState.WEAK_1 -> .25f - PasswordStrengthState.WEAK_2 -> .5f - PasswordStrengthState.WEAK_3 -> .66f - PasswordStrengthState.GOOD -> .82f - PasswordStrengthState.STRONG -> 1f + targetValue = if (minimumRequirementMet) { + when (state) { + PasswordStrengthState.NONE -> 0f + PasswordStrengthState.WEAK_1 -> .25f + PasswordStrengthState.WEAK_2 -> .5f + PasswordStrengthState.WEAK_3 -> .66f + PasswordStrengthState.GOOD -> .82f + PasswordStrengthState.STRONG -> 1f + } + } else { + 0f }, label = "Width Percent State", ) @@ -107,11 +113,13 @@ fun PasswordStrengthIndicator( minimumCharacterCount = minCount, ) } - Text( - text = label(), - style = BitwardenTheme.typography.labelSmall, - color = indicatorColor, - ) + if (minimumRequirementMet) { + Text( + text = label(), + style = BitwardenTheme.typography.labelSmall, + color = indicatorColor, + ) + } } } } @@ -122,14 +130,6 @@ private fun MinimumCharacterCount( minimumRequirementMet: Boolean, minimumCharacterCount: Int, ) { - val characterCountColor by animateColorAsState( - targetValue = if (minimumRequirementMet) { - BitwardenTheme.colorScheme.status.strong - } else { - BitwardenTheme.colorScheme.text.secondary - }, - label = "minmumCharacterCountColor", - ) Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, @@ -145,14 +145,14 @@ private fun MinimumCharacterCount( Icon( painter = rememberVectorPainter(id = it), contentDescription = null, - tint = characterCountColor, + tint = BitwardenTheme.colorScheme.text.secondary, modifier = Modifier.size(12.dp), ) } Spacer(modifier = Modifier.width(2.dp)) Text( text = stringResource(R.string.minimum_characters, minimumCharacterCount), - color = characterCountColor, + color = BitwardenTheme.colorScheme.text.secondary, style = BitwardenTheme.typography.labelSmall, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt index be2562f41..a3b47439c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt @@ -1,14 +1,11 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount import android.widget.Toast -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -17,31 +14,23 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri @@ -76,7 +65,6 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch -import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager @@ -187,10 +175,9 @@ fun CreateAccountScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .imePadding() .fillMaxSize() .verticalScroll(rememberScrollState()), @@ -284,6 +271,9 @@ fun CreateAccountScreen( onPrivacyPolicyClick = remember(viewModel) { { viewModel.trySendAction(PrivacyPolicyClick) } }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.navigationBarsPadding()) } @@ -291,52 +281,24 @@ fun CreateAccountScreen( } @OptIn(ExperimentalLayoutApi::class) -@Suppress("LongMethod") @Composable private fun TermsAndPrivacySwitch( isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onTermsClick: () -> Unit, onPrivacyPolicyClick: () -> Unit, + modifier: Modifier = Modifier, ) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .semantics(mergeDescendants = true) { - testTag = "AcceptPoliciesToggle" - toggleableState = ToggleableState(isChecked) - } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onCheckedChange.invoke(!isChecked) }, - ) - .padding(start = 16.dp) - .fillMaxWidth(), - ) { - Switch( - modifier = Modifier - .height(32.dp) - .width(52.dp), - checked = isChecked, - onCheckedChange = null, - colors = bitwardenSwitchColors(), - ) - Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) { - Text( - text = stringResource(id = R.string.accept_policies), - style = BitwardenTheme.typography.bodyLarge, - color = BitwardenTheme.colorScheme.text.primary, - ) + BitwardenSwitch( + modifier = modifier, + label = stringResource(id = R.string.accept_policies), + isChecked = isChecked, + contentDescription = "AcceptPoliciesToggle", + onCheckedChange = onCheckedChange, + subContent = { FlowRow( horizontalArrangement = Arrangement.Start, - modifier = Modifier - .padding(end = 16.dp) - .fillMaxWidth() - .wrapContentHeight(), + modifier = Modifier.fillMaxWidth(), ) { BitwardenClickableText( label = stringResource(id = R.string.terms_of_service), @@ -358,6 +320,6 @@ private fun TermsAndPrivacySwitch( innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), ) } - } - } + }, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt index 40042fc84..8d6515755 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt @@ -117,7 +117,7 @@ fun EnterpriseSignOnScreen( }, actions = { BitwardenTextButton( - label = stringResource(id = R.string.log_in), + label = stringResource(id = R.string.log_in_verb), onClick = remember(viewModel) { { viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) } }, @@ -126,15 +126,13 @@ fun EnterpriseSignOnScreen( }, ) }, - ) { innerPadding -> + ) { EnterpriseSignOnScreenContent( state = state, onOrgIdentifierInputChange = remember(viewModel) { { viewModel.trySendAction(EnterpriseSignOnAction.OrgIdentifierInputChange(it)) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt index 104324115..e627a6848 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt @@ -98,10 +98,9 @@ fun EnvironmentScreen( }, ) }, - ) { innerPadding -> + ) { Column( - Modifier - .padding(innerPadding) + modifier = Modifier .fillMaxSize() .imePadding() .verticalScroll(rememberScrollState()), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkScreen.kt index f90d2a711..4ea319c75 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -72,7 +71,7 @@ fun ExpiredRegistrationLinkScreen( ), ) }, - ) { innerPadding -> + ) { ExpiredRegistrationLinkContent( onNavigateToLogin = remember(viewModel) { { @@ -87,7 +86,6 @@ fun ExpiredRegistrationLinkScreen( } }, modifier = Modifier - .padding(innerPadding) .fillMaxSize() .verticalScroll(rememberScrollState()), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index 62b34b203..7643107b5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -148,7 +148,29 @@ fun LandingScreen( ) } }, - ) { innerPadding -> + overlay = { + BitwardenAccountSwitcher( + isVisible = isAccountMenuVisible, + accountSummaries = state.accountSummaries.toImmutableList(), + onSwitchAccountClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.SwitchAccountClick(it)) } + }, + onLockAccountClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.LockAccountClick(it)) } + }, + onLogoutAccountClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.LogoutAccountClick(it)) } + }, + onAddAccountClick = { + // Not available + }, + onDismissRequest = { isAccountMenuVisible = false }, + isAddAccountAvailable = false, + topAppBarScrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxSize(), + ) + }, + ) { LandingScreenContent( state = state, isAppBarVisible = isAppBarVisible, @@ -167,32 +189,7 @@ fun LandingScreen( onCreateAccountClick = remember(viewModel) { { viewModel.trySendAction(LandingAction.CreateAccountClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - ) - - BitwardenAccountSwitcher( - isVisible = isAccountMenuVisible, - accountSummaries = state.accountSummaries.toImmutableList(), - onSwitchAccountClick = remember(viewModel) { - { viewModel.trySendAction(LandingAction.SwitchAccountClick(it)) } - }, - onLockAccountClick = remember(viewModel) { - { viewModel.trySendAction(LandingAction.LockAccountClick(it)) } - }, - onLogoutAccountClick = remember(viewModel) { - { viewModel.trySendAction(LandingAction.LogoutAccountClick(it)) } - }, - onAddAccountClick = { - // Not available - }, - onDismissRequest = { isAccountMenuVisible = false }, - isAddAccountAvailable = false, - topAppBarScrollBehavior = scrollBehavior, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index 03faeeb49..d1c872ada 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -145,7 +145,28 @@ fun LoginScreen( }, ) }, - ) { innerPadding -> + overlay = { + BitwardenAccountSwitcher( + isVisible = isAccountMenuVisible, + accountSummaries = state.accountSummaries.toImmutableList(), + onSwitchAccountClick = remember(viewModel) { + { viewModel.trySendAction(LoginAction.SwitchAccountClick(it)) } + }, + onLockAccountClick = remember(viewModel) { + { viewModel.trySendAction(LoginAction.LockAccountClick(it)) } + }, + onLogoutAccountClick = remember(viewModel) { + { viewModel.trySendAction(LoginAction.LogoutAccountClick(it)) } + }, + onAddAccountClick = remember(viewModel) { + { viewModel.trySendAction(LoginAction.AddAccountClick) } + }, + onDismissRequest = { isAccountMenuVisible = false }, + topAppBarScrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxSize(), + ) + }, + ) { LoginScreenContent( state = state, onPasswordInputChanged = remember(viewModel) { @@ -169,31 +190,7 @@ fun LoginScreen( onNotYouButtonClick = remember(viewModel) { { viewModel.trySendAction(LoginAction.NotYouButtonClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - ) - - BitwardenAccountSwitcher( - isVisible = isAccountMenuVisible, - accountSummaries = state.accountSummaries.toImmutableList(), - onSwitchAccountClick = remember(viewModel) { - { viewModel.trySendAction(LoginAction.SwitchAccountClick(it)) } - }, - onLockAccountClick = remember(viewModel) { - { viewModel.trySendAction(LoginAction.LockAccountClick(it)) } - }, - onLogoutAccountClick = remember(viewModel) { - { viewModel.trySendAction(LoginAction.LogoutAccountClick(it)) } - }, - onAddAccountClick = remember(viewModel) { - { viewModel.trySendAction(LoginAction.AddAccountClick) } - }, - onDismissRequest = { isAccountMenuVisible = false }, - topAppBarScrollBehavior = scrollBehavior, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } @@ -275,7 +272,7 @@ private fun LoginScreenContent( if (state.shouldShowLoginWithDevice) { BitwardenOutlinedButton( label = stringResource(id = R.string.log_in_with_device), - icon = rememberVectorPainter(id = R.drawable.ic_mobile), + icon = rememberVectorPainter(id = R.drawable.ic_mobile_small), onClick = onLoginWithDeviceClick, modifier = Modifier .testTag("LogInWithAnotherDeviceButton") @@ -288,7 +285,7 @@ private fun LoginScreenContent( BitwardenOutlinedButton( label = stringResource(id = R.string.log_in_sso), - icon = rememberVectorPainter(id = R.drawable.ic_briefcase), + icon = rememberVectorPainter(id = R.drawable.ic_enterprise_small), onClick = onSingleSignOnClick, modifier = Modifier .testTag("LogInWithSsoButton") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt index 02f164179..87543ee10 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt @@ -102,10 +102,7 @@ fun LoginWithDeviceScreen( }, ) }, - ) { paddingValues -> - val modifier = Modifier - .fillMaxSize() - .padding(paddingValues) + ) { when (val viewState = state.viewState) { is LoginWithDeviceState.ViewState.Content -> { LoginWithDeviceScreenContent( @@ -116,12 +113,12 @@ fun LoginWithDeviceScreen( onViewAllLogInOptionsClick = remember(viewModel) { { viewModel.trySendAction(LoginWithDeviceAction.ViewAllLogInOptionsClick) } }, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } LoginWithDeviceState.ViewState.Loading -> BitwardenLoadingContent( - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt index 14bacb994..80e7d28d7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt @@ -116,11 +116,11 @@ class LoginWithDeviceViewModel @Inject constructor( ), dialogState = null, loginData = LoginWithDeviceState.LoginData( - accessCode = result.authRequestResponse.accessCode, + accessCode = result.accessCode, requestId = result.authRequest.id, masterPasswordHash = result.authRequest.masterPasswordHash, asymmetricalKey = requireNotNull(result.authRequest.key), - privateKey = result.authRequestResponse.privateKey, + privateKey = result.privateKey, captchaToken = null, ), ) @@ -157,31 +157,8 @@ class LoginWithDeviceViewModel @Inject constructor( } } - CreateAuthRequestResult.Declined -> { - when (state.loginWithDeviceType) { - LoginWithDeviceType.OTHER_DEVICE, - LoginWithDeviceType.SSO_OTHER_DEVICE, - -> { - mutableStateFlow.update { - it.copy( - viewState = LoginWithDeviceState.ViewState.Content( - loginWithDeviceType = it.loginWithDeviceType, - fingerprintPhrase = "", - isResendNotificationLoading = false, - ), - dialogState = LoginWithDeviceState.DialogState.Error( - title = null, - message = R.string.this_request_is_no_longer_valid.asText(), - ), - ) - } - } - - LoginWithDeviceType.SSO_ADMIN_APPROVAL -> { - // Do nothing, the user should not be informed of this state - } - } - } + // Do nothing, the user should not be informed of this state + CreateAuthRequestResult.Declined -> Unit CreateAuthRequestResult.Expired -> { mutableStateFlow.update { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt index 65e181247..45e2d8f7d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorScreen.kt @@ -98,12 +98,11 @@ fun MasterPasswordGeneratorScreen( snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) }, - ) { innerPadding -> + ) { Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(innerPadding), + .verticalScroll(rememberScrollState()), ) { MasterPasswordGeneratorContent( generatedPassword = state.generatedPassword, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt index 45e718619..6b2befc44 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt @@ -80,12 +80,11 @@ fun MasterPasswordGuidanceScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(innerPadding) .standardHorizontalMargin(), ) { Column( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt index e53adee1a..1c72b7a01 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintScreen.kt @@ -111,11 +111,9 @@ fun MasterPasswordHintScreen( }, ) }, - ) { innerPadding -> + ) { Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { BitwardenTextField( modifier = Modifier diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutScreen.kt index 5be5caadf..af4a3522f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutScreen.kt @@ -74,10 +74,9 @@ fun PreventAccountLockoutScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .fillMaxWidth() .standardHorizontalMargin() .verticalScroll(rememberScrollState()), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt index c9cee320f..62d656b79 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -65,7 +64,7 @@ fun RemovePasswordScreen( navigationIcon = null, ) }, - ) { innerPadding -> + ) { RemovePasswordScreenContent( state = state, onContinueClick = remember(viewModel) { @@ -74,9 +73,7 @@ fun RemovePasswordScreen( onInputChanged = remember(viewModel) { { viewModel.trySendAction(RemovePasswordAction.InputChanged(it)) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt index b500ea54b..c1caa5618 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordScreen.kt @@ -121,7 +121,7 @@ fun ResetPasswordScreen( }, ) }, - ) { innerPadding -> + ) { ResetPasswordScreenContent( state = state, onCurrentPasswordInputChanged = remember(viewModel) { @@ -136,9 +136,7 @@ fun ResetPasswordScreen( onPasswordHintInputChanged = remember(viewModel) { { viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it)) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt index bb48e1120..eecf0b875 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordScreen.kt @@ -84,7 +84,7 @@ fun SetPasswordScreen( }, ) }, - ) { innerPadding -> + ) { SetPasswordScreenContent( state = state, onPasswordInputChanged = remember(viewModel) { @@ -97,7 +97,6 @@ fun SetPasswordScreen( { viewModel.trySendAction(SetPasswordAction.PasswordHintInputChanged(it)) } }, modifier = Modifier - .padding(innerPadding) .imePadding() .fillMaxSize(), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt index 070cb86dd..57c3d271b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt @@ -2,8 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.startregistration import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,15 +14,12 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -38,8 +33,6 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -70,7 +63,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold -import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors +import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager @@ -177,10 +170,9 @@ fun StartRegistrationScreen( onNavigationIconClick = handler.onBackClick, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .imePadding() .fillMaxSize() .verticalScroll(rememberScrollState()), @@ -278,7 +270,9 @@ private fun StartRegistrationContent( isChecked = isReceiveMarketingEmailsToggled, onCheckedChange = handler.onReceiveMarketingEmailsToggle, onUnsubscribeClick = handler.onUnsubscribeMarketingEmailsClick, - modifier = Modifier.standardHorizontalMargin(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(24.dp)) } @@ -362,7 +356,6 @@ private fun TermsAndPrivacyText( } } -@Suppress("LongMethod") @Composable private fun ReceiveMarketingEmailsSwitch( isChecked: Boolean, @@ -374,7 +367,9 @@ private fun ReceiveMarketingEmailsSwitch( @Suppress("MaxLineLength") val annotatedLinkString = createClickableAnnotatedString( - mainString = stringResource(id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time), + mainString = stringResource( + id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time, + ), highlights = listOf( ClickableTextHighlight( textToHighlight = unsubscribeString, @@ -382,13 +377,9 @@ private fun ReceiveMarketingEmailsSwitch( ), ), ) - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, + BitwardenSwitch( modifier = modifier .semantics(mergeDescendants = true) { - testTag = "ReceiveMarketingEmailsToggle" - toggleableState = ToggleableState(isChecked) customActions = listOf( CustomAccessibilityAction( label = unsubscribeString, @@ -398,30 +389,12 @@ private fun ReceiveMarketingEmailsSwitch( }, ), ) - } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onCheckedChange.invoke(!isChecked) }, - ) - .fillMaxWidth(), - ) { - Switch( - modifier = Modifier - .height(32.dp) - .width(52.dp), - checked = isChecked, - onCheckedChange = null, - colors = bitwardenSwitchColors(), - ) - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = annotatedLinkString, - style = BitwardenTheme.typography.bodyMedium, - ) - } + }, + label = annotatedLinkString, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = "ReceiveMarketingEmailsToggle", + ) } @PreviewScreenSizes diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt index dd8b6298b..45f71fb14 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt @@ -114,10 +114,9 @@ private fun TrustedDeviceScaffold( ), ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt index f8b086a64..457b38094 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt @@ -160,7 +160,7 @@ fun TwoFactorLoginScreen( }, ) }, - ) { innerPadding -> + ) { TwoFactorLoginScreenContent( state = state, onCodeInputChange = remember(viewModel) { @@ -175,9 +175,7 @@ fun TwoFactorLoginScreen( onResendEmailButtonClick = remember(viewModel) { { viewModel.trySendAction(TwoFactorLoginAction.ResendEmailClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 555a0ce5f..d2fea85ba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.vaultunlock import android.widget.Toast import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -205,99 +204,7 @@ fun VaultUnlockScreen( }, ) }, - ) { innerPadding -> - Box { - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .verticalScroll(rememberScrollState()), - ) { - if (!state.hideInput) { - BitwardenPasswordField( - label = state.vaultUnlockType.unlockScreenInputLabel(), - value = state.input, - onValueChange = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) } - }, - keyboardType = state.vaultUnlockType.unlockScreenKeyboardType, - showPasswordTestTag = state - .vaultUnlockType - .inputFieldVisibilityToggleTestTag, - modifier = Modifier - .testTag(state.vaultUnlockType.unlockScreenInputTestTag) - .padding(horizontal = 16.dp) - .fillMaxWidth(), - autoFocus = state.showKeyboard, - imeAction = ImeAction.Done, - keyboardActions = KeyboardActions( - onDone = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } - }, - ), - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = state.vaultUnlockType.unlockScreenMessage(), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(8.dp)) - } - Text( - text = stringResource( - id = R.string.logged_in_as_on, - state.email, - state.environmentUrl, - ), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier - .testTag("UserAndEnvironmentDataLabel") - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(24.dp)) - if (state.showBiometricLogin && biometricsManager.isBiometricsSupported) { - BitwardenOutlinedButton( - label = stringResource(id = R.string.use_biometrics_to_unlock), - onClick = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) } - }, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(12.dp)) - } else if (state.showBiometricInvalidatedMessage) { - Text( - text = stringResource(R.string.account_biometric_invalidated), - textAlign = TextAlign.Start, - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.status.error, - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(12.dp)) - } - if (!state.hideInput) { - BitwardenFilledButton( - label = stringResource(id = R.string.unlock), - onClick = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } - }, - isEnabled = state.input.isNotEmpty(), - modifier = Modifier - .testTag("UnlockVaultButton") - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - } - Spacer(modifier = Modifier.navigationBarsPadding()) - } - + overlay = { BitwardenAccountSwitcher( isVisible = accountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), @@ -315,10 +222,98 @@ fun VaultUnlockScreen( }, onDismissRequest = { accountMenuVisible = false }, topAppBarScrollBehavior = scrollBehavior, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), + modifier = Modifier.fillMaxSize(), ) + }, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + if (!state.hideInput) { + BitwardenPasswordField( + label = state.vaultUnlockType.unlockScreenInputLabel(), + value = state.input, + onValueChange = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) } + }, + keyboardType = state.vaultUnlockType.unlockScreenKeyboardType, + showPasswordTestTag = state + .vaultUnlockType + .inputFieldVisibilityToggleTestTag, + modifier = Modifier + .testTag(state.vaultUnlockType.unlockScreenInputTestTag) + .padding(horizontal = 16.dp) + .fillMaxWidth(), + autoFocus = state.showKeyboard, + imeAction = ImeAction.Done, + keyboardActions = KeyboardActions( + onDone = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } + }, + ), + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = state.vaultUnlockType.unlockScreenMessage(), + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.text.primary, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + } + Text( + text = stringResource( + id = R.string.logged_in_as_on, + state.email, + state.environmentUrl, + ), + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.text.primary, + modifier = Modifier + .testTag("UserAndEnvironmentDataLabel") + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(24.dp)) + if (state.showBiometricLogin && biometricsManager.isBiometricsSupported) { + BitwardenOutlinedButton( + label = stringResource(id = R.string.use_biometrics_to_unlock), + onClick = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) } + }, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(12.dp)) + } else if (state.showBiometricInvalidatedMessage) { + Text( + text = stringResource(R.string.account_biometric_invalidated), + textAlign = TextAlign.Start, + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.status.error, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + } + if (!state.hideInput) { + BitwardenFilledButton( + label = stringResource(id = R.string.unlock), + onClick = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } + }, + isEnabled = state.input.isNotEmpty(), + modifier = Modifier + .testTag("UnlockVaultButton") + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreen.kt index 003fb7cf7..e0b8d8ee1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreen.kt @@ -32,6 +32,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -41,7 +44,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -81,7 +84,7 @@ fun WelcomeScreen( modifier = Modifier.fillMaxSize(), containerColor = BitwardenTheme.colorScheme.background.secondary, contentColor = BitwardenTheme.colorScheme.text.secondary, - ) { innerPadding -> + ) { WelcomeScreenContent( state = state, pagerState = pagerState, @@ -97,9 +100,7 @@ fun WelcomeScreen( onLoginClick = remember(viewModel) { { viewModel.trySendAction(WelcomeAction.LoginClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } @@ -125,15 +126,20 @@ private fun WelcomeScreenContent( Spacer(modifier = Modifier.weight(1f)) HorizontalPager(state = pagerState) { index -> + val pageNumberContentDescription = + stringResource(R.string.page_number_x_of_y, index + 1, state.pages.size) + val pagerSemanticsModifier = Modifier.semantics(mergeDescendants = true) { + contentDescription = pageNumberContentDescription + } if (LocalConfiguration.current.isPortrait) { WelcomeCardPortrait( state = state.pages[index], - modifier = Modifier.standardHorizontalMargin(), + modifier = pagerSemanticsModifier.standardHorizontalMargin(), ) } else { WelcomeCardLandscape( state = state.pages[index], - modifier = Modifier + modifier = pagerSemanticsModifier .standardHorizontalMargin(landscape = LANDSCAPE_HORIZONTAL_MARGIN), ) } @@ -158,8 +164,8 @@ private fun WelcomeScreenContent( .fillMaxWidth(), ) - BitwardenTextButton( - label = stringResource(id = R.string.log_in), + BitwardenOutlinedButton( + label = stringResource(id = R.string.log_in_verb), onClick = onLoginClick, modifier = Modifier .standardHorizontalMargin(landscape = LANDSCAPE_HORIZONTAL_MARGIN) @@ -178,12 +184,12 @@ private fun WelcomeCardLandscape( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) {}, ) { Image( painter = rememberVectorPainter(id = state.imageRes), contentDescription = null, - modifier = Modifier.size(132.dp), + modifier = Modifier.size(124.dp), ) Column( @@ -215,12 +221,13 @@ private fun WelcomeCardPortrait( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier, + modifier = modifier + .semantics(mergeDescendants = true) {}, ) { Image( painter = rememberVectorPainter(id = state.imageRes), contentDescription = null, - modifier = Modifier.size(200.dp), + modifier = Modifier.size(124.dp), ) Text( @@ -266,6 +273,9 @@ private fun IndicatorDots( Box( modifier = Modifier + .clearAndSetSemantics { + // clear semantics so indicator dots are skipped by screen reader + } .size(8.dp) .clip(CircleShape) .background(color.value) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeViewModel.kt index 53a16e3ee..8cde72b14 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeViewModel.kt @@ -82,7 +82,7 @@ data class WelcomeState( @Parcelize data object CardOne : WelcomeCard() { override val imageRes: Int get() = R.drawable.img_vault_items - override val titleRes: Int get() = R.string.privacy_prioritized + override val titleRes: Int get() = R.string.security_prioritized override val messageRes: Int get() = R.string.welcome_message_1 } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/account/BitwardenAccountSwitcher.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/account/BitwardenAccountSwitcher.kt index ce872bd13..8242552cc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/account/BitwardenAccountSwitcher.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/account/BitwardenAccountSwitcher.kt @@ -14,11 +14,18 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api @@ -107,9 +114,12 @@ fun BitwardenAccountSwitcher( onLogoutAccountClick: (AccountSummary) -> Unit, onAddAccountClick: () -> Unit, onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - isAddAccountAvailable: Boolean = true, topAppBarScrollBehavior: TopAppBarScrollBehavior, + modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout + .union(WindowInsets.navigationBars) + .only(WindowInsetsSides.Horizontal), + isAddAccountAvailable: Boolean = true, ) { // Track the actual visibility (according to the internal transitions) so that we know when we // can safely show dialogs. @@ -190,6 +200,7 @@ fun BitwardenAccountSwitcher( isAddAccountAvailable = isAddAccountAvailable, topAppBarScrollBehavior = topAppBarScrollBehavior, currentAnimationState = { isVisibleActual = it }, + windowInsets = windowInsets, modifier = Modifier .fillMaxWidth(), ) @@ -208,9 +219,12 @@ private fun AnimatedAccountSwitcher( onSwitchAccountLongClick: (AccountSummary) -> Unit, onAddAccountClick: () -> Unit, isAddAccountAvailable: Boolean, - modifier: Modifier = Modifier, topAppBarScrollBehavior: TopAppBarScrollBehavior, currentAnimationState: (isVisible: Boolean) -> Unit, + modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout + .union(WindowInsets.navigationBars) + .only(WindowInsetsSides.Horizontal), ) { val transition = updateTransition( targetState = isVisible, @@ -229,7 +243,8 @@ private fun AnimatedAccountSwitcher( // bottom padding. .padding(bottom = 24.dp) // Match the color of the switcher the different states of the app bar. - .scrolledContainerBackground(topAppBarScrollBehavior), + .scrolledContainerBackground(topAppBarScrollBehavior) + .windowInsetsPadding(windowInsets), ) { items(accountSummaries) { accountSummary -> AccountSummaryItem( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt index dd6f4039f..c6190301a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenMediumTopAppBar.kt @@ -1,6 +1,11 @@ package com.x8bit.bitwarden.ui.platform.components.appbar import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.union import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Text @@ -32,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param title The text to be displayed as the title of the app bar. * @param scrollBehavior Defines the scrolling behavior of the app bar. It controls how the app bar * behaves in conjunction with scrolling content. + * @param windowInsets The insets to be applied to this composable. * @param dividerStyle Determines how the bottom divider should be displayed. * @param actions A lambda containing the set of actions (usually icons or similar) to display * in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in @@ -43,10 +49,13 @@ fun BitwardenMediumTopAppBar( title: String, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets + .union(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), dividerStyle: TopAppBarDividerStyle = TopAppBarDividerStyle.ON_SCROLL, actions: @Composable RowScope.() -> Unit = {}, ) { TopAppBar( + windowInsets = windowInsets, colors = bitwardenTopAppBarColors(), scrollBehavior = scrollBehavior, expandedHeight = 56.dp, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt index 34ea1eab0..2300591e3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenSearchTopAppBar.kt @@ -1,11 +1,17 @@ package com.x8bit.bitwarden.ui.platform.components.appbar +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.union import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -42,6 +48,8 @@ fun BitwardenSearchTopAppBar( scrollBehavior: TopAppBarScrollBehavior, navigationIcon: NavigationIcon?, modifier: Modifier = Modifier, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets + .union(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), autoFocus: Boolean = true, ) { val focusRequester = remember { FocusRequester() } @@ -49,6 +57,7 @@ fun BitwardenSearchTopAppBar( modifier = modifier .testTag(tag = "HeaderBarComponent") .bottomDivider(), + windowInsets = windowInsets, colors = bitwardenTopAppBarColors(), scrollBehavior = scrollBehavior, navigationIcon = { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenTopAppBar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenTopAppBar.kt index 4291eee46..5fba3d52b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenTopAppBar.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/appbar/BitwardenTopAppBar.kt @@ -1,6 +1,11 @@ package com.x8bit.bitwarden.ui.platform.components.appbar import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.union import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Text @@ -50,6 +55,8 @@ fun BitwardenTopAppBar( navigationIconContentDescription: String, onNavigationIconClick: () -> Unit, modifier: Modifier = Modifier, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets + .union(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), dividerStyle: TopAppBarDividerStyle = TopAppBarDividerStyle.ON_SCROLL, actions: @Composable RowScope.() -> Unit = { }, ) { @@ -62,6 +69,7 @@ fun BitwardenTopAppBar( onNavigationIconClick = onNavigationIconClick, ), modifier = modifier, + windowInsets = windowInsets, dividerStyle = dividerStyle, actions = actions, ) @@ -87,6 +95,8 @@ fun BitwardenTopAppBar( scrollBehavior: TopAppBarScrollBehavior, navigationIcon: NavigationIcon?, modifier: Modifier = Modifier, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets + .union(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)), dividerStyle: TopAppBarDividerStyle = TopAppBarDividerStyle.ON_SCROLL, actions: @Composable RowScope.() -> Unit = {}, minimunHeight: Dp = 48.dp, @@ -129,6 +139,7 @@ fun BitwardenTopAppBar( if (titleTextHasOverflow) { MediumTopAppBar( + windowInsets = windowInsets, colors = bitwardenTopAppBarColors(), scrollBehavior = scrollBehavior, navigationIcon = navigationIconContent, @@ -149,6 +160,7 @@ fun BitwardenTopAppBar( ) } else { TopAppBar( + windowInsets = windowInsets, colors = bitwardenTopAppBarColors(), scrollBehavior = scrollBehavior, navigationIcon = navigationIconContent, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt index 85956def3..6a6bc2b18 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/bottomsheet/BitwardenModalBottomSheet.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.platform.components.bottomsheet -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api @@ -45,10 +44,7 @@ fun BitwardenModalBottomSheet( modifier: Modifier = Modifier, showBottomSheet: Boolean = true, sheetState: SheetState = rememberModalBottomSheetState(), - sheetContent: @Composable ( - paddingValues: PaddingValues, - animatedOnDismiss: () -> Unit, - ) -> Unit, + sheetContent: @Composable (animatedOnDismiss: () -> Unit) -> Unit, ) { if (!showBottomSheet) return ModalBottomSheet( @@ -79,8 +75,8 @@ fun BitwardenModalBottomSheet( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) .fillMaxSize(), - ) { paddingValues -> - sheetContent(paddingValues, animatedOnDismiss) + ) { + sheetContent(animatedOnDismiss) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt new file mode 100644 index 000000000..1f8564e9d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/BitwardenFilledIconButton.kt @@ -0,0 +1,55 @@ +package com.x8bit.bitwarden.ui.platform.components.button + +import androidx.annotation.DrawableRes +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.button.color.bitwardenFilledIconButtonColors +import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * A filled icon button that displays an icon. + * + * @param vectorIconRes Icon to display on the button. + * @param contentDescription The content description for this icon button. + * @param onClick Callback for when the icon button is clicked. + * @param modifier A [Modifier] for the composable. + * @param isEnabled Whether or not the button should be enabled. + */ +@Composable +fun BitwardenFilledIconButton( + @DrawableRes vectorIconRes: Int, + contentDescription: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + FilledIconButton( + modifier = modifier.semantics(mergeDescendants = true) {}, + onClick = onClick, + colors = bitwardenFilledIconButtonColors(), + enabled = isEnabled, + ) { + Icon( + painter = rememberVectorPainter(id = vectorIconRes), + contentDescription = contentDescription, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BitwardenFilledIconButton_preview() { + BitwardenTheme { + BitwardenFilledIconButton( + vectorIconRes = R.drawable.ic_question_circle, + contentDescription = "Sample Icon", + onClick = {}, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/color/BitwardenIconButtonColors.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/color/BitwardenIconButtonColors.kt index b27d01800..b0178c6f2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/color/BitwardenIconButtonColors.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/button/color/BitwardenIconButtonColors.kt @@ -5,6 +5,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +/** + * Provides a default set of Bitwarden-styled colors for a filled icon button. + */ +@Composable +fun bitwardenFilledIconButtonColors(): IconButtonColors = IconButtonColors( + containerColor = BitwardenTheme.colorScheme.filledButton.background, + contentColor = BitwardenTheme.colorScheme.filledButton.foreground, + disabledContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled, + disabledContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled, +) + /** * Provides a default set of Bitwarden-styled colors for a standard icon button. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenContentCard.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenContentCard.kt index 7b94d5cd8..423b2467c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenContentCard.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenContentCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider @@ -35,10 +36,8 @@ fun BitwardenContentCard( Column( modifier = modifier .fillMaxWidth() - .background( - color = BitwardenTheme.colorScheme.background.secondary, - shape = BitwardenTheme.shapes.content, - ), + .clip(shape = BitwardenTheme.shapes.content) + .background(color = BitwardenTheme.colorScheme.background.secondary), ) { contentItems.forEachIndexed { index, item -> Box( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/content/BitwardenContentBlock.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/content/BitwardenContentBlock.kt index c5a3d937d..c4f75a733 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/content/BitwardenContentBlock.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/content/BitwardenContentBlock.kt @@ -95,7 +95,7 @@ private fun BitwardenContentBlock( } Spacer(Modifier.height(12.dp)) } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/row/BitwardenBasicDialogRow.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/row/BitwardenBasicDialogRow.kt index 9034c6455..7ead6f5b3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/row/BitwardenBasicDialogRow.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/row/BitwardenBasicDialogRow.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -42,6 +43,7 @@ fun BitwardenBasicDialogRow( vertical = 16.dp, horizontal = 24.dp, ) - .fillMaxWidth(), + .fillMaxWidth() + .testTag("AlertSelectionOption"), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/fab/BitwardenFloatingActionButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/fab/BitwardenFloatingActionButton.kt index a4afa6c90..d83f319e1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/fab/BitwardenFloatingActionButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/fab/BitwardenFloatingActionButton.kt @@ -1,5 +1,10 @@ package com.x8bit.bitwarden.ui.platform.components.fab +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -14,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param painter The icon for the button. * @param contentDescription The content description for the button. * @param modifier The [Modifier] to be applied to the button. + * @param windowInsets The insets to be applied to this composable. */ @Composable fun BitwardenFloatingActionButton( @@ -21,13 +27,14 @@ fun BitwardenFloatingActionButton( painter: Painter, contentDescription: String, modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout.union(WindowInsets.navigationBars), ) { FloatingActionButton( containerColor = BitwardenTheme.colorScheme.filledButton.background, contentColor = BitwardenTheme.colorScheme.filledButton.foreground, onClick = onClick, shape = BitwardenTheme.shapes.fab, - modifier = modifier, + modifier = modifier.windowInsetsPadding(insets = windowInsets), ) { Icon( painter = painter, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/scaffold/BitwardenScaffold.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/scaffold/BitwardenScaffold.kt index 3804d6653..0f72eb766 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/scaffold/BitwardenScaffold.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/scaffold/BitwardenScaffold.kt @@ -1,12 +1,14 @@ package com.x8bit.bitwarden.ui.platform.components.scaffold import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold @@ -22,17 +24,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** * Direct passthrough to [Scaffold] but contains a few specific override values. Everything is * still overridable if necessary. + * + * The [utilityBar] is a nonstandard [Composable] that is placed below the [topBar] and does not + * scroll. + * The [overlay] is a nonstandard [Composable] that is placed over top the `utilityBar` and + * `content`. */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun BitwardenScaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = { }, + utilityBar: @Composable () -> Unit = { }, + overlay: @Composable () -> Unit = { }, bottomBar: @Composable () -> Unit = { }, snackbarHost: @Composable () -> Unit = { }, floatingActionButton: @Composable () -> Unit = { }, @@ -42,8 +52,9 @@ fun BitwardenScaffold( contentColor: Color = BitwardenTheme.colorScheme.text.primary, contentWindowInsets: WindowInsets = ScaffoldDefaults .contentWindowInsets - .exclude(WindowInsets.navigationBars), - content: @Composable (PaddingValues) -> Unit, + .union(WindowInsets.displayCutout) + .only(WindowInsetsSides.Horizontal), + content: @Composable () -> Unit, ) { Scaffold( modifier = Modifier @@ -52,36 +63,38 @@ fun BitwardenScaffold( topBar = topBar, bottomBar = bottomBar, snackbarHost = snackbarHost, - floatingActionButton = { - Box(modifier = Modifier.navigationBarsPadding()) { - floatingActionButton() - } - }, + floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, containerColor = containerColor, contentColor = contentColor, - contentWindowInsets = contentWindowInsets, + contentWindowInsets = WindowInsets(0.dp), content = { paddingValues -> - val internalPullToRefreshState = rememberPullToRefreshState() - Box( - modifier = Modifier.pullToRefresh( - state = internalPullToRefreshState, - isRefreshing = pullToRefreshState.isRefreshing, - onRefresh = pullToRefreshState.onRefresh, - enabled = pullToRefreshState.isEnabled, - ), - ) { - content(paddingValues) - - PullToRefreshDefaults.Indicator( + Column(modifier = Modifier.padding(paddingValues = paddingValues)) { + utilityBar() + val internalPullToRefreshState = rememberPullToRefreshState() + Box( modifier = Modifier - .padding(paddingValues) - .align(Alignment.TopCenter), - isRefreshing = pullToRefreshState.isRefreshing, - state = internalPullToRefreshState, - containerColor = BitwardenTheme.colorScheme.background.secondary, - color = BitwardenTheme.colorScheme.icon.secondary, - ) + .windowInsetsPadding(insets = contentWindowInsets) + .pullToRefresh( + state = internalPullToRefreshState, + isRefreshing = pullToRefreshState.isRefreshing, + onRefresh = pullToRefreshState.onRefresh, + enabled = pullToRefreshState.isEnabled, + ), + ) { + content() + + PullToRefreshDefaults.Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = pullToRefreshState.isRefreshing, + state = internalPullToRefreshState, + containerColor = BitwardenTheme.colorScheme.background.secondary, + color = BitwardenTheme.colorScheme.icon.secondary, + ) + } + } + Box(modifier = Modifier.padding(paddingValues = paddingValues)) { + overlay() } }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/segment/BitwardenSegmentedButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/segment/BitwardenSegmentedButton.kt index 3dad060ba..59874ac36 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/segment/BitwardenSegmentedButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/segment/BitwardenSegmentedButton.kt @@ -3,8 +3,15 @@ package com.x8bit.bitwarden.ui.platform.components.segment import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text @@ -23,17 +30,22 @@ import kotlinx.collections.immutable.ImmutableList * * @param options List of options to display. * @param modifier Modifier. + * @param windowInsets The insets to be applied to this composable. */ @Composable fun BitwardenSegmentedButton( options: ImmutableList, modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout + .union(WindowInsets.navigationBars) + .only(WindowInsetsSides.Horizontal), ) { if (options.isEmpty()) return Box( modifier = modifier .background(color = BitwardenTheme.colorScheme.background.secondary) - .padding(top = 4.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + .padding(top = 4.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) + .windowInsetsPadding(insets = windowInsets), ) { SingleChoiceSegmentedButtonRow( modifier = Modifier diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbar.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbar.kt index 827e8d2f9..a25592d6e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbar.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbar.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.platform.components.snackbar import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -28,6 +29,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * Custom snackbar for Bitwarden. * Shows a message with an optional actions and title. */ +@Suppress("LongMethod") @Composable fun BitwardenSnackbar( bitwardenSnackbarData: BitwardenSnackbarData, @@ -45,6 +47,12 @@ fun BitwardenSnackbar( color = BitwardenTheme.colorScheme.background.alert, shape = BitwardenTheme.shapes.snackbar, ) + // I there is no explicit dismiss action, the Snackbar can be dismissed by clicking + // anywhere on the Snackbar. + .clickable( + enabled = !bitwardenSnackbarData.withDismissAction, + onClick = onDismiss, + ) .padding(16.dp), ) { Column { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbarHost.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbarHost.kt index ada958dd6..5b5e5fc75 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbarHost.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/snackbar/BitwardenSnackbarHost.kt @@ -1,5 +1,10 @@ package com.x8bit.bitwarden.ui.platform.components.snackbar +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -9,15 +14,17 @@ import androidx.compose.ui.Modifier * * @param bitwardenHostState The state of this snackbar. * @param modifier The [Modifier] to be applied to the [SnackbarHost]. + * @param windowInsets The insets to be applied to this composable. */ @Composable fun BitwardenSnackbarHost( bitwardenHostState: BitwardenSnackbarHostState, modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout.union(WindowInsets.navigationBars), ) { SnackbarHost( hostState = bitwardenHostState.snackbarHostState, - modifier = modifier, + modifier = modifier.windowInsetsPadding(insets = windowInsets), ) { snackbarData -> val message = snackbarData.visuals.message val currentCustomSnackbarData = bitwardenHostState.currentSnackbarData diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt index 58681d6f7..289cf8a09 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/stepper/BitwardenStepper.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.text.input.KeyboardType import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.ZERO_WIDTH_CHARACTER import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions /** @@ -51,7 +51,7 @@ fun BitwardenStepper( value = clampedValue?.toString() ?: ZERO_WIDTH_CHARACTER, actionsTestTag = stepperActionsTestTag, actions = { - BitwardenTonalIconButton( + BitwardenFilledIconButton( vectorIconRes = R.drawable.ic_minus, contentDescription = "\u2212", onClick = { @@ -63,7 +63,7 @@ fun BitwardenStepper( isEnabled = isDecrementEnabled && !isAtRangeMinimum, modifier = Modifier.testTag("DecrementValue"), ) - BitwardenTonalIconButton( + BitwardenFilledIconButton( vectorIconRes = R.drawable.ic_plus, contentDescription = "+", onClick = { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt index a00cdbe6f..f82ee2c32 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt @@ -22,15 +22,17 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** - * A wide custom switch composable + * A custom switch composable * * @param label The descriptive text label to be displayed adjacent to the switch. * @param isChecked The current state of the switch (either checked or unchecked). @@ -45,7 +47,6 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. */ -@Suppress("LongMethod") @Composable fun BitwardenSwitch( label: String, @@ -57,6 +58,154 @@ fun BitwardenSwitch( readOnly: Boolean = false, enabled: Boolean = true, actions: (@Composable RowScope.() -> Unit)? = null, +) { + BitwardenSwitch( + modifier = modifier, + label = label.toAnnotatedString(), + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = { + description?.let { + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = if (enabled) { + BitwardenTheme.colorScheme.text.secondary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + ) + } + }, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param description An optional description label to be displayed below the [label]. + * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param readOnly Disables the click functionality without modifying the other UI characteristics. + * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + */ +@Composable +fun BitwardenSwitch( + label: AnnotatedString, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + description: String? = null, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, +) { + BitwardenSwitch( + modifier = modifier, + label = label, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = { + description?.let { + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = if (enabled) { + BitwardenTheme.colorScheme.text.secondary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + ) + } + }, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param readOnly Disables the click functionality without modifying the other UI characteristics. + * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + * @param subContent A lambda containing content directly below the label. + */ +@Composable +fun BitwardenSwitch( + label: String, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, + subContent: @Composable () -> Unit, +) { + BitwardenSwitch( + modifier = modifier, + label = label.toAnnotatedString(), + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = subContent, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param readOnly Disables the click functionality without modifying the other UI characteristics. + * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + * @param subContent A lambda containing content directly below the label. + */ +@Suppress("LongMethod") +@Composable +fun BitwardenSwitch( + label: AnnotatedString, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, + subContent: @Composable () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -95,17 +244,7 @@ fun BitwardenSwitch( }, modifier = Modifier.testTag(tag = "SwitchText"), ) - description?.let { - Text( - text = it, - style = BitwardenTheme.typography.bodyMedium, - color = if (enabled) { - BitwardenTheme.colorScheme.text.secondary - } else { - BitwardenTheme.colorScheme.filledButton.foregroundDisabled - }, - ) - } + subContent() } actions @@ -131,6 +270,7 @@ private fun BitwardenSwitch_preview() { Column { BitwardenSwitch( label = "Label", + description = "description", isChecked = true, onCheckedChange = {}, ) @@ -141,6 +281,7 @@ private fun BitwardenSwitch_preview() { ) BitwardenSwitch( label = "Label", + description = "description", isChecked = true, onCheckedChange = {}, actions = { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index be613928e..e20a156c1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -74,11 +74,9 @@ fun DebugMenuScreen( ), ) }, - ) { innerPadding -> + ) { Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(innerPadding), + modifier = Modifier.verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(16.dp)) FeatureFlagContent( @@ -113,6 +111,7 @@ fun DebugMenuScreen( } }, ) + Spacer(modifier = Modifier.navigationBarsPadding()) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt index 42bffa0d7..37194d61a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreen.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.search import android.widget.Toast -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -91,14 +90,7 @@ fun SearchScreen( ), ) }, - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - ) { + utilityBar = { val vaultFilterData = state.vaultFilterData if (state.viewState.hasVaultFilter && vaultFilterData != null) { VaultFilter( @@ -116,32 +108,39 @@ fun SearchScreen( .fillMaxWidth(), ) } + }, + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection), + ) { + when (val viewState = state.viewState) { + is SearchState.ViewState.Content -> SearchContent( + viewState = viewState, + searchHandlers = searchHandlers, + searchType = state.searchType, + modifier = Modifier + .fillMaxSize() + .imePadding(), + ) - val innerModifier = Modifier - .fillMaxSize() - .imePadding() - when (val viewState = state.viewState) { - is SearchState.ViewState.Content -> SearchContent( - viewState = viewState, - searchHandlers = searchHandlers, - searchType = state.searchType, - modifier = innerModifier, - ) + is SearchState.ViewState.Empty -> SearchEmptyContent( + viewState = viewState, + modifier = Modifier + .fillMaxSize() + .imePadding(), + ) - is SearchState.ViewState.Empty -> SearchEmptyContent( - viewState = viewState, - modifier = innerModifier, - ) + is SearchState.ViewState.Error -> BitwardenErrorContent( + message = viewState.message(), + modifier = Modifier + .fillMaxSize() + .imePadding(), + ) - is SearchState.ViewState.Error -> BitwardenErrorContent( - message = viewState.message(), - modifier = innerModifier, - ) - - SearchState.ViewState.Loading -> BitwardenLoadingContent( - modifier = innerModifier, - ) - } + SearchState.ViewState.Loading -> BitwardenLoadingContent( + modifier = Modifier + .fillMaxSize() + .imePadding(), + ) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt index e3238ff55..b21c51e38 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt @@ -80,10 +80,9 @@ fun SettingsScreen( ) }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .fillMaxSize() .verticalScroll(state = rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt index 45448d57d..3de12b656 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutScreen.kt @@ -111,12 +111,10 @@ fun AboutScreen( }, ) }, - ) { innerPadding -> + ) { ContentColumn( state = state, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), onHelpCenterClick = remember(viewModel) { { viewModel.trySendAction(AboutAction.HelpCenterClick) } }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 2eb2733c6..56f89d749 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -174,10 +174,9 @@ fun AccountSecurityScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier - .padding(innerPadding) .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt index 3fec7d5eb..d2be0f9d0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt @@ -118,12 +118,11 @@ fun DeleteAccountScreen( }, ) }, - ) { innerPadding -> + ) { Column( modifier = Modifier .imePadding() .fillMaxSize() - .padding(innerPadding) .verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt index 3f6c8f82f..d6d28598e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt @@ -218,13 +218,12 @@ private fun DeleteAccountConfirmationScaffold( onNavigationIconClick = onCloseClick, ) }, - ) { innerPadding -> + ) { DeleteAccountConfirmationContent( state = state, onDeleteAccountClick = onDeleteAccountClick, onResendCodeClick = onResendCodeClick, onVerificationCodeTextChange = onVerificationCodeTextChange, - modifier = Modifier.padding(innerPadding), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt index 4eecfc59a..5e4d1a6ba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalScreen.kt @@ -104,7 +104,7 @@ fun LoginApprovalScreen( }, ) }, - ) { innerPadding -> + ) { when (val viewState = state.viewState) { is LoginApprovalState.ViewState.Content -> { LoginApprovalContent( @@ -115,26 +115,20 @@ fun LoginApprovalScreen( onDeclineLoginClick = remember(viewModel) { { viewModel.trySendAction(LoginApprovalAction.DeclineRequestClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is LoginApprovalState.ViewState.Error -> { BitwardenErrorContent( message = stringResource(id = R.string.generic_error_message), - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is LoginApprovalState.ViewState.Loading -> { BitwardenLoadingContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt index 9bea08324..28b7d7e10 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsScreen.kt @@ -125,9 +125,8 @@ fun PendingRequestsScreen( }, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), modifier = Modifier.statusBarsPadding(), - ) { paddingValues, animatedOnDismiss -> + ) { animatedOnDismiss -> PendingRequestsBottomSheetContent( - modifier = Modifier.padding(paddingValues), permissionsManager = permissionsManager, onDismiss = animatedOnDismiss, ) @@ -150,13 +149,11 @@ fun PendingRequestsScreen( ) }, pullToRefreshState = pullToRefreshState, - ) { innerPadding -> + ) { when (val viewState = state.viewState) { is PendingRequestsState.ViewState.Content -> { PendingRequestsContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), state = viewState, onDeclineAllRequestsConfirm = remember(viewModel) { { @@ -176,22 +173,16 @@ fun PendingRequestsScreen( } is PendingRequestsState.ViewState.Empty -> PendingRequestsEmpty( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) PendingRequestsState.ViewState.Error -> BitwardenErrorContent( message = stringResource(R.string.generic_error_message), - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) PendingRequestsState.ViewState.Loading -> BitwardenLoadingContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt index 224786fc1..6ca4ed614 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt @@ -73,10 +73,9 @@ fun AppearanceScreen( }, ) }, - ) { innerPadding -> + ) { Column( - Modifier - .padding(innerPadding) + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 1f657333c..be7c5938c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -126,10 +126,9 @@ fun AutoFillScreen( }, ) }, - ) { innerPadding -> + ) { Column( - Modifier - .padding(innerPadding) + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt index 5be5df8fc..2b7e76718 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillScreen.kt @@ -141,11 +141,9 @@ fun BlockAutoFillScreen( ) } }, - ) { innerPadding -> + ) { LazyColumn( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { when (val viewState = state.viewState) { is BlockAutoFillState.ViewState.Content -> { @@ -191,9 +189,7 @@ fun BlockAutoFillScreen( addItemClickAction = remember(viewModel) { { viewModel.trySendAction(BlockAutoFillAction.AddUriClick) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt index e7f2df461..39936431b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt @@ -160,7 +160,7 @@ fun ExportVaultScreen( }, ) }, - ) { innerPadding -> + ) { ExportVaultScreenContent( state = state, onConfirmFilePasswordInputChanged = remember(viewModel) { @@ -179,9 +179,7 @@ fun ExportVaultScreen( { viewModel.trySendAction(ExportVaultAction.SendCodeClick) } }, onExportVaultClick = { shouldShowConfirmationDialog = true }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt index d8ea2423e..867df5b3f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersScreen.kt @@ -98,7 +98,7 @@ fun FoldersScreen( .navigationBarsPadding(), ) }, - ) { innerPadding -> + ) { when (val viewState = state.value.viewState) { is FoldersState.ViewState.Content -> { FoldersContent( @@ -106,26 +106,20 @@ fun FoldersScreen( onItemClick = remember(viewModel) { { viewModel.trySendAction(FoldersAction.FolderClick(it)) } }, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is FoldersState.ViewState.Error -> { BitwardenErrorContent( message = viewState.message(), - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is FoldersState.ViewState.Loading -> { BitwardenLoadingContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt index e8428ec03..fd50478df 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreen.kt @@ -124,13 +124,11 @@ fun FolderAddEditScreen( }, ) }, - ) { innerPadding -> + ) { when (val viewState = state.viewState) { is FolderAddEditState.ViewState.Content -> { Column( - Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { BitwardenTextField( label = stringResource(id = R.string.name), @@ -148,17 +146,13 @@ fun FolderAddEditScreen( is FolderAddEditState.ViewState.Error -> { BitwardenErrorContent( message = viewState.message(), - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is FolderAddEditState.ViewState.Loading -> { BitwardenLoadingContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt index 20df6aad9..7c6fd068b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt @@ -93,10 +93,9 @@ fun OtherScreen( }, ) }, - ) { innerPadding -> + ) { Column( - Modifier - .padding(innerPadding) + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index a8e91c748..7a5b4a524 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -100,10 +100,9 @@ fun VaultSettingsScreen( bitwardenHostState = snackbarHostState, ) }, - ) { innerPadding -> + ) { Column( - Modifier - .padding(innerPadding) + modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index d9264faca..1c96b4a94 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -75,8 +75,12 @@ fun NavGraphBuilder.vaultUnlockedGraph( vaultItemListingDestinationAsRoot( onNavigateBack = { navController.popBackStack() }, onNavigateToVaultItemScreen = { navController.navigateToVaultItem(vaultItemId = it) }, - onNavigateToVaultAddItemScreen = { - navController.navigateToVaultAddEdit(VaultAddEditType.AddItem(it)) + onNavigateToVaultAddItemScreen = { cipherType, selectedFolderId, collectionId -> + navController.navigateToVaultAddEdit( + VaultAddEditType.AddItem(cipherType), + selectedFolderId, + collectionId, + ) }, onNavigateToSearchVault = { navController.navigateToSearch(searchType = it) }, onNavigateToVaultEditItemScreen = { @@ -86,8 +90,12 @@ fun NavGraphBuilder.vaultUnlockedGraph( vaultUnlockedNavBarDestination( onNavigateToExportVault = { navController.navigateToExportVault() }, onNavigateToFolders = { navController.navigateToFolders() }, - onNavigateToVaultAddItem = { - navController.navigateToVaultAddEdit(VaultAddEditType.AddItem(it)) + onNavigateToVaultAddItem = { cipherType, selectedFolderId, collectionId -> + navController.navigateToVaultAddEdit( + VaultAddEditType.AddItem(cipherType), + selectedFolderId, + collectionId, + ) }, onNavigateToVaultItem = { navController.navigateToVaultItem(it) }, onNavigateToVaultEditItem = { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index af86915c2..a6a43bf39 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -25,7 +25,7 @@ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) */ @Suppress("LongParameterList") fun NavGraphBuilder.vaultUnlockedNavBarDestination( - onNavigateToVaultAddItem: (VaultItemCipherType) -> Unit, + onNavigateToVaultAddItem: (VaultItemCipherType, String?, String?) -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 1a3b029cd..d40df2294 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -2,16 +2,14 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.only import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -21,6 +19,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry @@ -35,7 +34,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect -import com.x8bit.bitwarden.ui.platform.base.util.max import com.x8bit.bitwarden.ui.platform.base.util.toDp import com.x8bit.bitwarden.ui.platform.components.navigation.BitwardenNavigationBarItem import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold @@ -64,7 +62,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType fun VaultUnlockedNavBarScreen( viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(), navController: NavHostController = rememberNavController(), - onNavigateToVaultAddItem: (VaultItemCipherType) -> Unit, + onNavigateToVaultAddItem: (VaultItemCipherType, String?, String?) -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, @@ -158,7 +156,7 @@ private fun VaultUnlockedNavBarScaffold( sendTabClickedAction: () -> Unit, generatorTabClickedAction: () -> Unit, settingsTabClickedAction: () -> Unit, - navigateToVaultAddItem: (VaultItemCipherType) -> Unit, + navigateToVaultAddItem: (VaultItemCipherType, String?, String?) -> Unit, onNavigateToVaultItem: (vaultItemId: String) -> Unit, onNavigateToVaultEditItem: (vaultItemId: String) -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, @@ -177,10 +175,9 @@ private fun VaultUnlockedNavBarScaffold( var shouldDimNavBar by remember { mutableStateOf(false) } // This scaffold will host screens that contain top bars while not hosting one itself. - // We need to ignore the status bar insets here and let the content screens handle - // it themselves. + // We need to ignore the all insets here and let the content screens handle it themselves. BitwardenScaffold( - contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars), + contentWindowInsets = WindowInsets(0.dp), bottomBar = { Box { var appBarHeightPx by remember { mutableIntStateOf(0) } @@ -208,17 +205,16 @@ private fun VaultUnlockedNavBarScaffold( ) } }, - ) { innerPadding -> + ) { // Because this Scaffold has a bottom navigation bar, the NavHost will: - // - consume the navigation bar insets. + // - consume the vertical navigation bar insets. // - consume the IME insets. NavHost( navController = navController, startDestination = VAULT_GRAPH_ROUTE, modifier = Modifier - .consumeWindowInsets(WindowInsets.navigationBars) - .consumeWindowInsets(WindowInsets.ime) - .padding(innerPadding.max(WindowInsets.ime)), + .consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) + .consumeWindowInsets(WindowInsets.ime), enterTransition = RootTransitionProviders.Enter.fadeIn, exitTransition = RootTransitionProviders.Exit.fadeOut, popEnterTransition = RootTransitionProviders.Enter.fadeIn, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/exit/ExitManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/exit/ExitManagerImpl.kt index 25e3afeab..f8ea1be8b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/exit/ExitManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/exit/ExitManagerImpl.kt @@ -8,9 +8,9 @@ import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage */ @OmitFromCoverage class ExitManagerImpl( - val activity: Activity, + private val activity: Activity, ) : ExitManager { override fun exitApplication() { - activity.finish() + activity.finishAndRemoveTask() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index 6166f50a9..61e617ed4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -67,6 +67,20 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Pa import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Passphrase.Companion.PASSPHRASE_MIN_NUMBER_OF_WORDS import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceTypeOption +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.CatchAllEmailHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.ForwardedEmailAliasHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.PassphraseHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.PasswordHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.PlusAddressedEmailHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.RandomWordHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.UsernameTypeHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberCatchAllEmailHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberForwardedEmailAliasHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberPassphraseHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberPasswordHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberPlusAddressedEmailHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberRandomWordHandlers +import com.x8bit.bitwarden.ui.tools.feature.generator.handlers.rememberUsernameTypeHandlers import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -135,47 +149,26 @@ fun GeneratorScreen( remember(viewModel) { { viewModel.trySendAction( - GeneratorAction.MainType.Username.UsernameTypeOptionSelect( - it, - ), + GeneratorAction.MainType.Username.UsernameTypeOptionSelect(it), ) } } - val passwordHandlers = remember(viewModel) { - PasswordHandlers.create(viewModel = viewModel) - } - - val passphraseHandlers = remember(viewModel) { - PassphraseHandlers.create(viewModel = viewModel) - } - - val usernameTypeHandlers = remember(viewModel) { - UsernameTypeHandlers.create(viewModel = viewModel) - } - - val forwardedEmailAliasHandlers = remember(viewModel) { - ForwardedEmailAliasHandlers.create(viewModel = viewModel) - } - - val plusAddressedEmailHandlers = remember(viewModel) { - PlusAddressedEmailHandlers.create(viewModel = viewModel) - } - - val catchAllEmailHandlers = remember(viewModel) { - CatchAllEmailHandlers.create(viewModel = viewModel) - } - - val randomWordHandlers = remember(viewModel) { - RandomWordHandlers.create(viewModel = viewModel) - } + val passwordHandlers = rememberPasswordHandlers(viewModel) + val passphraseHandlers = rememberPassphraseHandlers(viewModel) + val usernameTypeHandlers = rememberUsernameTypeHandlers(viewModel) + val forwardedEmailAliasHandlers = rememberForwardedEmailAliasHandlers(viewModel) + val plusAddressedEmailHandlers = rememberPlusAddressedEmailHandlers(viewModel) + val catchAllEmailHandlers = rememberCatchAllEmailHandlers(viewModel) + val randomWordHandlers = rememberRandomWordHandlers(viewModel) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) BitwardenScaffold( topBar = { - when (state.generatorMode) { + when (val generatorMode = state.generatorMode) { is GeneratorMode.Modal -> { ModalAppBar( + generatorMode = generatorMode, scrollBehavior = scrollBehavior, onCloseClick = remember(viewModel) { { viewModel.trySendAction(GeneratorAction.CloseClick) } @@ -196,12 +189,7 @@ fun GeneratorScreen( } } }, - snackbarHost = { - BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) - }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { innerPadding -> - Column(modifier = Modifier.padding(innerPadding)) { + utilityBar = { MainStateOptionsItem( selectedType = state.selectedType, passcodePolicyOverride = state.passcodePolicyOverride, @@ -210,20 +198,25 @@ fun GeneratorScreen( modifier = Modifier .scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior), ) - ScrollContent( - state = state, - onRegenerateClick = onRegenerateClick, - onCopyClick = onCopyClick, - onUsernameSubStateOptionClicked = onUsernameOptionClicked, - passwordHandlers = passwordHandlers, - passphraseHandlers = passphraseHandlers, - usernameTypeHandlers = usernameTypeHandlers, - forwardedEmailAliasHandlers = forwardedEmailAliasHandlers, - plusAddressedEmailHandlers = plusAddressedEmailHandlers, - catchAllEmailHandlers = catchAllEmailHandlers, - randomWordHandlers = randomWordHandlers, - ) - } + }, + snackbarHost = { + BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { + ScrollContent( + state = state, + onRegenerateClick = onRegenerateClick, + onCopyClick = onCopyClick, + onUsernameSubStateOptionClicked = onUsernameOptionClicked, + passwordHandlers = passwordHandlers, + passphraseHandlers = passphraseHandlers, + usernameTypeHandlers = usernameTypeHandlers, + forwardedEmailAliasHandlers = forwardedEmailAliasHandlers, + plusAddressedEmailHandlers = plusAddressedEmailHandlers, + catchAllEmailHandlers = catchAllEmailHandlers, + randomWordHandlers = randomWordHandlers, + ) } } @@ -234,6 +227,7 @@ fun GeneratorScreen( private fun DefaultAppBar( scrollBehavior: TopAppBarScrollBehavior, onPasswordHistoryClick: () -> Unit, + modifier: Modifier = Modifier, ) { BitwardenMediumTopAppBar( title = stringResource(id = R.string.generator), @@ -249,15 +243,18 @@ private fun DefaultAppBar( ), ) }, + modifier = modifier, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModalAppBar( + generatorMode: GeneratorMode.Modal, scrollBehavior: TopAppBarScrollBehavior, onCloseClick: () -> Unit, onSelectClick: () -> Unit, + modifier: Modifier = Modifier, ) { BitwardenTopAppBar( title = stringResource(id = R.string.generator), @@ -265,6 +262,10 @@ private fun ModalAppBar( navigationIconContentDescription = stringResource(id = R.string.close), onNavigationIconClick = onCloseClick, scrollBehavior = scrollBehavior, + dividerStyle = when (generatorMode) { + GeneratorMode.Modal.Password -> TopAppBarDividerStyle.NONE + is GeneratorMode.Modal.Username -> TopAppBarDividerStyle.ON_SCROLL + }, actions = { BitwardenTextButton( label = stringResource(id = R.string.select), @@ -272,6 +273,7 @@ private fun ModalAppBar( modifier = Modifier.testTag("SelectButton"), ) }, + modifier = modifier, ) } @@ -365,6 +367,7 @@ private fun GeneratedStringItem( generatedText: String, onCopyClick: () -> Unit, onRegenerateClick: () -> Unit, + modifier: Modifier = Modifier, ) { BitwardenTextFieldWithActions( label = "", @@ -390,7 +393,7 @@ private fun GeneratedStringItem( textStyle = BitwardenTheme.typography.sensitiveInfoSmall, shouldAddCustomLineBreaks = true, visualTransformation = nonLetterColorVisualTransformation(), - modifier = Modifier.padding(horizontal = 16.dp), + modifier = modifier.padding(horizontal = 16.dp), ) } @@ -452,8 +455,12 @@ private fun ColumnScope.PasswordTypeContent( BitwardenSlider( value = passwordTypeState.length, - onValueChange = passwordHandlers.onPasswordSliderLengthChange, - range = passwordTypeState.computedMinimumLength..passwordTypeState.maxLength, + onValueChange = { newValue, isUserInteracting -> + if (newValue >= passwordTypeState.computedMinimumLength) { + passwordHandlers.onPasswordSliderLengthChange(newValue, isUserInteracting) + } + }, + range = passwordTypeState.minLength..passwordTypeState.maxLength, sliderTag = "PasswordLengthSlider", valueTag = "PasswordLengthLabel", modifier = Modifier @@ -469,26 +476,25 @@ private fun ColumnScope.PasswordTypeContent( PasswordCapitalLettersToggleItem( useCapitals = passwordTypeState.useCapitals, - onPasswordToggleCapitalLettersChange = - passwordHandlers.onPasswordToggleCapitalLettersChange, + onPasswordToggleCapitalLettersChange = passwordHandlers + .onPasswordToggleCapitalLettersChange, enabled = passwordTypeState.capitalsEnabled, ) PasswordLowercaseLettersToggleItem( useLowercase = passwordTypeState.useLowercase, - onPasswordToggleLowercaseLettersChange = - passwordHandlers.onPasswordToggleLowercaseLettersChange, + onPasswordToggleLowercaseLettersChange = passwordHandlers + .onPasswordToggleLowercaseLettersChange, enabled = passwordTypeState.lowercaseEnabled, ) PasswordNumbersToggleItem( useNumbers = passwordTypeState.useNumbers, - onPasswordToggleNumbersChange = - passwordHandlers.onPasswordToggleNumbersChange, + onPasswordToggleNumbersChange = passwordHandlers.onPasswordToggleNumbersChange, enabled = passwordTypeState.numbersEnabled, ) PasswordSpecialCharactersToggleItem( useSpecialChars = passwordTypeState.useSpecialChars, - onPasswordToggleSpecialCharactersChange = - passwordHandlers.onPasswordToggleSpecialCharactersChange, + onPasswordToggleSpecialCharactersChange = passwordHandlers + .onPasswordToggleSpecialCharactersChange, enabled = passwordTypeState.specialCharsEnabled, ) } @@ -497,8 +503,7 @@ private fun ColumnScope.PasswordTypeContent( PasswordMinNumbersCounterItem( minNumbers = passwordTypeState.minNumbers, - onPasswordMinNumbersCounterChange = - passwordHandlers.onPasswordMinNumbersCounterChange, + onPasswordMinNumbersCounterChange = passwordHandlers.onPasswordMinNumbersCounterChange, maxValue = max(passwordTypeState.maxNumbersAllowed, passwordTypeState.minNumbersAllowed), minValue = passwordTypeState.minNumbersAllowed, ) @@ -507,8 +512,8 @@ private fun ColumnScope.PasswordTypeContent( PasswordMinSpecialCharactersCounterItem( minSpecial = passwordTypeState.minSpecial, - onPasswordMinSpecialCharactersChange = - passwordHandlers.onPasswordMinSpecialCharactersChange, + onPasswordMinSpecialCharactersChange = passwordHandlers + .onPasswordMinSpecialCharactersChange, maxValue = max(passwordTypeState.maxSpecialAllowed, passwordTypeState.minSpecialAllowed), minValue = passwordTypeState.minSpecialAllowed, ) @@ -517,8 +522,8 @@ private fun ColumnScope.PasswordTypeContent( PasswordAvoidAmbiguousCharsToggleItem( avoidAmbiguousChars = passwordTypeState.avoidAmbiguousChars, - onPasswordToggleAvoidAmbiguousCharsChange = - passwordHandlers.onPasswordToggleAvoidAmbiguousCharsChange, + onPasswordToggleAvoidAmbiguousCharsChange = passwordHandlers + .onPasswordToggleAvoidAmbiguousCharsChange, enabled = passwordTypeState.ambiguousCharsEnabled, ) } @@ -527,18 +532,19 @@ private fun ColumnScope.PasswordTypeContent( private fun PasswordCapitalLettersToggleItem( useCapitals: Boolean, onPasswordToggleCapitalLettersChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( label = "A—Z", + contentDescription = stringResource(id = R.string.uppercase_ato_z), isChecked = useCapitals, onCheckedChange = onPasswordToggleCapitalLettersChange, enabled = enabled, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("UppercaseAtoZToggle") .padding(horizontal = 16.dp), - contentDescription = stringResource(id = R.string.uppercase_ato_z), ) } @@ -546,18 +552,19 @@ private fun PasswordCapitalLettersToggleItem( private fun PasswordLowercaseLettersToggleItem( useLowercase: Boolean, onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( label = "a—z", + contentDescription = stringResource(id = R.string.lowercase_ato_z), isChecked = useLowercase, onCheckedChange = onPasswordToggleLowercaseLettersChange, enabled = enabled, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("LowercaseAtoZToggle") .padding(horizontal = 16.dp), - contentDescription = stringResource(id = R.string.lowercase_ato_z), ) } @@ -565,18 +572,19 @@ private fun PasswordLowercaseLettersToggleItem( private fun PasswordNumbersToggleItem( useNumbers: Boolean, onPasswordToggleNumbersChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( label = "0-9", + contentDescription = stringResource(id = R.string.numbers_zero_to_nine), isChecked = useNumbers, onCheckedChange = onPasswordToggleNumbersChange, enabled = enabled, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("NumbersZeroToNineToggle") .padding(horizontal = 16.dp), - contentDescription = stringResource(id = R.string.numbers_zero_to_nine), ) } @@ -584,18 +592,19 @@ private fun PasswordNumbersToggleItem( private fun PasswordSpecialCharactersToggleItem( useSpecialChars: Boolean, onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( label = "!@#$%^&*", + contentDescription = stringResource(id = R.string.special_characters), isChecked = useSpecialChars, onCheckedChange = onPasswordToggleSpecialCharactersChange, enabled = enabled, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("SpecialCharactersToggle") .padding(horizontal = 16.dp), - contentDescription = stringResource(id = R.string.special_characters), ) } @@ -605,13 +614,14 @@ private fun PasswordMinNumbersCounterItem( onPasswordMinNumbersCounterChange: (Int) -> Unit, minValue: Int, maxValue: Int, + modifier: Modifier = Modifier, ) { BitwardenStepper( label = stringResource(id = R.string.min_numbers), value = minNumbers.coerceIn(minValue, maxValue), range = minValue..maxValue, onValueChange = onPasswordMinNumbersCounterChange, - modifier = Modifier + modifier = modifier .testTag("MinNumberValueLabel") .padding(horizontal = 16.dp), ) @@ -623,13 +633,14 @@ private fun PasswordMinSpecialCharactersCounterItem( onPasswordMinSpecialCharactersChange: (Int) -> Unit, minValue: Int, maxValue: Int, + modifier: Modifier = Modifier, ) { BitwardenStepper( label = stringResource(id = R.string.min_special), value = minSpecial.coerceIn(minValue, maxValue), range = minValue..maxValue, onValueChange = onPasswordMinSpecialCharactersChange, - modifier = Modifier + modifier = modifier .testTag("MinSpecialValueLabel") .padding(horizontal = 16.dp), ) @@ -639,6 +650,7 @@ private fun PasswordMinSpecialCharactersCounterItem( private fun PasswordAvoidAmbiguousCharsToggleItem( avoidAmbiguousChars: Boolean, onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( @@ -646,7 +658,7 @@ private fun PasswordAvoidAmbiguousCharsToggleItem( isChecked = avoidAmbiguousChars, enabled = enabled, onCheckedChange = onPasswordToggleAvoidAmbiguousCharsChange, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("AvoidAmbiguousCharsToggle") .padding(horizontal = 16.dp), @@ -666,8 +678,7 @@ private fun ColumnScope.PassphraseTypeContent( PassphraseNumWordsCounterItem( numWords = passphraseTypeState.numWords, - onPassphraseNumWordsCounterChange = - passphraseHandlers.onPassphraseNumWordsCounterChange, + onPassphraseNumWordsCounterChange = passphraseHandlers.onPassphraseNumWordsCounterChange, minValue = passphraseTypeState.minNumWords, maxValue = passphraseTypeState.maxNumWords, ) @@ -686,14 +697,14 @@ private fun ColumnScope.PassphraseTypeContent( ) { PassphraseCapitalizeToggleItem( capitalize = passphraseTypeState.capitalize, - onPassphraseCapitalizeToggleChange = - passphraseHandlers.onPassphraseCapitalizeToggleChange, + onPassphraseCapitalizeToggleChange = passphraseHandlers + .onPassphraseCapitalizeToggleChange, enabled = passphraseTypeState.capitalizeEnabled, ) PassphraseIncludeNumberToggleItem( includeNumber = passphraseTypeState.includeNumber, - onPassphraseIncludeNumberToggleChange = - passphraseHandlers.onPassphraseIncludeNumberToggleChange, + onPassphraseIncludeNumberToggleChange = passphraseHandlers + .onPassphraseIncludeNumberToggleChange, enabled = passphraseTypeState.includeNumberEnabled, ) } @@ -703,6 +714,7 @@ private fun ColumnScope.PassphraseTypeContent( private fun PassphraseNumWordsCounterItem( numWords: Int, onPassphraseNumWordsCounterChange: (Int) -> Unit, + modifier: Modifier = Modifier, minValue: Int = PASSPHRASE_MIN_NUMBER_OF_WORDS, maxValue: Int = PASSPHRASE_MAX_NUMBER_OF_WORDS, ) { @@ -714,7 +726,7 @@ private fun PassphraseNumWordsCounterItem( range = minValue..maxValue, onValueChange = onPassphraseNumWordsCounterChange, stepperActionsTestTag = "NumberOfWordsStepper", - modifier = Modifier + modifier = modifier .testTag("NumberOfWordsLabel") .padding(horizontal = 16.dp), ) @@ -724,6 +736,7 @@ private fun PassphraseNumWordsCounterItem( private fun PassphraseWordSeparatorInputItem( wordSeparator: Char?, onPassphraseWordSeparatorChange: (wordSeparator: Char?) -> Unit, + modifier: Modifier = Modifier, ) { BitwardenTextField( label = stringResource(id = R.string.word_separator), @@ -737,7 +750,7 @@ private fun PassphraseWordSeparatorInputItem( onPassphraseWordSeparatorChange(char) } }, - modifier = Modifier + modifier = modifier .testTag("WordSeparatorEntry") .fillMaxWidth() .padding(horizontal = 16.dp), @@ -748,6 +761,7 @@ private fun PassphraseWordSeparatorInputItem( private fun PassphraseCapitalizeToggleItem( capitalize: Boolean, onPassphraseCapitalizeToggleChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, enabled: Boolean = true, ) { BitwardenSwitch( @@ -755,7 +769,7 @@ private fun PassphraseCapitalizeToggleItem( isChecked = capitalize, onCheckedChange = onPassphraseCapitalizeToggleChange, enabled = enabled, - modifier = Modifier + modifier = modifier .testTag("CapitalizePassphraseToggle") .fillMaxWidth() .padding(horizontal = 16.dp), @@ -767,13 +781,14 @@ private fun PassphraseIncludeNumberToggleItem( includeNumber: Boolean, onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit, enabled: Boolean, + modifier: Modifier = Modifier, ) { BitwardenSwitch( label = stringResource(id = R.string.include_number), isChecked = includeNumber, enabled = enabled, onCheckedChange = onPassphraseIncludeNumberToggleChange, - modifier = Modifier + modifier = modifier .testTag("IncludeNumbersToggle") .fillMaxWidth() .padding(horizontal = 16.dp), @@ -832,6 +847,7 @@ private fun UsernameOptionsItem( currentSubState: GeneratorState.MainType.Username, onSubStateOptionClicked: (GeneratorState.MainType.Username.UsernameTypeOption) -> Unit, usernameTypeHandlers: UsernameTypeHandlers, + modifier: Modifier = Modifier, ) { val possibleSubStates = GeneratorState.MainType.Username.UsernameTypeOption.entries val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) } @@ -845,10 +861,6 @@ private fun UsernameOptionsItem( optionsWithStrings.entries.first { it.value == selectedOption }.key onSubStateOptionClicked(selectedOptionId) }, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .testTag("UsernameTypePicker"), supportingText = currentSubState.selectedType.supportingStringResId?.let { stringResource(id = it) }, @@ -856,6 +868,10 @@ private fun UsernameOptionsItem( onClick = usernameTypeHandlers.onUsernameTooltipClicked, contentDescription = stringResource(id = R.string.learn_more), ), + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .testTag("UsernameTypePicker"), ) } @@ -1002,6 +1018,7 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( private fun ServiceTypeOptionsItem( currentSubState: GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias, onSubStateOptionClicked: (ServiceTypeOption) -> Unit, + modifier: Modifier = Modifier, ) { val possibleSubStates = ServiceTypeOption.entries val optionsWithStrings = possibleSubStates.associateWith { stringResource(id = it.labelRes) } @@ -1017,7 +1034,7 @@ private fun ServiceTypeOptionsItem( optionsWithStrings.entries.first { it.value == selectedOption }.key onSubStateOptionClicked(selectedOptionId) }, - modifier = Modifier + modifier = modifier .padding(horizontal = 16.dp) .testTag("ServiceTypePicker") .fillMaxWidth(), @@ -1043,14 +1060,13 @@ private fun ColumnScope.PlusAddressedEmailTypeContent( private fun PlusAddressedEmailTextInputItem( email: String, onPlusAddressedEmailTextChange: (email: String) -> Unit, + modifier: Modifier = Modifier, ) { BitwardenTextField( label = stringResource(id = R.string.email_required_parenthesis), value = email, - onValueChange = { - onPlusAddressedEmailTextChange(it) - }, - modifier = Modifier + onValueChange = onPlusAddressedEmailTextChange, + modifier = modifier .fillMaxWidth() .testTag("PlusAddressedEmailEntry") .padding(horizontal = 16.dp), @@ -1078,14 +1094,13 @@ private fun ColumnScope.CatchAllEmailTypeContent( private fun CatchAllEmailTextInputItem( domain: String, onDomainTextChange: (domain: String) -> Unit, + modifier: Modifier = Modifier, ) { BitwardenTextField( label = stringResource(id = R.string.domain_name_required_parenthesis), value = domain, - onValueChange = { - onDomainTextChange(it) - }, - modifier = Modifier + onValueChange = onDomainTextChange, + modifier = modifier .fillMaxWidth() .testTag("CatchAllEmailDomainEntry") .padding(horizontal = 16.dp), @@ -1118,12 +1133,13 @@ private fun ColumnScope.RandomWordTypeContent( private fun RandomWordCapitalizeToggleItem( capitalize: Boolean, onRandomWordCapitalizeToggleChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { BitwardenSwitch( label = stringResource(id = R.string.capitalize), isChecked = capitalize, onCheckedChange = onRandomWordCapitalizeToggleChange, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("CapitalizeRandomWordUsernameToggle") .padding(horizontal = 16.dp), @@ -1134,12 +1150,13 @@ private fun RandomWordCapitalizeToggleItem( private fun RandomWordIncludeNumberToggleItem( includeNumber: Boolean, onRandomWordIncludeNumberToggleChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { BitwardenSwitch( label = stringResource(id = R.string.include_number), isChecked = includeNumber, onCheckedChange = onRandomWordIncludeNumberToggleChange, - modifier = Modifier + modifier = modifier .fillMaxWidth() .testTag("IncludeNumberRandomWordUsernameToggle") .padding(horizontal = 16.dp), @@ -1150,7 +1167,7 @@ private fun RandomWordIncludeNumberToggleItem( @Preview(showBackground = true) @Composable -private fun GeneratorPreview() { +private fun Generator_preview() { BitwardenTheme { GeneratorScreen( onNavigateToPasswordHistory = {}, @@ -1158,404 +1175,3 @@ private fun GeneratorPreview() { ) } } - -/** - * A class dedicated to handling user interactions related to password configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -@Suppress("LongParameterList") -private data class PasswordHandlers( - val onPasswordSliderLengthChange: (Int, Boolean) -> Unit, - val onPasswordToggleCapitalLettersChange: (Boolean) -> Unit, - val onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit, - val onPasswordToggleNumbersChange: (Boolean) -> Unit, - val onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit, - val onPasswordMinNumbersCounterChange: (Int) -> Unit, - val onPasswordMinSpecialCharactersChange: (Int) -> Unit, - val onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - @Suppress("LongMethod") - fun create(viewModel: GeneratorViewModel): PasswordHandlers { - return PasswordHandlers( - onPasswordSliderLengthChange = { newLength, isUserInteracting -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.SliderLengthChange( - length = newLength, - isUserInteracting = isUserInteracting, - ), - ) - }, - onPasswordToggleCapitalLettersChange = { shouldUseCapitals -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleCapitalLettersChange( - useCapitals = shouldUseCapitals, - ), - ) - }, - onPasswordToggleLowercaseLettersChange = { shouldUseLowercase -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleLowercaseLettersChange( - useLowercase = shouldUseLowercase, - ), - ) - }, - onPasswordToggleNumbersChange = { shouldUseNumbers -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleNumbersChange( - useNumbers = shouldUseNumbers, - ), - ) - }, - onPasswordToggleSpecialCharactersChange = { shouldUseSpecialChars -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleSpecialCharactersChange( - useSpecialChars = shouldUseSpecialChars, - ), - ) - }, - onPasswordMinNumbersCounterChange = { newMinNumbers -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.MinNumbersCounterChange( - minNumbers = newMinNumbers, - ), - ) - }, - onPasswordMinSpecialCharactersChange = { newMinSpecial -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.MinSpecialCharactersChange( - minSpecial = newMinSpecial, - ), - ) - }, - onPasswordToggleAvoidAmbiguousCharsChange = { shouldAvoidAmbiguousChars -> - viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange( - avoidAmbiguousChars = shouldAvoidAmbiguousChars, - ), - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to passphrase configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -private data class PassphraseHandlers( - val onPassphraseNumWordsCounterChange: (Int) -> Unit, - val onPassphraseWordSeparatorChange: (Char?) -> Unit, - val onPassphraseCapitalizeToggleChange: (Boolean) -> Unit, - val onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - fun create(viewModel: GeneratorViewModel): PassphraseHandlers { - return PassphraseHandlers( - onPassphraseNumWordsCounterChange = { changeInCounter -> - viewModel.trySendAction( - GeneratorAction.MainType.Passphrase.NumWordsCounterChange( - numWords = changeInCounter, - ), - ) - }, - onPassphraseWordSeparatorChange = { newSeparator -> - viewModel.trySendAction( - GeneratorAction.MainType.Passphrase.WordSeparatorTextChange( - wordSeparator = newSeparator, - ), - ) - }, - onPassphraseCapitalizeToggleChange = { shouldCapitalize -> - viewModel.trySendAction( - GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange( - capitalize = shouldCapitalize, - ), - ) - }, - onPassphraseIncludeNumberToggleChange = { shouldIncludeNumber -> - viewModel.trySendAction( - GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange( - includeNumber = shouldIncludeNumber, - ), - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to all username configurations. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -@Suppress("LongParameterList") -private data class UsernameTypeHandlers( - val onUsernameTooltipClicked: () -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - fun create(viewModel: GeneratorViewModel): UsernameTypeHandlers { - return UsernameTypeHandlers( - onUsernameTooltipClicked = { - viewModel.trySendAction( - GeneratorAction.MainType.Username.UsernameType.TooltipClick, - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to forwarded email alias - * configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -@Suppress("LongParameterList") -private data class ForwardedEmailAliasHandlers( - val onServiceChange: (ServiceTypeOption) -> Unit, - val onAddyIoAccessTokenTextChange: (String) -> Unit, - val onAddyIoDomainNameTextChange: (String) -> Unit, - val onDuckDuckGoApiKeyTextChange: (String) -> Unit, - val onFastMailApiKeyTextChange: (String) -> Unit, - val onFirefoxRelayAccessTokenTextChange: (String) -> Unit, - val onForwardEmailApiKeyTextChange: (String) -> Unit, - val onForwardEmailDomainNameTextChange: (String) -> Unit, - val onSimpleLoginApiKeyTextChange: (String) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - @Suppress("LongMethod") - fun create(viewModel: GeneratorViewModel): ForwardedEmailAliasHandlers { - return ForwardedEmailAliasHandlers( - onServiceChange = { newServiceTypeOption -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .ServiceTypeOptionSelect( - serviceTypeOption = newServiceTypeOption, - ), - ) - }, - onAddyIoAccessTokenTextChange = { newAccessToken -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .AddyIo - .AccessTokenTextChange( - accessToken = newAccessToken, - ), - ) - }, - onAddyIoDomainNameTextChange = { newDomainName -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .AddyIo - .DomainTextChange( - domain = newDomainName, - ), - ) - }, - onDuckDuckGoApiKeyTextChange = { newApiKey -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .DuckDuckGo - .ApiKeyTextChange( - apiKey = newApiKey, - ), - ) - }, - onFastMailApiKeyTextChange = { newApiKey -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .FastMail - .ApiKeyTextChange( - apiKey = newApiKey, - ), - ) - }, - onFirefoxRelayAccessTokenTextChange = { newAccessToken -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .FirefoxRelay - .AccessTokenTextChange( - accessToken = newAccessToken, - ), - ) - }, - onForwardEmailApiKeyTextChange = { newApiKey -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .ForwardEmail - .ApiKeyTextChange( - apiKey = newApiKey, - ), - ) - }, - onForwardEmailDomainNameTextChange = { newDomainName -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .ForwardEmail - .DomainNameTextChange( - domainName = newDomainName, - ), - ) - }, - onSimpleLoginApiKeyTextChange = { newApiKey -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .ForwardedEmailAlias - .SimpleLogin - .ApiKeyTextChange( - apiKey = newApiKey, - ), - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to plus addressed email - * configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -private data class PlusAddressedEmailHandlers( - val onEmailChange: (String) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - fun create(viewModel: GeneratorViewModel): PlusAddressedEmailHandlers { - return PlusAddressedEmailHandlers( - onEmailChange = { newEmail -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .PlusAddressedEmail - .EmailTextChange( - email = newEmail, - ), - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to plus addressed email - * configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -private data class CatchAllEmailHandlers( - val onDomainChange: (String) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - fun create(viewModel: GeneratorViewModel): CatchAllEmailHandlers { - return CatchAllEmailHandlers( - onDomainChange = { newDomain -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .CatchAllEmail - .DomainTextChange( - domain = newDomain, - ), - ) - }, - ) - } - } -} - -/** - * A class dedicated to handling user interactions related to Random Word - * configuration. - * Each lambda corresponds to a specific user action, allowing for easy delegation of - * logic when user input is detected. - */ -private data class RandomWordHandlers( - val onCapitalizeChange: (Boolean) -> Unit, - val onIncludeNumberChange: (Boolean) -> Unit, -) { - @Suppress("UndocumentedPublicClass") - companion object { - fun create(viewModel: GeneratorViewModel): RandomWordHandlers { - return RandomWordHandlers( - onCapitalizeChange = { shouldCapitalize -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .RandomWord - .ToggleCapitalizeChange( - capitalize = shouldCapitalize, - ), - ) - }, - onIncludeNumberChange = { shouldIncludeNumber -> - viewModel.trySendAction( - GeneratorAction - .MainType - .Username - .UsernameType - .RandomWord - .ToggleIncludeNumberChange( - includeNumber = shouldIncludeNumber, - ), - ) - }, - ) - } - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index 3be394188..c59dea833 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -213,7 +213,7 @@ class GeneratorViewModel @Inject constructor( handleUpdateGeneratedPassphraseResult(action) } - is GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult -> { + is GeneratorAction.Internal.UpdateGeneratedPlusAddressedUsernameResult -> { handleUpdatePlusAddressedGeneratedUsernameResult(action) } @@ -676,7 +676,7 @@ class GeneratorViewModel @Inject constructor( } private fun handleUpdatePlusAddressedGeneratedUsernameResult( - action: GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult, + action: GeneratorAction.Internal.UpdateGeneratedPlusAddressedUsernameResult, ) { when (val result = action.result) { is GeneratedPlusAddressedUsernameResult.Success -> { @@ -809,7 +809,7 @@ class GeneratorViewModel @Inject constructor( handleMinSpecialChange(action) } - is GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange -> { + is GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange -> { handleToggleAmbiguousChars(action) } } @@ -895,7 +895,7 @@ class GeneratorViewModel @Inject constructor( } private fun handleToggleAmbiguousChars( - action: GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange, + action: GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange, ) { updatePasswordType { currentPasswordType -> currentPasswordType.copy( @@ -1440,7 +1440,7 @@ class GeneratorViewModel @Inject constructor( email = plusAddressedEmail.email, ), ) - sendAction(GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult(result)) + sendAction(GeneratorAction.Internal.UpdateGeneratedPlusAddressedUsernameResult(result)) } private suspend fun generateCatchAllEmail(catchAllEmail: CatchAllEmail) { @@ -2264,7 +2264,7 @@ sealed class GeneratorAction { * @property avoidAmbiguousChars Flag indicating whether ambiguous characters * should be avoided. */ - data class ToggleAvoidAmbigousCharactersChange( + data class ToggleAvoidAmbiguousCharactersChange( val avoidAmbiguousChars: Boolean, ) : Password() } @@ -2519,7 +2519,7 @@ sealed class GeneratorAction { /** * Indicates a generated text update is received. */ - data class UpdateGeneratedPlusAddessedUsernameResult( + data class UpdateGeneratedPlusAddressedUsernameResult( val result: GeneratedPlusAddressedUsernameResult, ) : Internal() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/CatchAllEmailHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/CatchAllEmailHandlers.kt new file mode 100644 index 000000000..cc99b75e0 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/CatchAllEmailHandlers.kt @@ -0,0 +1,40 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction.MainType.Username.UsernameType.CatchAllEmail as CatchAllEmailAction + +/** + * A class dedicated to handling user interactions related to plus addressed email + * configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +data class CatchAllEmailHandlers( + val onDomainChange: (String) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [CatchAllEmailHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): CatchAllEmailHandlers = CatchAllEmailHandlers( + onDomainChange = { newDomain -> + viewModel.trySendAction(CatchAllEmailAction.DomainTextChange(domain = newDomain)) + }, + ) + } +} + +/** + * Helper function to remember a [CatchAllEmailHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberCatchAllEmailHandlers(viewModel: GeneratorViewModel): CatchAllEmailHandlers = + remember(viewModel) { + CatchAllEmailHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/ForwardedEmailAliasHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/ForwardedEmailAliasHandlers.kt new file mode 100644 index 000000000..916b467d5 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/ForwardedEmailAliasHandlers.kt @@ -0,0 +1,102 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias as ForwardedEmailAliasAction + +/** + * A class dedicated to handling user interactions related to forwarded email alias + * configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +@Suppress("LongParameterList") +data class ForwardedEmailAliasHandlers( + val onServiceChange: (ForwardedEmailAlias.ServiceTypeOption) -> Unit, + val onAddyIoAccessTokenTextChange: (String) -> Unit, + val onAddyIoDomainNameTextChange: (String) -> Unit, + val onDuckDuckGoApiKeyTextChange: (String) -> Unit, + val onFastMailApiKeyTextChange: (String) -> Unit, + val onFirefoxRelayAccessTokenTextChange: (String) -> Unit, + val onForwardEmailApiKeyTextChange: (String) -> Unit, + val onForwardEmailDomainNameTextChange: (String) -> Unit, + val onSimpleLoginApiKeyTextChange: (String) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [ForwardedEmailAliasHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): ForwardedEmailAliasHandlers = ForwardedEmailAliasHandlers( + onServiceChange = { newServiceTypeOption -> + viewModel.trySendAction( + ForwardedEmailAliasAction.ServiceTypeOptionSelect( + serviceTypeOption = newServiceTypeOption, + ), + ) + }, + onAddyIoAccessTokenTextChange = { newAccessToken -> + viewModel.trySendAction( + ForwardedEmailAliasAction.AddyIo.AccessTokenTextChange( + accessToken = newAccessToken, + ), + ) + }, + onAddyIoDomainNameTextChange = { newDomainName -> + viewModel.trySendAction( + ForwardedEmailAliasAction.AddyIo.DomainTextChange(domain = newDomainName), + ) + }, + onDuckDuckGoApiKeyTextChange = { newApiKey -> + viewModel.trySendAction( + ForwardedEmailAliasAction.DuckDuckGo.ApiKeyTextChange(apiKey = newApiKey), + ) + }, + onFastMailApiKeyTextChange = { newApiKey -> + viewModel.trySendAction( + ForwardedEmailAliasAction.FastMail.ApiKeyTextChange(apiKey = newApiKey), + ) + }, + onFirefoxRelayAccessTokenTextChange = { newAccessToken -> + viewModel.trySendAction( + ForwardedEmailAliasAction.FirefoxRelay.AccessTokenTextChange( + accessToken = newAccessToken, + ), + ) + }, + onForwardEmailApiKeyTextChange = { newApiKey -> + viewModel.trySendAction( + ForwardedEmailAliasAction.ForwardEmail.ApiKeyTextChange(apiKey = newApiKey), + ) + }, + onForwardEmailDomainNameTextChange = { newDomainName -> + viewModel.trySendAction( + ForwardedEmailAliasAction.ForwardEmail.DomainNameTextChange( + domainName = newDomainName, + ), + ) + }, + onSimpleLoginApiKeyTextChange = { newApiKey -> + viewModel.trySendAction( + ForwardedEmailAliasAction.SimpleLogin.ApiKeyTextChange(apiKey = newApiKey), + ) + }, + ) + } +} + +/** + * Helper function to remember a [ForwardedEmailAliasHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberForwardedEmailAliasHandlers( + viewModel: GeneratorViewModel, +): ForwardedEmailAliasHandlers = + remember(viewModel) { + ForwardedEmailAliasHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PassphraseHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PassphraseHandlers.kt new file mode 100644 index 000000000..90a99bf28 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PassphraseHandlers.kt @@ -0,0 +1,67 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel + +/** + * A class dedicated to handling user interactions related to passphrase configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +data class PassphraseHandlers( + val onPassphraseNumWordsCounterChange: (Int) -> Unit, + val onPassphraseWordSeparatorChange: (Char?) -> Unit, + val onPassphraseCapitalizeToggleChange: (Boolean) -> Unit, + val onPassphraseIncludeNumberToggleChange: (Boolean) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [PassphraseHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): PassphraseHandlers = PassphraseHandlers( + onPassphraseNumWordsCounterChange = { changeInCounter -> + viewModel.trySendAction( + GeneratorAction.MainType.Passphrase.NumWordsCounterChange( + numWords = changeInCounter, + ), + ) + }, + onPassphraseWordSeparatorChange = { newSeparator -> + viewModel.trySendAction( + GeneratorAction.MainType.Passphrase.WordSeparatorTextChange( + wordSeparator = newSeparator, + ), + ) + }, + onPassphraseCapitalizeToggleChange = { shouldCapitalize -> + viewModel.trySendAction( + GeneratorAction.MainType.Passphrase.ToggleCapitalizeChange( + capitalize = shouldCapitalize, + ), + ) + }, + onPassphraseIncludeNumberToggleChange = { shouldIncludeNumber -> + viewModel.trySendAction( + GeneratorAction.MainType.Passphrase.ToggleIncludeNumberChange( + includeNumber = shouldIncludeNumber, + ), + ) + }, + ) + } +} + +/** + * Helper function to remember a [PassphraseHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberPassphraseHandlers(viewModel: GeneratorViewModel): PassphraseHandlers = + remember(viewModel) { + PassphraseHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PasswordHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PasswordHandlers.kt new file mode 100644 index 000000000..e97d1f684 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PasswordHandlers.kt @@ -0,0 +1,101 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel + +/** + * A class dedicated to handling user interactions related to password configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +@Suppress("LongParameterList") +data class PasswordHandlers( + val onPasswordSliderLengthChange: (Int, Boolean) -> Unit, + val onPasswordToggleCapitalLettersChange: (Boolean) -> Unit, + val onPasswordToggleLowercaseLettersChange: (Boolean) -> Unit, + val onPasswordToggleNumbersChange: (Boolean) -> Unit, + val onPasswordToggleSpecialCharactersChange: (Boolean) -> Unit, + val onPasswordMinNumbersCounterChange: (Int) -> Unit, + val onPasswordMinSpecialCharactersChange: (Int) -> Unit, + val onPasswordToggleAvoidAmbiguousCharsChange: (Boolean) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [PasswordHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): PasswordHandlers = PasswordHandlers( + onPasswordSliderLengthChange = { newLength, isUserInteracting -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.SliderLengthChange( + length = newLength, + isUserInteracting = isUserInteracting, + ), + ) + }, + onPasswordToggleCapitalLettersChange = { shouldUseCapitals -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.ToggleCapitalLettersChange( + useCapitals = shouldUseCapitals, + ), + ) + }, + onPasswordToggleLowercaseLettersChange = { shouldUseLowercase -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.ToggleLowercaseLettersChange( + useLowercase = shouldUseLowercase, + ), + ) + }, + onPasswordToggleNumbersChange = { shouldUseNumbers -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.ToggleNumbersChange( + useNumbers = shouldUseNumbers, + ), + ) + }, + onPasswordToggleSpecialCharactersChange = { shouldUseSpecialChars -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.ToggleSpecialCharactersChange( + useSpecialChars = shouldUseSpecialChars, + ), + ) + }, + onPasswordMinNumbersCounterChange = { newMinNumbers -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.MinNumbersCounterChange( + minNumbers = newMinNumbers, + ), + ) + }, + onPasswordMinSpecialCharactersChange = { newMinSpecial -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.MinSpecialCharactersChange( + minSpecial = newMinSpecial, + ), + ) + }, + onPasswordToggleAvoidAmbiguousCharsChange = { shouldAvoidAmbiguousChars -> + viewModel.trySendAction( + GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange( + avoidAmbiguousChars = shouldAvoidAmbiguousChars, + ), + ) + }, + ) + } +} + +/** + * Helper function to remember a [PasswordHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberPasswordHandlers(viewModel: GeneratorViewModel): PasswordHandlers = + remember(viewModel) { + PasswordHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PlusAddressedEmailHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PlusAddressedEmailHandlers.kt new file mode 100644 index 000000000..009928082 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/PlusAddressedEmailHandlers.kt @@ -0,0 +1,41 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail as PlusAddressedEmailAction + +/** + * A class dedicated to handling user interactions related to plus addressed email + * configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +data class PlusAddressedEmailHandlers( + val onEmailChange: (String) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [PlusAddressedEmailHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create(viewModel: GeneratorViewModel): PlusAddressedEmailHandlers = + PlusAddressedEmailHandlers( + onEmailChange = { newEmail -> + viewModel.trySendAction( + PlusAddressedEmailAction.EmailTextChange(email = newEmail), + ) + }, + ) + } +} + +/** + * Helper function to remember a [PlusAddressedEmailHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberPlusAddressedEmailHandlers(viewModel: GeneratorViewModel): PlusAddressedEmailHandlers = + remember(viewModel) { + PlusAddressedEmailHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/RandomWordHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/RandomWordHandlers.kt new file mode 100644 index 000000000..a90ff6925 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/RandomWordHandlers.kt @@ -0,0 +1,48 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction.MainType.Username.UsernameType.RandomWord as RandomWordAction + +/** + * A class dedicated to handling user interactions related to Random Word + * configuration. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +data class RandomWordHandlers( + val onCapitalizeChange: (Boolean) -> Unit, + val onIncludeNumberChange: (Boolean) -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [RandomWordHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): RandomWordHandlers = RandomWordHandlers( + onCapitalizeChange = { shouldCapitalize -> + viewModel.trySendAction( + RandomWordAction.ToggleCapitalizeChange(capitalize = shouldCapitalize), + ) + }, + onIncludeNumberChange = { shouldIncludeNumber -> + viewModel.trySendAction( + RandomWordAction.ToggleIncludeNumberChange(includeNumber = shouldIncludeNumber), + ) + }, + ) + } +} + +/** + * Helper function to remember a [RandomWordHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberRandomWordHandlers(viewModel: GeneratorViewModel): RandomWordHandlers = + remember(viewModel) { + RandomWordHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/UsernameTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/UsernameTypeHandlers.kt new file mode 100644 index 000000000..485937e4d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/handlers/UsernameTypeHandlers.kt @@ -0,0 +1,40 @@ +package com.x8bit.bitwarden.ui.tools.feature.generator.handlers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorAction +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorViewModel + +/** + * A class dedicated to handling user interactions related to all username configurations. + * Each lambda corresponds to a specific user action, allowing for easy delegation of + * logic when user input is detected. + */ +@Suppress("LongParameterList") +data class UsernameTypeHandlers( + val onUsernameTooltipClicked: () -> Unit, +) { + @Suppress("UndocumentedPublicClass") + companion object { + /** + * Creates an instance of [UsernameTypeHandlers] by binding actions to the provided + * [GeneratorViewModel]. + */ + fun create( + viewModel: GeneratorViewModel, + ): UsernameTypeHandlers = UsernameTypeHandlers( + onUsernameTooltipClicked = { + viewModel.trySendAction(GeneratorAction.MainType.Username.UsernameType.TooltipClick) + }, + ) + } +} + +/** + * Helper function to remember a [UsernameTypeHandlers] instance in a [Composable] scope. + */ +@Composable +fun rememberUsernameTypeHandlers(viewModel: GeneratorViewModel): UsernameTypeHandlers = + remember(viewModel) { + UsernameTypeHandlers.create(viewModel = viewModel) + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt index 9bb0b84fe..149639760 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryScreen.kt @@ -97,30 +97,24 @@ fun PasswordHistoryScreen( }, ) }, - content = { innerPadding -> + content = { when (val viewState = state.viewState) { is PasswordHistoryState.ViewState.Loading -> { PasswordHistoryLoading( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is PasswordHistoryState.ViewState.Error -> { PasswordHistoryError( state = viewState, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } is PasswordHistoryState.ViewState.Empty -> { PasswordHistoryEmpty( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } @@ -129,8 +123,7 @@ fun PasswordHistoryScreen( state = viewState, modifier = Modifier .fillMaxSize() - .imePadding() - .padding(innerPadding), + .imePadding(), onPasswordCopyClick = { password -> viewModel.trySendAction( PasswordHistoryAction.PasswordCopyClick(password), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt index 9baa6b920..36bff1330 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt @@ -21,6 +21,8 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers import kotlinx.collections.immutable.toImmutableList +private const val SEND_TYPES_COUNT: Int = 2 + /** * Content view for the [SendScreen]. */ @@ -48,6 +50,7 @@ fun SendContent( item { BitwardenListHeaderText( label = stringResource(id = R.string.types), + supportingLabel = SEND_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt index 52f37ef03..fa24a7f81 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -76,7 +77,8 @@ fun SendEmpty( color = BitwardenTheme.colorScheme.text.primary, modifier = Modifier .fillMaxWidth() - .standardHorizontalMargin(), + .standardHorizontalMargin() + .testTag("EmptySendListText"), ) Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt index 2502e46ad..df6027502 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -161,11 +160,10 @@ fun SendScreen( } }, pullToRefreshState = pullToRefreshState, - ) { padding -> + ) { val modifier = Modifier .imePadding() .fillMaxSize() - .padding(padding) when (val viewState = state.viewState) { is SendState.ViewState.Content -> SendContent( policyDisablesSend = state.policyDisablesSend, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt index 5835e29bb..add9a98ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,22 +34,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText -import com.x8bit.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton -import com.x8bit.bitwarden.ui.platform.components.segment.SegmentedButtonState import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers -import kotlinx.collections.immutable.persistentListOf /** * Content view for the [AddSendScreen]. @@ -60,7 +55,6 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun AddSendContent( state: AddSendState.ViewState.Content, - scrollBehavior: TopAppBarScrollBehavior, policyDisablesSend: Boolean, policySendOptionsInEffect: Boolean, isAddMode: Boolean, @@ -72,201 +66,177 @@ fun AddSendContent( val chooseFileCameraPermissionLauncher = permissionsManager.getLauncher { isGranted -> addSendHandlers.onChooseFileClick(isGranted) } - Column(modifier = modifier) { - if (isAddMode && !isShared) { - BitwardenSegmentedButton( + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + ) { + if (policyDisablesSend) { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenInfoCalloutCard( + text = stringResource(id = R.string.send_disabled_warning), modifier = Modifier - .scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior) - .fillMaxWidth(), - options = persistentListOf( - SegmentedButtonState( - text = stringResource(id = R.string.file), - onClick = addSendHandlers.onFileTypeSelect, - isChecked = state.isFileType, - testTag = "SendFileButton", - ), - SegmentedButtonState( - text = stringResource(id = R.string.text), - onClick = addSendHandlers.onTextTypeSelect, - isChecked = state.isTextType, - testTag = "SendTextButton", - ), - ), + .padding(horizontal = 16.dp) + .fillMaxWidth() + .testTag("SendPolicyInEffectLabel"), ) + Spacer(modifier = Modifier.height(16.dp)) } - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - if (policyDisablesSend) { - Spacer(modifier = Modifier.height(8.dp)) - BitwardenInfoCalloutCard( - text = stringResource(id = R.string.send_disabled_warning), - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth() - .testTag("SendPolicyInEffectLabel"), - ) - Spacer(modifier = Modifier.height(16.dp)) - } - - if (policySendOptionsInEffect) { - BitwardenInfoCalloutCard( - text = stringResource(id = R.string.send_options_policy_in_effect), - modifier = Modifier - .testTag(tag = "SendPolicyInEffectLabel") - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - } - - BitwardenTextField( + if (policySendOptionsInEffect) { + BitwardenInfoCalloutCard( + text = stringResource(id = R.string.send_options_policy_in_effect), modifier = Modifier - .testTag(tag = "SendNameEntry") - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.name), - hint = stringResource(id = R.string.name_info), - readOnly = policyDisablesSend, - value = state.common.name, - onValueChange = addSendHandlers.onNamChange, + .testTag(tag = "SendPolicyInEffectLabel") + .padding(horizontal = 16.dp) + .fillMaxWidth(), ) - Spacer(modifier = Modifier.height(8.dp)) - when (val type = state.selectedType) { - is AddSendState.ViewState.Content.SendType.File -> { - BitwardenListHeaderText( - label = stringResource(id = R.string.file), + Spacer(modifier = Modifier.height(16.dp)) + } + + BitwardenTextField( + modifier = Modifier + .testTag(tag = "SendNameEntry") + .fillMaxWidth() + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.name), + hint = stringResource(id = R.string.name_info), + readOnly = policyDisablesSend, + value = state.common.name, + onValueChange = addSendHandlers.onNamChange, + ) + + Spacer(modifier = Modifier.height(8.dp)) + when (val type = state.selectedType) { + is AddSendState.ViewState.Content.SendType.File -> { + BitwardenListHeaderText( + label = stringResource(id = R.string.file), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + if (isShared) { + Text( + text = type.name.orEmpty(), + color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodyMedium, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.max_file_size), + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } else if (isAddMode) { + Text( + modifier = Modifier + .testTag(tag = "SendCurrentFileNameLabel") + .align(Alignment.CenterHorizontally), + text = type.name ?: stringResource(id = R.string.no_file_chosen), + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenOutlinedButton( + label = stringResource(id = R.string.choose_file), + onClick = { + @Suppress("MaxLineLength") + if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) { + addSendHandlers.onChooseFileClick(true) + } else { + chooseFileCameraPermissionLauncher.launch( + Manifest.permission.CAMERA, + ) + } + }, + modifier = Modifier + .testTag(tag = "SendChooseFileButton") + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.max_file_size), + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + ) Spacer(modifier = Modifier.height(16.dp)) - if (isShared) { + Text( + text = stringResource(id = R.string.type_file_info), + color = BitwardenTheme.colorScheme.text.secondary, + style = BitwardenTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { Text( text = type.name.orEmpty(), color = BitwardenTheme.colorScheme.text.primary, + style = BitwardenTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = type.displaySize.orEmpty(), + color = BitwardenTheme.colorScheme.text.primary, style = BitwardenTheme.typography.bodyMedium, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.max_file_size), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } else if (isAddMode) { - Text( - modifier = Modifier - .testTag(tag = "SendCurrentFileNameLabel") - .align(Alignment.CenterHorizontally), - text = type.name ?: stringResource(id = R.string.no_file_chosen), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenOutlinedButton( - label = stringResource(id = R.string.choose_file), - onClick = { - @Suppress("MaxLineLength") - if (permissionsManager.checkPermission(Manifest.permission.CAMERA)) { - addSendHandlers.onChooseFileClick(true) - } else { - chooseFileCameraPermissionLauncher.launch( - Manifest.permission.CAMERA, - ) - } - }, - modifier = Modifier - .testTag(tag = "SendChooseFileButton") - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.max_file_size), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.type_file_info), - color = BitwardenTheme.colorScheme.text.secondary, - style = BitwardenTheme.typography.bodySmall, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } else { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Text( - text = type.name.orEmpty(), - color = BitwardenTheme.colorScheme.text.primary, - style = BitwardenTheme.typography.bodyLarge, - modifier = Modifier.weight(1f), - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = type.displaySize.orEmpty(), - color = BitwardenTheme.colorScheme.text.primary, - style = BitwardenTheme.typography.bodyMedium, - ) - } } } - - is AddSendState.ViewState.Content.SendType.Text -> { - BitwardenTextField( - modifier = Modifier - .testTag(tag = "SendTextContentEntry") - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.text), - hint = stringResource(id = R.string.type_text_info), - readOnly = policyDisablesSend, - value = type.input, - singleLine = false, - onValueChange = addSendHandlers.onTextChange, - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenSwitch( - modifier = Modifier - .testTag(tag = "SendHideTextByDefaultToggle") - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.hide_text_by_default), - isChecked = type.isHideByDefaultChecked, - onCheckedChange = addSendHandlers.onIsHideByDefaultToggle, - readOnly = policyDisablesSend, - ) - } } - Spacer(modifier = Modifier.height(16.dp)) - AddSendOptions( - state = state, - sendRestrictionPolicy = policyDisablesSend, - isAddMode = isAddMode, - addSendHandlers = addSendHandlers, - ) - - Spacer(modifier = Modifier.height(24.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) + is AddSendState.ViewState.Content.SendType.Text -> { + BitwardenTextField( + modifier = Modifier + .testTag(tag = "SendTextContentEntry") + .fillMaxWidth() + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.text), + hint = stringResource(id = R.string.type_text_info), + readOnly = policyDisablesSend, + value = type.input, + singleLine = false, + onValueChange = addSendHandlers.onTextChange, + ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenSwitch( + modifier = Modifier + .testTag(tag = "SendHideTextByDefaultToggle") + .fillMaxWidth() + .padding(horizontal = 16.dp), + label = stringResource(id = R.string.hide_text_by_default), + isChecked = type.isHideByDefaultChecked, + onCheckedChange = addSendHandlers.onIsHideByDefaultToggle, + readOnly = policyDisablesSend, + ) + } } + + Spacer(modifier = Modifier.height(16.dp)) + AddSendOptions( + state = state, + sendRestrictionPolicy = policyDisablesSend, + isAddMode = isAddMode, + addSendHandlers = addSendHandlers, + ) + + Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt index 1aa631e55..393c6907a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt @@ -3,8 +3,8 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -23,6 +23,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem @@ -37,6 +38,8 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton +import com.x8bit.bitwarden.ui.platform.components.segment.SegmentedButtonState import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager @@ -46,11 +49,12 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers +import kotlinx.collections.immutable.persistentListOf /** * Displays new send UX. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddSendScreen( @@ -187,16 +191,41 @@ fun AddSendScreen( }, ) }, - ) { innerPadding -> + utilityBar = { + val viewState = state.viewState + if (state.isAddMode && + !state.isShared && + viewState is AddSendState.ViewState.Content + ) { + BitwardenSegmentedButton( + modifier = Modifier + .scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior) + .fillMaxWidth(), + options = persistentListOf( + SegmentedButtonState( + text = stringResource(id = R.string.file), + onClick = addSendHandlers.onFileTypeSelect, + isChecked = viewState.isFileType, + testTag = "SendFileButton", + ), + SegmentedButtonState( + text = stringResource(id = R.string.text), + onClick = addSendHandlers.onTextTypeSelect, + isChecked = viewState.isTextType, + testTag = "SendTextButton", + ), + ), + ) + } + }, + ) { val modifier = Modifier .imePadding() .fillMaxSize() - .padding(paddingValues = innerPadding) when (val viewState = state.viewState) { is AddSendState.ViewState.Content -> AddSendContent( state = viewState, - scrollBehavior = scrollBehavior, policyDisablesSend = state.policyDisablesSend, policySendOptionsInEffect = state.shouldDisplayPolicyWarning, isAddMode = state.isAddMode, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt index ea2ec0fa6..c62ceb612 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt @@ -26,12 +26,16 @@ private const val ADD_ITEM_TYPE: String = "vault_add_item_type" private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item" private const val ADD_EDIT_ITEM_TYPE: String = "vault_add_edit_type" +private const val ADD_SELECTED_FOLDER_ID: String = "vault_add_selected_folder_id" +private const val ADD_SELECTED_COLLECTION_ID: String = "vault_add_selected_collection_id" private const val ADD_EDIT_ITEM_ROUTE: String = ADD_EDIT_ITEM_PREFIX + "/{$ADD_EDIT_ITEM_TYPE}" + "?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" + - "?$ADD_ITEM_TYPE={$ADD_ITEM_TYPE}" + "?$ADD_ITEM_TYPE={$ADD_ITEM_TYPE}" + + "?$ADD_SELECTED_FOLDER_ID={$ADD_SELECTED_FOLDER_ID}" + + "?$ADD_SELECTED_COLLECTION_ID={$ADD_SELECTED_COLLECTION_ID}" /** * Class to retrieve vault add & edit arguments from the [SavedStateHandle]. @@ -39,6 +43,8 @@ private const val ADD_EDIT_ITEM_ROUTE: String = @OmitFromCoverage data class VaultAddEditArgs( val vaultAddEditType: VaultAddEditType, + val selectedFolderId: String? = null, + val selectedCollectionId: String? = null, ) { constructor(savedStateHandle: SavedStateHandle) : this( vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { @@ -53,6 +59,8 @@ data class VaultAddEditArgs( CLONE_TYPE -> VaultAddEditType.CloneItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) else -> throw IllegalStateException("Unknown VaultAddEditType.") }, + selectedFolderId = savedStateHandle[ADD_SELECTED_FOLDER_ID], + selectedCollectionId = savedStateHandle[ADD_SELECTED_COLLECTION_ID], ) } @@ -72,6 +80,14 @@ fun NavGraphBuilder.vaultAddEditDestination( route = ADD_EDIT_ITEM_ROUTE, arguments = listOf( navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType }, + navArgument(ADD_SELECTED_FOLDER_ID) { + type = NavType.StringType + nullable = true + }, + navArgument(ADD_SELECTED_COLLECTION_ID) { + type = NavType.StringType + nullable = true + }, ), ) { VaultAddEditScreen( @@ -90,12 +106,16 @@ fun NavGraphBuilder.vaultAddEditDestination( */ fun NavController.navigateToVaultAddEdit( vaultAddEditType: VaultAddEditType, + selectedFolderId: String? = null, + selectedCollectionId: String? = null, navOptions: NavOptions? = null, ) { navigate( route = "$ADD_EDIT_ITEM_PREFIX/${vaultAddEditType.toTypeString()}" + "?$EDIT_ITEM_ID=${vaultAddEditType.toIdOrNull()}" + - "?$ADD_ITEM_TYPE=${vaultAddEditType.toVaultItemCipherTypeOrNull()}", + "?$ADD_ITEM_TYPE=${vaultAddEditType.toVaultItemCipherTypeOrNull()}" + + "?$ADD_SELECTED_FOLDER_ID=$selectedFolderId" + + "?$ADD_SELECTED_COLLECTION_ID=$selectedCollectionId", navOptions = navOptions, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt index 0e302f146..f33e0b751 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt @@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -312,7 +311,7 @@ fun VaultAddEditScreen( .takeUnless { state.isAddItemMode || !state.isCipherInCollection || - !state.canAssociateToCollections + !state.canAssociateToCollections }, OverflowMenuItemData( text = stringResource(id = R.string.delete), @@ -324,7 +323,7 @@ fun VaultAddEditScreen( }, ) }, - ) { innerPadding -> + ) { when (val viewState = state.viewState) { is VaultAddEditState.ViewState.Content -> { VaultAddEditContent( @@ -342,7 +341,6 @@ fun VaultAddEditScreen( sshKeyItemTypeHandlers = sshKeyItemTypeHandlers, modifier = Modifier .imePadding() - .padding(innerPadding) .fillMaxSize(), ) } @@ -350,17 +348,13 @@ fun VaultAddEditScreen( is VaultAddEditState.ViewState.Error -> { BitwardenErrorContent( message = viewState.message(), - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } VaultAddEditState.ViewState.Loading -> { BitwardenLoadingContent( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index 240d052ec..1a08cce10 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -16,12 +16,10 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager -import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull @@ -55,6 +53,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState +import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections +import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType @@ -103,12 +103,13 @@ class VaultAddEditViewModel @Inject constructor( private val resourceManager: ResourceManager, private val clock: Clock, private val organizationEventManager: OrganizationEventManager, - private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { val vaultAddEditType = VaultAddEditArgs(savedStateHandle).vaultAddEditType + val selectedFolderId = VaultAddEditArgs(savedStateHandle).selectedFolderId + val selectedCollectionId = VaultAddEditArgs(savedStateHandle).selectedCollectionId val isIndividualVaultDisabled = policyManager .getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP) .any() @@ -149,11 +150,12 @@ class VaultAddEditViewModel @Inject constructor( attestationOptions = fido2AttestationOptions, isIndividualVaultDisabled = isIndividualVaultDisabled, ) - ?: totpData?.toDefaultAddTypeContent( - isIndividualVaultDisabled = isIndividualVaultDisabled, - ) + ?: totpData?.toDefaultAddTypeContent(isIndividualVaultDisabled) ?: VaultAddEditState.ViewState.Content( - common = VaultAddEditState.ViewState.Content.Common(), + common = VaultAddEditState.ViewState.Content.Common( + selectedFolderId = selectedFolderId, + selectedCollectionId = selectedCollectionId, + ), isIndividualVaultDisabled = isIndividualVaultDisabled, type = vaultAddEditType.vaultItemCipherType.toItemType(), ) @@ -167,11 +169,7 @@ class VaultAddEditViewModel @Inject constructor( // Set special conditions for autofill and fido2 save shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null, shouldExitOnSave = shouldExitOnSave, - supportedItemTypes = getSupportedItemTypeOptions( - isSshKeyVaultItemSupported = featureFlagManager.getFeatureFlag( - key = FlagKey.SshKeyCipherItems, - ), - ), + supportedItemTypes = getSupportedItemTypeOptions(), ) }, ) { @@ -213,11 +211,6 @@ class VaultAddEditViewModel @Inject constructor( } .onEach(::sendAction) .launchIn(viewModelScope) - - featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems) - .map { VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(it) } - .onEach(::sendAction) - .launchIn(viewModelScope) } override fun handleAction(action: VaultAddEditAction) { @@ -1444,10 +1437,6 @@ class VaultAddEditViewModel @Inject constructor( is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> { handleValidateFido2PinResultReceive(action) } - - is VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive -> { - handleSshKeyCipherItemsFeatureFlagReceive(action) - } } } @@ -1592,27 +1581,13 @@ class VaultAddEditViewModel @Inject constructor( vaultAddEditType = vaultAddEditType, ) { currentAccount, cipherView -> - // Deletion is not allowed when the item is in a collection that the user - // does not have "manage" permission for. - val canDelete = vaultData.collectionViewList - .none { - val isItemInCollection = cipherView - ?.collectionIds - ?.contains(it.id) == true + val canDelete = vaultData + .collectionViewList + .hasDeletePermissionInAtLeastOneCollection(cipherView?.collectionIds) - isItemInCollection && !it.manage - } - - // Assigning to a collection is not allowed when the item is in a collection - // that the user does not have "manage" and "edit" permission for. - val canAssignToCollections = vaultData.collectionViewList - .none { - val isItemInCollection = cipherView - ?.collectionIds - ?.contains(it.id) == true - - isItemInCollection && (!it.manage || it.readOnly) - } + val canAssignToCollections = vaultData + .collectionViewList + .canAssignToCollections(cipherView?.collectionIds) // Derive the view state from the current Cipher for Edit mode // or use the current state for Add @@ -1782,19 +1757,6 @@ class VaultAddEditViewModel @Inject constructor( getRequestAndRegisterCredential() } - - private fun handleSshKeyCipherItemsFeatureFlagReceive( - action: VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive, - ) { - mutableStateFlow.update { - it.copy( - supportedItemTypes = getSupportedItemTypeOptions( - isSshKeyVaultItemSupported = action.enabled, - ), - ) - } - } - //endregion Internal Type Handlers //region Utility Functions @@ -2139,6 +2101,7 @@ data class VaultAddEditState( val favorite: Boolean = false, val customFieldData: List = emptyList(), val notes: String = "", + val selectedCollectionId: String? = null, val selectedFolderId: String? = null, val availableFolders: List = emptyList(), val selectedOwnerId: String? = null, @@ -3109,13 +3072,6 @@ sealed class VaultAddEditAction { val generatorResult: GeneratorResult, ) : Internal() - /** - * Indicates that the the SSH key vault item feature flag state has been received. - */ - data class SshKeyCipherItemsFeatureFlagReceive( - val enabled: Boolean, - ) : Internal() - /** * Indicates that the vault item data has been received. */ @@ -3170,7 +3126,10 @@ sealed class VaultAddEditAction { } } -private fun getSupportedItemTypeOptions( - isSshKeyVaultItemSupported: Boolean, -) = VaultAddEditState.ItemTypeOption.entries - .filter { isSshKeyVaultItemSupported || it != VaultAddEditState.ItemTypeOption.SSH_KEYS } +/** + * Returns a list of item type options that can be selected during item creation. + * + * TODO: [PM-10413] Allow SSH key creation when the SDK supports it. + */ +private fun getSupportedItemTypeOptions() = VaultAddEditState.ItemTypeOption.entries + .filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt index 11f6fe81f..805dda6e1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensions.kt @@ -131,17 +131,21 @@ fun VaultAddEditState.ViewState.appendFolderAndOwnerData( common = currentContentState.common.copy( selectedFolderId = folderViewList.toSelectedFolderId( cipherView = currentContentState.common.originalCipher, - ), + ) + ?: currentContentState.common.selectedFolderId, availableFolders = folderViewList.toAvailableFolders( resourceManager = resourceManager, ), - selectedOwnerId = activeAccount.toSelectedOwnerId( - cipherView = currentContentState.common.originalCipher, - ), + selectedOwnerId = activeAccount + .toSelectedOwnerId(cipherView = currentContentState.common.originalCipher) + ?: collectionViewList + .firstOrNull { it.id == currentContentState.common.selectedCollectionId } + ?.organizationId, availableOwners = activeAccount.toAvailableOwners( collectionViewList = collectionViewList, cipherView = currentContentState.common.originalCipher, isIndividualVaultDisabled = isIndividualVaultDisabled, + selectedCollectionId = currentContentState.common.selectedCollectionId, ), isUnlockWithPasswordEnabled = activeAccount.hasMasterPassword, hasOrganizations = activeAccount.organizations.isNotEmpty(), @@ -196,13 +200,15 @@ private fun UserState.Account.toAvailableOwners( collectionViewList: List, cipherView: CipherView?, isIndividualVaultDisabled: Boolean, + selectedCollectionId: String? = null, ): List = listOfNotNull( - VaultAddEditState.Owner( - name = email, - id = null, - collections = emptyList(), - ) + VaultAddEditState + .Owner( + name = email, + id = null, + collections = emptyList(), + ) .takeUnless { isIndividualVaultDisabled }, *organizations .map { @@ -218,9 +224,11 @@ private fun UserState.Account.toAvailableOwners( VaultCollection( id = collection.id.orEmpty(), name = collection.name, - isSelected = cipherView + isSelected = (cipherView ?.collectionIds - ?.contains(collection.id) == true, + ?.contains(collection.id)) + ?: (selectedCollectionId != null && + collection.id == selectedCollectionId), ) }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt index c530c23b6..bd5577c51 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.vault.feature.attachments import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -98,24 +97,21 @@ fun AttachmentsScreen( }, ) }, - ) { innerPadding -> - val modifier = Modifier - .fillMaxSize() - .padding(innerPadding) + ) { when (val viewState = state.viewState) { is AttachmentsState.ViewState.Content -> AttachmentsContent( viewState = viewState, attachmentsHandlers = attachmentsHandlers, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) is AttachmentsState.ViewState.Error -> BitwardenErrorContent( message = viewState.message(), - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) AttachmentsState.ViewState.Loading -> BitwardenLoadingContent( - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt index 2f8ed1e3b..9750c2460 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt @@ -104,10 +104,9 @@ fun ImportLoginsScreen( onDismiss = handler.onSuccessfulSyncAcknowledged, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), modifier = Modifier.statusBarsPadding(), - ) { paddingValues, animatedOnDismiss -> + ) { animatedOnDismiss -> ImportLoginsSuccessBottomSheetContent( onCompleteImportLogins = animatedOnDismiss, - modifier = Modifier.padding(paddingValues), ) } @@ -132,13 +131,11 @@ fun ImportLoginsScreen( scrollBehavior = scrollBehavior, ) }, - ) { innerPadding -> + ) { Crossfade( targetState = state.viewState, label = "CrossfadeBetweenViewStates", - modifier = Modifier - .fillMaxSize() - .padding(paddingValues = innerPadding), + modifier = Modifier.fillMaxSize(), ) { viewState -> when (viewState) { ImportLoginsState.ViewState.InitialContent -> { @@ -527,6 +524,8 @@ private fun ImportLoginsSuccessBottomSheetContent( iconVectorResource = R.drawable.ic_shield, ), ), + bottomDividerPaddingStart = 48.dp, + showBottomDivider = true, modifier = Modifier.standardHorizontalMargin(), ) { contentData -> BitwardenContentBlock( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt index ac60bd3e3..f49d9517b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text @@ -68,8 +67,6 @@ fun ImportLoginsInstructionStep( ) { instructionStep -> InstructionRowItem( instructionStep = instructionStep, - modifier = modifier - .padding(all = 12.dp), ) } Spacer(Modifier.height(24.dp)) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt index 29d25f365..fd2a182d7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemCardContent.kt @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordFieldWithActions import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers @@ -173,14 +174,22 @@ fun VaultItemCardContent( .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + BitwardenTextFieldWithActions( label = stringResource(id = R.string.notes), value = notes, onValueChange = { }, readOnly = true, singleLine = false, + actions = { + BitwardenTonalIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", modifier = Modifier - .testTag("CipherNotesLabel") .fillMaxWidth() .padding(horizontal = 16.dp), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt index 6606a4bae..3e82215ff 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemIdentityContent.kt @@ -13,19 +13,23 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers /** * The top level content UI state for the [VaultItemScreen] when viewing a Identity cipher. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "MaxLineLength") @Composable fun VaultItemIdentityContent( identityState: VaultItemState.ViewState.Content.ItemType.Identity, commonState: VaultItemState.ViewState.Content.Common, vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, + vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers, modifier: Modifier = Modifier, ) { LazyColumn(modifier = modifier) { @@ -54,14 +58,14 @@ fun VaultItemIdentityContent( identityState.identityName?.let { identityName -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.identity_name), value = identityName, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_identity_name), + textFieldTestTag = "IdentityNameEntry", + copyActionTestTag = "IdentityCopyNameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyIdentityNameClick, modifier = Modifier - .testTag("IdentityNameEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -70,14 +74,14 @@ fun VaultItemIdentityContent( identityState.username?.let { username -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.username), value = username, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_username), + textFieldTestTag = "IdentityUsernameEntry", + copyActionTestTag = "IdentityCopyUsernameButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyUsernameClick, modifier = Modifier - .testTag("IdentityUsernameEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -86,14 +90,14 @@ fun VaultItemIdentityContent( identityState.company?.let { company -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.company), value = company, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_company), + textFieldTestTag = "IdentityCompanyEntry", + copyActionTestTag = "IdentityCopyCompanyButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyCompanyClick, modifier = Modifier - .testTag("IdentityCompanyEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -102,14 +106,14 @@ fun VaultItemIdentityContent( identityState.ssn?.let { ssn -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.ssn), value = ssn, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_ssn), + textFieldTestTag = "IdentitySsnEntry", + copyActionTestTag = "IdentityCopySsnButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopySsnClick, modifier = Modifier - .testTag("IdentitySsnEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -118,14 +122,14 @@ fun VaultItemIdentityContent( identityState.passportNumber?.let { passportNumber -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.passport_number), value = passportNumber, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_passport_number), + textFieldTestTag = "IdentityPassportNumberEntry", + copyActionTestTag = "IdentityCopyPassportNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPassportNumberClick, modifier = Modifier - .testTag("IdentityPassportNumberEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -134,14 +138,14 @@ fun VaultItemIdentityContent( identityState.licenseNumber?.let { licenseNumber -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.license_number), value = licenseNumber, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_license_number), + textFieldTestTag = "IdentityLicenseNumberEntry", + copyActionTestTag = "IdentityCopyLicenseNumberButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyLicenseNumberClick, modifier = Modifier - .testTag("IdentityLicenseNumberEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -150,14 +154,14 @@ fun VaultItemIdentityContent( identityState.email?.let { email -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.email), value = email, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_email), + textFieldTestTag = "IdentityEmailEntry", + copyActionTestTag = "IdentityCopyEmailButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyEmailClick, modifier = Modifier - .testTag("IdentityEmailEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -166,14 +170,14 @@ fun VaultItemIdentityContent( identityState.phone?.let { phone -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.phone), value = phone, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_phone), + textFieldTestTag = "IdentityPhoneEntry", + copyActionTestTag = "IdentityCopyPhoneButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyPhoneClick, modifier = Modifier - .testTag("IdentityPhoneEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -182,20 +186,19 @@ fun VaultItemIdentityContent( identityState.address?.let { address -> item { Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.address), value = address, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_address), + textFieldTestTag = "IdentityAddressEntry", + copyActionTestTag = "IdentityCopyAddressButton", + onCopyClick = vaultIdentityItemTypeHandlers.onCopyAddressClick, modifier = Modifier - .testTag("IdentityAddressEntry") .fillMaxWidth() .padding(horizontal = 16.dp), ) } } - commonState.notes?.let { notes -> item { Spacer(modifier = Modifier.height(4.dp)) @@ -206,14 +209,14 @@ fun VaultItemIdentityContent( .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + IdentityCopyField( label = stringResource(id = R.string.notes), value = notes, - onValueChange = { }, - readOnly = true, - singleLine = false, + copyContentDescription = stringResource(id = R.string.copy_notes), + textFieldTestTag = "CipherNotesLabel", + copyActionTestTag = "CipherNotesCopyButton", + onCopyClick = vaultCommonItemTypeHandlers.onCopyNotesClick, modifier = Modifier - .testTag("CipherNotesLabel") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -284,3 +287,32 @@ fun VaultItemIdentityContent( } } } + +@Composable +private fun IdentityCopyField( + label: String, + value: String, + copyContentDescription: String, + textFieldTestTag: String, + copyActionTestTag: String, + onCopyClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BitwardenTextFieldWithActions( + label = label, + value = value, + onValueChange = { }, + readOnly = true, + singleLine = false, + actions = { + BitwardenTonalIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = copyContentDescription, + onClick = onCopyClick, + modifier = Modifier.testTag(tag = copyActionTestTag), + ) + }, + modifier = modifier, + textFieldTestTag = textFieldTestTag, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 308741fb3..fce007507 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -158,8 +158,8 @@ fun VaultItemLoginContent( Spacer(modifier = Modifier.height(8.dp)) NotesField( notes = notes, + onCopyAction = vaultCommonItemTypeHandlers.onCopyNotesClick, modifier = Modifier - .testTag("CipherNotesLabel") .fillMaxWidth() .padding(horizontal = 16.dp), ) @@ -273,14 +273,24 @@ private fun Fido2CredentialField( @Composable private fun NotesField( notes: String, + onCopyAction: () -> Unit, modifier: Modifier = Modifier, ) { - BitwardenTextField( + BitwardenTextFieldWithActions( label = stringResource(id = R.string.notes), value = notes, onValueChange = { }, readOnly = true, singleLine = false, + actions = { + BitwardenTonalIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = onCopyAction, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt index dc46ed4d6..268ac415a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreen.kt @@ -44,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers +import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers @@ -254,13 +255,12 @@ fun VaultItemScreen( ) } }, - ) { innerPadding -> + ) { VaultItemContent( viewState = state.viewState, modifier = Modifier .imePadding() - .fillMaxSize() - .padding(innerPadding), + .fillMaxSize(), vaultCommonItemTypeHandlers = remember(viewModel) { VaultCommonItemTypeHandlers.create(viewModel = viewModel) }, @@ -273,6 +273,9 @@ fun VaultItemScreen( vaultSshKeyItemTypeHandlers = remember(viewModel) { VaultSshKeyItemTypeHandlers.create(viewModel = viewModel) }, + vaultIdentityItemTypeHandlers = remember(viewModel) { + VaultIdentityItemTypeHandlers.create(viewModel = viewModel) + }, ) } } @@ -351,6 +354,7 @@ private fun VaultItemContent( vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers, vaultCardItemTypeHandlers: VaultCardItemTypeHandlers, vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers, + vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers, modifier: Modifier = Modifier, ) { when (viewState) { @@ -387,6 +391,7 @@ private fun VaultItemContent( commonState = viewState.common, identityState = viewState.type, vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers, + vaultIdentityItemTypeHandlers = vaultIdentityItemTypeHandlers, modifier = modifier, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt index 7fbbdfc64..9d68e3dc5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemSecureNoteContent.kt @@ -16,7 +16,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTonalIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithActions import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers @@ -66,14 +68,22 @@ fun VaultItemSecureNoteContent( .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( + BitwardenTextFieldWithActions( label = stringResource(id = R.string.notes), value = notes, onValueChange = { }, readOnly = true, singleLine = false, + actions = { + BitwardenTonalIconButton( + vectorIconRes = R.drawable.ic_copy, + contentDescription = stringResource(id = R.string.copy_notes), + onClick = vaultCommonItemTypeHandlers.onCopyNotesClick, + modifier = Modifier.testTag(tag = "CipherNotesCopyButton"), + ) + }, + textFieldTestTag = "CipherNotesLabel", modifier = Modifier - .testTag("CipherNotesLabel") .fillMaxWidth() .padding(horizontal = 16.dp), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 250f2562e..021d88624 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -28,6 +28,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState +import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections +import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType import dagger.hilt.android.lifecycle.HiltViewModel @@ -103,29 +105,15 @@ class VaultItemViewModel @Inject constructor( // we map it to the appropriate value below. } .mapNullable { - // Deletion is not allowed when the item is in a collection that the user - // does not have "manage" permission for. - val canDelete = collectionsState.data - ?.none { - val itemIsInCollection = cipherViewState.data - ?.collectionIds - ?.contains(it.id) == true + val canDelete = collectionsState + .data + .hasDeletePermissionInAtLeastOneCollection( + collectionIds = cipherViewState.data?.collectionIds, + ) - itemIsInCollection && !it.manage - } - ?: true - - // Assigning to a collection is not allowed when the item is in a collection - // that the user does not have "manage" and "edit" permission for. - val canAssignToCollections = collectionsState.data - ?.none { - val itemIsInCollection = cipherViewState.data - ?.collectionIds - ?.contains(it.id) == true - - itemIsInCollection && !it.manage && it.readOnly - } - ?: true + val canAssignToCollections = collectionsState + .data + .canAssignToCollections(cipherViewState.data?.collectionIds) VaultItemStateData( cipher = cipherViewState.data, @@ -145,6 +133,7 @@ class VaultItemViewModel @Inject constructor( is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action) is VaultItemAction.ItemType.Card -> handleCardTypeActions(action) is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action) + is VaultItemAction.ItemType.Identity -> handleIdentityTypeActions(action) is VaultItemAction.Common -> handleCommonActions(action) is VaultItemAction.Internal -> handleInternalAction(action) } @@ -196,6 +185,7 @@ class VaultItemViewModel @Inject constructor( } is VaultItemAction.Common.RestoreVaultItemClick -> handleRestoreItemClicked() + is VaultItemAction.Common.CopyNotesClick -> handleCopyNotesClick() } } @@ -520,6 +510,13 @@ class VaultItemViewModel @Inject constructor( } } + private fun handleCopyNotesClick() { + onContent { content -> + val notes = content.common.notes.orEmpty() + clipboardManager.setText(text = notes) + } + } + //endregion Common Handlers //region Login Type Handlers @@ -824,6 +821,99 @@ class VaultItemViewModel @Inject constructor( //endregion SSH Key Type Handlers + //region Identity Type Handlers + + private fun handleIdentityTypeActions(action: VaultItemAction.ItemType.Identity) { + when (action) { + VaultItemAction.ItemType.Identity.CopyIdentityNameClick -> { + handleCopyIdentityNameClick() + } + + VaultItemAction.ItemType.Identity.CopyUsernameClick -> { + handleCopyIdentityUsernameClick() + } + + VaultItemAction.ItemType.Identity.CopyCompanyClick -> handleCopyCompanyClick() + VaultItemAction.ItemType.Identity.CopySsnClick -> handleCopySsnClick() + VaultItemAction.ItemType.Identity.CopyPassportNumberClick -> { + handleCopyPassportNumberClick() + } + + VaultItemAction.ItemType.Identity.CopyLicenseNumberClick -> { + handleCopyLicenseNumberClick() + } + + VaultItemAction.ItemType.Identity.CopyEmailClick -> handleCopyEmailClick() + VaultItemAction.ItemType.Identity.CopyPhoneClick -> handleCopyPhoneClick() + VaultItemAction.ItemType.Identity.CopyAddressClick -> handleCopyAddressClick() + } + } + + private fun handleCopyIdentityNameClick() { + onIdentityContent { _, identity -> + val identityName = identity.identityName.orEmpty() + clipboardManager.setText(text = identityName) + } + } + + private fun handleCopyIdentityUsernameClick() { + onIdentityContent { _, identity -> + val username = identity.username.orEmpty() + clipboardManager.setText(text = username) + } + } + + private fun handleCopyCompanyClick() { + onIdentityContent { _, identity -> + val company = identity.company.orEmpty() + clipboardManager.setText(text = company) + } + } + + private fun handleCopySsnClick() { + onIdentityContent { _, identity -> + val ssn = identity.ssn.orEmpty() + clipboardManager.setText(text = ssn) + } + } + + private fun handleCopyPassportNumberClick() { + onIdentityContent { _, identity -> + val passportNumber = identity.passportNumber.orEmpty() + clipboardManager.setText(text = passportNumber) + } + } + + private fun handleCopyLicenseNumberClick() { + onIdentityContent { _, identity -> + val licenseNumber = identity.licenseNumber.orEmpty() + clipboardManager.setText(text = licenseNumber) + } + } + + private fun handleCopyEmailClick() { + onIdentityContent { _, identity -> + val email = identity.email.orEmpty() + clipboardManager.setText(text = email) + } + } + + private fun handleCopyPhoneClick() { + onIdentityContent { _, identity -> + val phone = identity.phone.orEmpty() + clipboardManager.setText(text = phone) + } + } + + private fun handleCopyAddressClick() { + onIdentityContent { _, identity -> + val address = identity.address.orEmpty() + clipboardManager.setText(text = address) + } + } + + //endregion Identity Type Handlers + //region Internal Type Handlers private fun handleInternalAction(action: VaultItemAction.Internal) { @@ -1145,6 +1235,21 @@ class VaultItemViewModel @Inject constructor( } } } + + private inline fun onIdentityContent( + crossinline block: ( + VaultItemState.ViewState.Content, + VaultItemState.ViewState.Content.ItemType.Identity, + ) -> Unit, + ) { + state.viewState.asContentOrNull() + ?.let { content -> + (content.type as? VaultItemState.ViewState.Content.ItemType.Identity) + ?.let { identityContent -> + block(content, identityContent) + } + } + } } /** @@ -1736,6 +1841,11 @@ sealed class VaultItemAction { * The user confirmed cloning a cipher without its FIDO 2 credentials. */ data object ConfirmCloneWithoutFido2CredentialClick : Common() + + /** + * The user has clicked the copy button for notes text field. + */ + data object CopyNotesClick : Common() } /** @@ -1839,6 +1949,56 @@ sealed class VaultItemAction { */ data object CopyFingerprintClick : SshKey() } + + /** + * Represents actions specific to the Identity type. + */ + sealed class Identity : VaultItemAction() { + /** + * The user has clicked the copy button for the identity name. + */ + data object CopyIdentityNameClick : Identity() + + /** + * The user has clicked the copy button for the username. + */ + data object CopyUsernameClick : Identity() + + /** + * The user has clicked the copy button for the company. + */ + data object CopyCompanyClick : Identity() + + /** + * The user has clicked the copy button for the SSN. + */ + data object CopySsnClick : Identity() + + /** + * The user has clicked the copy button for the passport number. + */ + data object CopyPassportNumberClick : Identity() + + /** + * The user has clicked the copy button for the license number. + */ + data object CopyLicenseNumberClick : Identity() + + /** + * The user has clicked the copy button for the email. + */ + data object CopyEmailClick : Identity() + + /** + * The user has clicked the copy button for the phone number. + */ + data object CopyPhoneClick : Identity() + + /** + * The user has clicked the copy button for the address. + */ + data object CopyAddressClick : Identity() + } } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt index ec011346e..7dfa682ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultCommonItemTypeHandlers.kt @@ -21,6 +21,7 @@ data class VaultCommonItemTypeHandlers( Boolean, ) -> Unit, val onAttachmentDownloadClick: (VaultItemState.ViewState.Content.Common.AttachmentItem) -> Unit, + val onCopyNotesClick: () -> Unit, ) { @Suppress("UndocumentedPublicClass") companion object { @@ -52,6 +53,9 @@ data class VaultCommonItemTypeHandlers( onAttachmentDownloadClick = { viewModel.trySendAction(VaultItemAction.Common.AttachmentDownloadClick(it)) }, + onCopyNotesClick = { + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + }, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt new file mode 100644 index 000000000..d44ac6066 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/handlers/VaultIdentityItemTypeHandlers.kt @@ -0,0 +1,59 @@ +package com.x8bit.bitwarden.ui.vault.feature.item.handlers + +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction +import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel + +/** + * A collection of handler functions for managing actions within the context of viewing identity + * items in a vault. + */ +data class VaultIdentityItemTypeHandlers( + val onCopyIdentityNameClick: () -> Unit, + val onCopyUsernameClick: () -> Unit, + val onCopyCompanyClick: () -> Unit, + val onCopySsnClick: () -> Unit, + val onCopyPassportNumberClick: () -> Unit, + val onCopyLicenseNumberClick: () -> Unit, + val onCopyEmailClick: () -> Unit, + val onCopyPhoneClick: () -> Unit, + val onCopyAddressClick: () -> Unit, +) { + @Suppress("UndocumentedPublicClass", "MaxLineLength") + companion object { + /** + * Creates the [VaultIdentityItemTypeHandlers] using the [viewModel] to send desired actions. + */ + fun create( + viewModel: VaultItemViewModel, + ): VaultIdentityItemTypeHandlers = + VaultIdentityItemTypeHandlers( + onCopyIdentityNameClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick) + }, + onCopyUsernameClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick) + }, + onCopyCompanyClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick) + }, + onCopySsnClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick) + }, + onCopyPassportNumberClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick) + }, + onCopyLicenseNumberClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick) + }, + onCopyEmailClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick) + }, + onCopyPhoneClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick) + }, + onCopyAddressClick = { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick) + }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt index b076c532a..e447270eb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt @@ -64,7 +64,11 @@ fun NavGraphBuilder.vaultItemListingDestination( onNavigateToVaultItemScreen: (id: String) -> Unit, onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit, onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit, - onNavigateToVaultAddItemScreen: (vaultItemCipherType: VaultItemCipherType) -> Unit, + onNavigateToVaultAddItemScreen: ( + cipherType: VaultItemCipherType, + selectedFolderId: String?, + selectedCollectionId: String?, + ) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, ) { internalVaultItemListingDestination( @@ -87,7 +91,11 @@ fun NavGraphBuilder.vaultItemListingDestinationAsRoot( onNavigateBack: () -> Unit, onNavigateToVaultItemScreen: (id: String) -> Unit, onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit, - onNavigateToVaultAddItemScreen: (VaultItemCipherType) -> Unit, + onNavigateToVaultAddItemScreen: ( + cipherType: VaultItemCipherType, + selectedFolderId: String?, + selectedCollectionId: String?, + ) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, ) { composableWithStayTransitions( @@ -128,7 +136,7 @@ fun NavGraphBuilder.sendItemListingDestination( onNavigateBack = onNavigateBack, onNavigateToAddSendItem = onNavigateToAddSendItem, onNavigateToEditSendItem = onNavigateToEditSendItem, - onNavigateToVaultAddItemScreen = { }, + onNavigateToVaultAddItemScreen = { _, _, _ -> }, onNavigateToVaultItemScreen = { }, onNavigateToVaultEditItemScreen = { }, onNavigateToVaultItemListing = { }, @@ -146,7 +154,11 @@ private fun NavGraphBuilder.internalVaultItemListingDestination( onNavigateToVaultItemScreen: (id: String) -> Unit, onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit, onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit, - onNavigateToVaultAddItemScreen: (vaultItemCipherType: VaultItemCipherType) -> Unit, + onNavigateToVaultAddItemScreen: ( + cipherType: VaultItemCipherType, + selectedFolderId: String?, + selectedCollectionId: String?, + ) -> Unit, onNavigateToAddSendItem: () -> Unit, onNavigateToEditSendItem: (sendId: String) -> Unit, onNavigateToSearch: (searchType: SearchType) -> Unit, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index 3285eb877..5a8360bf1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -72,7 +71,11 @@ fun VaultItemListingScreen( onNavigateToVaultItem: (id: String) -> Unit, onNavigateToVaultEditItemScreen: (cipherVaultId: String) -> Unit, onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit, - onNavigateToVaultAddItemScreen: (vaultItemCipherType: VaultItemCipherType) -> Unit, + onNavigateToVaultAddItemScreen: ( + vaultItemCipherType: VaultItemCipherType, + selectedFolderId: String?, + selectedCollectionId: String?, + ) -> Unit, onNavigateToAddSendItem: () -> Unit, onNavigateToEditSendItem: (sendId: String) -> Unit, onNavigateToSearch: (searchType: SearchType) -> Unit, @@ -113,7 +116,11 @@ fun VaultItemListingScreen( } is VaultItemListingEvent.NavigateToAddVaultItem -> { - onNavigateToVaultAddItemScreen(event.vaultItemCipherType) + onNavigateToVaultAddItemScreen( + event.vaultItemCipherType, + event.selectedFolderId, + event.selectedCollectionId, + ) } is VaultItemListingEvent.NavigateToEditCipher -> { @@ -441,7 +448,7 @@ private fun VaultItemListingScaffold( ) }, floatingActionButton = { - if (state.itemListingType.hasFab) { + if (state.hasAddItemFabButton) { BitwardenFloatingActionButton( onClick = vaultItemListingHandlers.addVaultItemClick, painter = rememberVectorPainter(id = R.drawable.ic_plus_large), @@ -450,12 +457,24 @@ private fun VaultItemListingScaffold( ) } }, + overlay = { + BitwardenAccountSwitcher( + isVisible = isAccountMenuVisible, + accountSummaries = state.accountSummaries.toImmutableList(), + onSwitchAccountClick = vaultItemListingHandlers.switchAccountClick, + onLockAccountClick = vaultItemListingHandlers.lockAccountClick, + onLogoutAccountClick = vaultItemListingHandlers.logoutAccountClick, + onAddAccountClick = { + // Not available + }, + onDismissRequest = { isAccountMenuVisible = false }, + isAddAccountAvailable = false, + topAppBarScrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxSize(), + ) + }, pullToRefreshState = pullToRefreshState, - ) { paddingValues -> - val modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - + ) { when (state.viewState) { is VaultItemListingState.ViewState.Content -> { VaultItemListingContent( @@ -469,7 +488,7 @@ private fun VaultItemListingScaffold( masterPasswordRepromptSubmit = vaultItemListingHandlers.masterPasswordRepromptSubmit, onOverflowItemClick = vaultItemListingHandlers.overflowItemClick, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } @@ -479,7 +498,7 @@ private fun VaultItemListingScaffold( policyDisablesSend = state.policyDisablesSend && state.itemListingType is VaultItemListingState.ItemListingType.Send, addItemClickAction = vaultItemListingHandlers.addVaultItemClick, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } @@ -487,28 +506,13 @@ private fun VaultItemListingScaffold( BitwardenErrorContent( message = state.viewState.message(), onTryAgainClick = vaultItemListingHandlers.refreshClick, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } is VaultItemListingState.ViewState.Loading -> { - BitwardenLoadingContent(modifier = modifier) + BitwardenLoadingContent(modifier = Modifier.fillMaxSize()) } } - - BitwardenAccountSwitcher( - isVisible = isAccountMenuVisible, - accountSummaries = state.accountSummaries.toImmutableList(), - onSwitchAccountClick = vaultItemListingHandlers.switchAccountClick, - onLockAccountClick = vaultItemListingHandlers.lockAccountClick, - onLogoutAccountClick = vaultItemListingHandlers.logoutAccountClick, - onAddAccountClick = { - // Not available - }, - onDismissRequest = { isAccountMenuVisible = false }, - isAddAccountAvailable = false, - topAppBarScrollBehavior = scrollBehavior, - modifier = modifier, - ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index e1e8084ef..57d289093 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -534,6 +534,20 @@ class VaultItemListingViewModel @Inject constructor( private fun handleAddVaultItemClick() { val event = when (val itemListingType = state.itemListingType) { + is VaultItemListingState.ItemListingType.Vault.Folder -> { + VaultItemListingEvent.NavigateToAddVaultItem( + vaultItemCipherType = itemListingType.toVaultItemCipherType(), + selectedFolderId = itemListingType.folderId, + ) + } + + is VaultItemListingState.ItemListingType.Vault.Collection -> { + VaultItemListingEvent.NavigateToAddVaultItem( + vaultItemCipherType = itemListingType.toVaultItemCipherType(), + selectedCollectionId = itemListingType.collectionId, + ) + } + is VaultItemListingState.ItemListingType.Vault -> { VaultItemListingEvent.NavigateToAddVaultItem( vaultItemCipherType = itemListingType.toVaultItemCipherType(), @@ -843,7 +857,7 @@ class VaultItemListingViewModel @Inject constructor( private fun handleBackClick() { sendEvent( - event = if (state.isTotp) { + event = if (state.isTotp || state.isAutofill) { VaultItemListingEvent.ExitApp } else { VaultItemListingEvent.NavigateBack @@ -1732,6 +1746,13 @@ data class VaultItemListingState( val isPremium: Boolean, val isRefreshing: Boolean, ) { + /** + * Whether or not the add FAB should be shown. + */ + val hasAddItemFabButton: Boolean + get() = itemListingType.hasFab && + !(viewState is ViewState.NoItems && viewState.shouldShowAddButton) + /** * Whether or not this represents a listing screen for autofill. */ @@ -2089,7 +2110,7 @@ data class VaultItemListingState( get() = folderId ?.let { folderName.asText() } ?: R.string.folder_none.asText() - override val hasFab: Boolean get() = false + override val hasFab: Boolean get() = true } /** @@ -2104,7 +2125,7 @@ data class VaultItemListingState( val collectionName: String = "", ) : Vault() { override val titleText: Text get() = collectionName.asText() - override val hasFab: Boolean get() = false + override val hasFab: Boolean get() = true } } @@ -2150,6 +2171,8 @@ sealed class VaultItemListingEvent { */ data class NavigateToAddVaultItem( val vaultItemCipherType: VaultItemCipherType, + val selectedFolderId: String? = null, + val selectedCollectionId: String? = null, ) : VaultItemListingEvent() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt index 87814f044..02fe69bb8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensions.kt @@ -40,8 +40,8 @@ fun VaultItemListingState.ItemListingType.Vault.toVaultItemCipherType(): VaultIt is VaultItemListingState.ItemListingType.Vault.SshKey -> VaultItemCipherType.SSH_KEY is VaultItemListingState.ItemListingType.Vault.Login -> VaultItemCipherType.LOGIN is VaultItemListingState.ItemListingType.Vault.Collection -> VaultItemCipherType.LOGIN + is VaultItemListingState.ItemListingType.Vault.Folder -> VaultItemCipherType.LOGIN is VaultItemListingState.ItemListingType.Vault.Trash, - is VaultItemListingState.ItemListingType.Vault.Folder, -> { throw IllegalStateException( "Cannot create vault item from this VaultItemListingState!", diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryScreen.kt index 9fff98335..4ac5a552d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryScreen.kt @@ -121,9 +121,8 @@ fun ManualCodeEntryScreen( scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), ) }, - ) { paddingValues -> - Column(modifier = Modifier.padding(paddingValues)) { - + ) { + Column { Text( text = stringResource(id = R.string.enter_key_manually), style = BitwardenTheme.typography.titleMedium, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModel.kt index 172afcc9a..fccbcbbc9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModel.kt @@ -46,7 +46,7 @@ class ManualCodeEntryViewModel @Inject constructor( } private fun handleCodeSubmit() { - vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(state.code)) + vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(state.code.trim())) sendEvent(ManualCodeEntryEvent.NavigateBack) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt index f64da9f80..f8e4c84f9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationScreen.kt @@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -126,12 +125,10 @@ private fun VaultMoveToOrganizationScaffold( }, ) }, - ) { innerPadding -> + ) { val modifier = Modifier .imePadding() .fillMaxSize() - .padding(innerPadding) - when (state.viewState) { is VaultMoveToOrganizationState.ViewState.Content -> { VaultMoveToOrganizationContent( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt index 2e14935b9..204f19d8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanScreen.kt @@ -125,24 +125,21 @@ fun QrCodeScanScreen( ), ) }, - ) { innerPadding -> + ) { CameraPreview( cameraErrorReceive = remember(viewModel) { { viewModel.trySendAction(QrCodeScanAction.CameraSetupErrorReceive) } }, qrCodeAnalyzer = qrCodeAnalyzer, - modifier = Modifier.padding(innerPadding), ) if (LocalConfiguration.current.isPortrait) { PortraitQRCodeContent( onEnterCodeManuallyClick = onEnterCodeManuallyClick, - modifier = Modifier.padding(innerPadding), ) } else { LandscapeQRCodeContent( onEnterCodeManuallyClick = onEnterCodeManuallyClick, - modifier = Modifier.padding(innerPadding), ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt index 67870e68e..b75b186be 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensions.kt @@ -88,3 +88,37 @@ fun String.toCollectionDisplayName(list: List): String { return collectionName } + +/** + * Checks if the user has delete permission in at least one collection. + * + * Deletion is allowed when the item is in any collection that the user has "manage" permission for. + */ +fun List?.hasDeletePermissionInAtLeastOneCollection( + collectionIds: List?, +): Boolean { + if (this.isNullOrEmpty() || collectionIds.isNullOrEmpty()) return true + return this + .any { collectionView -> + collectionIds + .contains(collectionView.id) + .let { isInCollection -> isInCollection && collectionView.manage } + } +} + +/** + * Checks if the user has permission to assign an item to a collection. + * + * Assigning to a collection is not allowed when the item is in a collection that the user does not + * have "manage" and "edit" permission for. + */ +fun List?.canAssignToCollections(currentCollectionIds: List?) = + this + ?.none { + val itemIsInCollection = currentCollectionIds + ?.contains(it.id) + ?: false + + itemIsInCollection && (!it.manage || it.readOnly) + } + ?: true diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt index 54f2859db..e08d409ae 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt @@ -22,6 +22,9 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList +private const val TOTP_TYPES_COUNT: Int = 1 +private const val TRASH_TYPES_COUNT: Int = 1 + /** * Content view for the [VaultScreen]. */ @@ -41,7 +44,7 @@ fun VaultContent( item { BitwardenListHeaderText( label = stringResource(id = R.string.totp), - supportingLabel = "1", + supportingLabel = TOTP_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), @@ -341,7 +344,7 @@ fun VaultContent( item { BitwardenListHeaderText( label = stringResource(id = R.string.trash), - supportingLabel = "1", + supportingLabel = TRASH_TYPES_COUNT.toString(), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultFilter.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultFilter.kt index 6753fcaae..4ff90c09b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultFilter.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultFilter.kt @@ -2,8 +2,15 @@ package com.x8bit.bitwarden.ui.vault.feature.vault import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior @@ -37,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList * @param topAppBarScrollBehavior Used to derive the background color of the content and keep it in * sync with the associated app bar. * @param modifier A [Modifier] for the composable. + * @param windowInsets The insets to be applied to this composable. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,6 +54,9 @@ fun VaultFilter( onVaultFilterTypeSelect: (VaultFilterType) -> Unit, topAppBarScrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier, + windowInsets: WindowInsets = WindowInsets.displayCutout + .union(WindowInsets.navigationBars) + .only(WindowInsetsSides.Horizontal), ) { var shouldShowSelectionDialog by remember { mutableStateOf(false) } @@ -73,7 +84,8 @@ fun VaultFilter( .scrolledContainerBottomDivider(topAppBarScrollBehavior = topAppBarScrollBehavior) .padding(vertical = 8.dp) .testTag("ActiveFilterRow") - .then(modifier), + .then(modifier) + .windowInsetsPadding(insets = windowInsets), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 8b4a052fa..40069a2f3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -20,7 +20,11 @@ const val VAULT_GRAPH_ROUTE: String = "vault_graph" @Suppress("LongParameterList") fun NavGraphBuilder.vaultGraph( navController: NavController, - onNavigateToVaultAddItemScreen: (vaultItemCipherType: VaultItemCipherType) -> Unit, + onNavigateToVaultAddItemScreen: ( + vaultItemCipherType: VaultItemCipherType, + selectedFolderId: String?, + selectedCollectionId: String?, + ) -> Unit, onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, @@ -33,7 +37,7 @@ fun NavGraphBuilder.vaultGraph( ) { vaultDestination( onNavigateToVaultAddItemScreen = { - onNavigateToVaultAddItemScreen(VaultItemCipherType.LOGIN) + onNavigateToVaultAddItemScreen(VaultItemCipherType.LOGIN, null, null) }, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 04f3c078a..fcdc65591 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -4,7 +4,6 @@ import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -240,6 +239,24 @@ private fun VaultScreenScaffold( }, ) }, + utilityBar = { + state.vaultFilterDataWithFilter?.let { + VaultFilter( + selectedVaultFilterType = it.selectedVaultFilterType, + vaultFilterTypes = it.vaultFilterTypes.toImmutableList(), + onVaultFilterTypeSelect = vaultHandlers.vaultFilterTypeSelect, + topAppBarScrollBehavior = scrollBehavior, + modifier = Modifier + .padding( + start = 16.dp, + // There is some built-in padding to the menu button that makes up + // the visual difference here. + end = 12.dp, + ) + .fillMaxWidth(), + ) + } + }, snackbarHost = { BitwardenSnackbarHost( bitwardenHostState = snackbarHostState, @@ -259,81 +276,7 @@ private fun VaultScreenScaffold( ) } }, - pullToRefreshState = pullToRefreshState, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { paddingValues -> - Box { - val innerModifier = Modifier - .fillMaxSize() - val outerModifier = Modifier - .fillMaxSize() - .padding(paddingValues) - Column(modifier = outerModifier) { - state.vaultFilterDataWithFilter?.let { - VaultFilter( - selectedVaultFilterType = it.selectedVaultFilterType, - vaultFilterTypes = it.vaultFilterTypes.toImmutableList(), - onVaultFilterTypeSelect = vaultHandlers.vaultFilterTypeSelect, - topAppBarScrollBehavior = scrollBehavior, - modifier = Modifier - .padding( - start = 16.dp, - // There is some built-in padding to the menu button that makes up - // the visual difference here. - end = 12.dp, - ) - .fillMaxWidth(), - ) - } - - when (val viewState = state.viewState) { - is VaultState.ViewState.Content -> VaultContent( - state = viewState, - showSshKeys = state.showSshKeys, - vaultHandlers = vaultHandlers, - onOverflowOptionClick = { masterPasswordRepromptAction = it }, - modifier = innerModifier, - ) - - is VaultState.ViewState.Loading -> BitwardenLoadingContent( - modifier = innerModifier, - ) - - is VaultState.ViewState.NoItems -> { - AnimatedVisibility( - visible = state.showImportActionCard, - exit = actionCardExitAnimation(), - label = "VaultNoItemsActionCard", - ) { - BitwardenActionCard( - cardTitle = stringResource(R.string.import_saved_logins), - cardSubtitle = stringResource( - R.string.use_a_computer_to_import_logins, - ), - actionText = stringResource(R.string.get_started), - onActionClick = vaultHandlers.importActionCardClick, - onDismissClick = vaultHandlers.dismissImportActionCard, - modifier = Modifier - .fillMaxWidth() - .standardHorizontalMargin() - .padding(top = 12.dp), - ) - } - VaultNoItems( - modifier = innerModifier, - policyDisablesSend = false, - addItemClickAction = vaultHandlers.addItemClickAction, - ) - } - - is VaultState.ViewState.Error -> BitwardenErrorContent( - message = viewState.message(), - onTryAgainClick = vaultHandlers.tryAgainClick, - modifier = innerModifier, - ) - } - } - + overlay = { BitwardenAccountSwitcher( isVisible = accountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), @@ -343,8 +286,61 @@ private fun VaultScreenScaffold( onAddAccountClick = vaultHandlers.addAccountClickAction, onDismissRequest = { updateAccountMenuVisibility(false) }, topAppBarScrollBehavior = scrollBehavior, - modifier = outerModifier, + modifier = Modifier.fillMaxSize(), ) + }, + pullToRefreshState = pullToRefreshState, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + when (val viewState = state.viewState) { + is VaultState.ViewState.Content -> VaultContent( + state = viewState, + showSshKeys = state.showSshKeys, + vaultHandlers = vaultHandlers, + onOverflowOptionClick = { masterPasswordRepromptAction = it }, + modifier = Modifier.fillMaxSize(), + ) + + is VaultState.ViewState.Loading -> BitwardenLoadingContent( + modifier = Modifier.fillMaxSize(), + ) + + is VaultState.ViewState.NoItems -> { + AnimatedVisibility( + visible = state.showImportActionCard, + exit = actionCardExitAnimation(), + label = "VaultNoItemsActionCard", + ) { + BitwardenActionCard( + cardTitle = stringResource(R.string.import_saved_logins), + cardSubtitle = stringResource( + R.string.use_a_computer_to_import_logins, + ), + actionText = stringResource(R.string.get_started), + onActionClick = vaultHandlers.importActionCardClick, + onDismissClick = vaultHandlers.dismissImportActionCard, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin() + .padding(top = 12.dp), + ) + } + VaultNoItems( + policyDisablesSend = false, + addItemClickAction = vaultHandlers.addItemClickAction, + modifier = Modifier.fillMaxSize(), + ) + } + + is VaultState.ViewState.Error -> BitwardenErrorContent( + message = viewState.message(), + onTryAgainClick = vaultHandlers.tryAgainClick, + modifier = Modifier.fillMaxSize(), + ) + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt index ee28a1d56..13d461bfb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt @@ -106,18 +106,14 @@ fun VerificationCodeScreen( ) }, pullToRefreshState = pullToRefreshState, - ) { paddingValues -> - val modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - + ) { when (val viewState = state.viewState) { is VerificationCodeState.ViewState.Content -> { VerificationCodeContent( items = viewState.verificationCodeDisplayItems.toImmutableList(), onCopyClick = verificationCodeHandler.copyClick, itemClick = verificationCodeHandler.itemClick, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } @@ -125,12 +121,12 @@ fun VerificationCodeScreen( BitwardenErrorContent( message = viewState.message.invoke(), onTryAgainClick = verificationCodeHandler.refreshClick, - modifier = modifier, + modifier = Modifier.fillMaxSize(), ) } is VerificationCodeState.ViewState.Loading -> { - BitwardenLoadingContent(modifier = modifier) + BitwardenLoadingContent(modifier = Modifier.fillMaxSize()) } } } diff --git a/app/src/main/res/drawable/ic_briefcase.xml b/app/src/main/res/drawable/ic_briefcase.xml deleted file mode 100644 index 48c500a25..000000000 --- a/app/src/main/res/drawable/ic_briefcase.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_enterprise_small.xml b/app/src/main/res/drawable/ic_enterprise_small.xml new file mode 100644 index 000000000..ea40d6e8b --- /dev/null +++ b/app/src/main/res/drawable/ic_enterprise_small.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mobile.xml b/app/src/main/res/drawable/ic_mobile.xml deleted file mode 100644 index 1f26b04b5..000000000 --- a/app/src/main/res/drawable/ic_mobile.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_mobile_small.xml b/app/src/main/res/drawable/ic_mobile_small.xml new file mode 100644 index 000000000..9523892dc --- /dev/null +++ b/app/src/main/res/drawable/ic_mobile_small.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_number1.xml b/app/src/main/res/drawable/ic_number1.xml index 70c814975..07586068c 100644 --- a/app/src/main/res/drawable/ic_number1.xml +++ b/app/src/main/res/drawable/ic_number1.xml @@ -1,13 +1,13 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M0,0.333h24v24h-24z"/> diff --git a/app/src/main/res/drawable/ic_number2.xml b/app/src/main/res/drawable/ic_number2.xml index 3937b2d4c..8c42b86c0 100644 --- a/app/src/main/res/drawable/ic_number2.xml +++ b/app/src/main/res/drawable/ic_number2.xml @@ -1,17 +1,9 @@ - - - - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_number3.xml b/app/src/main/res/drawable/ic_number3.xml index 65d016ae6..1a882b95d 100644 --- a/app/src/main/res/drawable/ic_number3.xml +++ b/app/src/main/res/drawable/ic_number3.xml @@ -1,17 +1,13 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> - - - - + android:pathData="M0,0.333h24v24h-24z"/> + diff --git a/app/src/main/res/drawable/ic_number4.xml b/app/src/main/res/drawable/ic_number4.xml index 229f2704a..9644de83e 100644 --- a/app/src/main/res/drawable/ic_number4.xml +++ b/app/src/main/res/drawable/ic_number4.xml @@ -1,17 +1,13 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> - - - - + android:pathData="M0,0.333h24v24h-24z"/> + diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 263366235..48cbf0658 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -952,7 +952,7 @@ Wil u na die rekening omskakel? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Wil u na die rekening omskakel? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Wil u na die rekening omskakel? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index 23855fd85..11b653501 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -952,7 +952,7 @@ أو تسجيل الدخول، قد يكون لديك حساب بالفعل. الحصول على رسائل البريد الإلكتروني من Bitwarden للحصول على الإعلانات والنصائح وفرص البحث. إلغاء الاشتراك في أي وقت. احصل على المشورة والإعلانات وفرص البحث من Bitwarden في صندوق الوارد الخاص بك. قم بإلغاء الاشتراك في أي وقت. - الخصوصية، مرتبة حسب الأولوية + الخصوصية، مرتبة حسب الأولوية حفظ تسجيلات الدخول والبطاقات والهويات إلى خزانتك الآمنة. يستخدم Bitwarden المعرفة الصفرية، التشفير من النهاية إلى النهاية لحماية ما هو مهم بالنسبة لك. قم بإعداد فتح القفل الحيوي والتعبئة التلقائية لتسجيل الدخول إلى حساباتك دون كتابة حرف واحد. تسجيل دخول سريع وسهل @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-az-rAZ/strings.xml b/app/src/main/res/values-az-rAZ/strings.xml index 762ea8c39..419cde4e1 100644 --- a/app/src/main/res/values-az-rAZ/strings.xml +++ b/app/src/main/res/values-az-rAZ/strings.xml @@ -70,7 +70,7 @@ Təşəkkür edirik Alətlər URI - Kilidi barmaq izi ilə açın + Kilidi barmaq izi ilə aç İstifadəçi adı %1$s xanası lazımlıdır. %1$s kopyalandı @@ -104,7 +104,7 @@ Hesab yarat Hesab yaradılır... Elementə düzəliş et - Avto-sinxr icazə ver + Avto-sinxrn icazə ver Ana parol məsləhətini alacağınız hesabınızın e-poçt ünvanını daxil edin. Tətbiq uzantısını təkrar aktivləşdir Demək olar ki, hazırdır! @@ -183,7 +183,7 @@ Touch ID İki mərhələli giriş %1$s ilə kilidi aç - PIN kod ilə kilidi açın + Kilidi PIN kodla aç Doğrulanır Doğrulama kodu Elementə bax @@ -334,7 +334,7 @@ Skan prosesi avtomatik baş tutacaq. Tullantı qutusunda heç bir element yoxdur. Avto-doldurma əlçatımlılıq xidməti Digər tətbiqlərdə və vebdə istifadəçi adı və parol xanalarının doldurulmasına kömək edin. - Bitwarden avto-doldurma xidməti, giriş məlumatlarının cihazınızdakı digər tətbiqlərdə doldurmasına kömək etməsi üçün Android Avto-doldurma Çərçivəsini istifadə edir. + Bitwarden avto-doldurma xidməti, cihazınızdakı digər tətbiqlərə giriş məlumatlarını doldurmağa kömək edən Android Avto-doldurma Çərçivəsini istifadə edir. Giriş məlumatlarını digər tətbiqlərdə doldurmaq üçün Bitwarden avto-doldurma xidmətini istifadə edin. Avto-doldurma ayarlarını aç Face ID @@ -477,7 +477,7 @@ Skan prosesi avtomatik baş tutacaq. Seansın müddəti bitdi. Biometrik doğrulama Biometrik - Kilidi biometriklə aç + Kilidi açmaq üçün biometrik istifadə et Bitwarden diqqətinizi tələb edir - Bitwarden ayarlarında \"Avto-doldurma əlçatımlılıq xidməti\"nə baxın 3. Android tətbiq ayarlarında Bitwarden-i tapın və \"Digər tətbiqlərin üzərində göstər\" seçiminə (\"Qabaqcıl\" seçimin altında ola bilər) gedin və örtük dəstəyinə icazə vermək üçün açara toxunun. İcazə @@ -515,7 +515,7 @@ Skan prosesi avtomatik baş tutacaq. Həqiqətən tullantı qutusuna göndərmək istəyirsiniz? Ana parolun doğrulanması gözlənildiyi üçün bu hesab üzrə biometrik kilid açma sıradan çıxarıldı. Ana parolun doğrulanması gözlənildiyi üçün bu hesab üzrə avto-doldurma biometrik kilid açma sıradan çıxarıldı. - Təzələmə zamanı sinxr icazə ver + Təzələmə zamanı sinxrn icazə ver Barmağınızla aşağı çəkdikdə seyfi sinxronlaşdır. Müəssisə üçün tək daxil olma Təşkilatınızın tək daxil olma portalını istifadə edərək daha tez giriş edə bilərsiniz. Başlatmaq üçün lütfən təşkilatınızın identifikatorunu daxil edin. @@ -538,16 +538,16 @@ Skan prosesi avtomatik baş tutacaq. Xidmət Şərtləri Gizlilik Siyasəti Bitwarden diqqətinizi tələb edir - Bitwarden Ayarlarında \"Avto-doldurma xidməti\"ndə \"Üzərindən göstər\"i işə salın - Keçid açarının idarə edilməsi + Keçid açarını idarəetmə Avto-doldurma xidmətləri - Sətir daxili avto-doldurmanı istifadə edin - Seçdiyiniz IME (klaviatura) dəstəkləyirsə sətir daxili avto-doldurmanı istifadə edə bilərsiniz. Əgər konfiqurasiyanız dəstəkləmirsə (və ya bu seçim sıradan çıxarılıbsa) ilkin Avto-doldurma qutusu istifadə ediləcəkdir. + Sətirdaxili avto-doldurmanı istifadə et + Seçdiyiniz IME (klaviatura) dəstəkləyirsə sətirdaxili avto-doldurmanı istifadə edin. Əgər konfiqurasiyanız dəstəkləmirsə (və ya bu seçim söndürülübsə) ilkin Avto-doldurma örtüyü istifadə ediləcəkdir. Əlçatımlılığı istifadə et Saytda və vebdə giriş məlumatlarınızı avto-doldurmaq üçün Bitwarden Əlçatımlılıq Xidmətini istifadə edin. Qurulanda, giriş xanalarını seçərkən açılan pəncərə göstərəcəyik. Saytda və vebdə giriş məlumatlarınızı avto-doldurmaq üçün Bitwarden Əlçatımlılıq Xidmətini istifadə edin. (\"Üzərində göstər\" seçimi açıq olmalıdır) - Avto-doldurma cəld əməliyyat qutusunu istifadə etmək üçün Bitwarden əlçatımlılıq xidmətini istifadə edin və/və ya \"Üzərində göstər\"i (əgər fəaldırsa) istifadə edərək açılan bir pəncərə göstərə bilərsiniz. - Avto-doldurma cəld əməliyyat qutusunu istifadə etmək və ya \"Üzərində göstər\"i (əgər fəaldırsa) istifadə edərək Avto-doldurma xidmətini təqlid etmək tələb olunur. - Avto-doldurma Cəld Əməliyyat xanasını istifadə etmək tələb olunur. + Avto-doldurma Cəld Əməliyyat Xanasını istifadə etmək üçün Bitwarden Əlçatımlılıq Xidmətini istifadə edin və/və ya \"Üzərində göstər\"i (əgər açıqdırsa) istifadə edərək açılan bir pəncərədə göstərin. + Avto-doldurma Cəld Əməliyyat Xanasını istifadə etmək və ya \"Üzərində göstər\"i (əgər açıqdırsa) istifadə edərək Avto-doldurma xidmətini təqlid etmək üçün tələb olunur. + Avto-doldurma Cəld Əməliyyat Xanasını istifadə etmək üçün tələb olunur. \"Üzərində göstər\"i istifadə edin Giriş xanalarını seçdikdə Bitwarden Əlçatımlılıq Xidmətinin açılan pəncərə göstərməsinə icazə verir. İşə salındıqda Bitwarden Əlçatımlılıq Xidməti, giriş məlumatlarınızın avto-doldurmasına kömək etmək üçün giriş xanaları seçildikdə açılan pəncərə göstərəcək. @@ -767,7 +767,7 @@ Bu hesaba keçmək istəyirsiniz? Başqa bir seçimə ehtiyacınız var? Bütün giriş etmə seçimlərinə bax Bu tələb artıq yararsızdır - Giriş tələbləri gözlənilir + Gözlənilən giriş tələbləri Bütün tələbləri rədd et Gözləyən bütün giriş tələblərini rədd etmək istədiyinizə əminsiniz? Tələblər rədd edildi @@ -860,8 +860,8 @@ Bu hesaba keçmək istəyirsiniz? %1$s saat %2$s dəqiqə %1$s saat Yeni keçid açarlarını saxlamaq və seyfinizdəki keçid açarları ilə giriş etmək üçün Bitwarden-i istifadə edin. - Android Avto-doldurma Çərçivəsi, giriş məlumatlarını cihazınızdakı digər tətbiqlərə doldurmağa kömək etmək üçün istifadə olunur. - Seçdiyiniz klaviatura dəstəkləyirsə sətir daxili avto-doldurmanı istifadə edin. Əks halda, ilkin örtük istifadə edin. + Android Avto-doldurma Çərçivəsi, cihazınızdakı digər tətbiqlərə giriş məlumatlarının doldurulmasına kömək etmək üçün istifadə olunur. + Seçdiyiniz klaviatura dəstəkləyirsə sətirdaxili avto-doldurmanı istifadə edin. Əks halda, ilkin örtük istifadə edin. Əlavə seçimlər Veb tətbiqlə davam edilsin? %1$s ilə davam edilsin? @@ -951,7 +951,7 @@ Bu hesaba keçmək istəyirsiniz? Ya da bir hesabınız varsa giriş edin. Elanlar, məsləhətlər və araşdırma fürsətləri üçün Bitwarden-dən e-poçt alın. İstənilən vaxt abunəlikdən çıxa bilərsiniz. Gələn qutunuzda Bitwarden-dən məsləhət, elan və araşdırma imkanları ilə bağlı məktublar alın. İstənilən vaxt abunəlikdən çıxa bilərsiniz. - Məxfilik, prioritetdir + Məxfilik, prioritetdir Girişləri, kart və kimlik məlumatlarınızı güvənli seyfinizdə saxlayın. Bitwarden, sizin üçün mühüm olanları qorumaq üçün zero-knowledge və ucdan-uca şifrələmə istifadə edir. Tək bir hərf yazmadan hesablarınıza giriş etmək üçün biometrik kilid açma və avto-doldurma seçimlərini qurun. Cəld və asan giriş @@ -1000,7 +1000,7 @@ Bu hesaba keçmək istəyirsiniz? Lütfən qeydiyyatı yenidən başladın və ya giriş etməyə çalışın. Artıq bir hesabınız ola bilər. Qeydiyyatı yenidən başlat Kimlik doğrulayıcı sinxrn - Bitwarden Kimlik doğrulayıcı sinxronlaşdırmasına icazə ver + Kimlik doğrulayıcını sinxrn icazə ver Qeydiyyat jetonu doğrulanarkən bir problem baş verdi. Avto-doldurmanı işə sal Tək toxunuşda hesablarınıza giriş etmək üçün avto-doldurmanı istifadə edin. @@ -1085,4 +1085,5 @@ Bu hesaba keçmək istəyirsiniz? Cihazlar arasında cəld və asan giriş et Bitwarden, başqa bir cihazdan yeni bir giriş tələbi aldığınız hər dəfəsində sizi məlumatlandıracaq. İndilik ötür + Bitdi diff --git a/app/src/main/res/values-be-rBY/strings.xml b/app/src/main/res/values-be-rBY/strings.xml index aedc2be7c..5abc8677a 100644 --- a/app/src/main/res/values-be-rBY/strings.xml +++ b/app/src/main/res/values-be-rBY/strings.xml @@ -951,7 +951,7 @@ Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1034,7 +1034,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1085,4 +1085,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index ba2b23494..fe13e338c 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -952,7 +952,7 @@ Или се впишете – може вече да имате регистрация. Получавайте е-писма от Битоурден за новини, съвети и възможности за проучвания. Можете да се отпишете по всяко време. Получавайте съвети, обявления и предложения за участия в проучвания от Битоурден в е-пощата си. Можете да се отпишете по всяко време. - Поверителността е от първостепенна важност + Поверителността е от първостепенна важност Съхранявайте данни за вписване, карти и самоличности в своя защитен трезор. Битуорден използва шифроване от край до край за да пази това, което е важно за Вас. Настройте отключването чрез биометрични данни и автоматичното попълване, за да се вписвате в регистрациите си без да натискате и един клавиш. Бързо и лесно вписване @@ -1086,4 +1086,5 @@ Вписввайте се бързо и лесно на всяко устройство Битуорден може да Ви известява всеки път, когато получите нова заявка за вписване от друго устройство. Пропускане засега + Готово diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 67986a711..a4066ae83 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-bs-rBA/strings.xml b/app/src/main/res/values-bs-rBA/strings.xml index ec415032d..83870b072 100644 --- a/app/src/main/res/values-bs-rBA/strings.xml +++ b/app/src/main/res/values-bs-rBA/strings.xml @@ -951,7 +951,7 @@ Skeniranje će biti izvršeno automatski. Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1034,7 +1034,7 @@ Skeniranje će biti izvršeno automatski. log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1085,4 +1085,5 @@ Skeniranje će biti izvršeno automatski. Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index fcfded796..c8e205848 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -952,7 +952,7 @@ Voleu canviar a aquest compte? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Voleu canviar a aquest compte? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Voleu canviar a aquest compte? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 8f2846aa3..df83e3686 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -951,7 +951,7 @@ Chcete se přepnout na tento účet? Nebo se přihlaste, možná už máte účet. Získejte e-maily od Bitwardenu pro oznámení, poradenství a výzkumné příležitosti. Můžete se kdykoli odhlásit. Dostávejte do své e-mailové schránky rady, oznámení a příležitosti k výzkumu od společnosti Bitwarden. Odhlásit odběr můžete kdykoli. - Soukromí, upřednostněno + Soukromí, upřednostněno Uložte si přihlašovací údaje, karty a identity do zabezpečeného trezoru. Bitwarden používá šifrování end-to-end s nulovou znalostí, aby ochránil to, co je pro Vás důležité. Nastavte biometrické odemknutí a automatické vyplňování pro přihlášení k Vašim účtům bez zadání jediného písmene. Rychlé a snadné přihlášení @@ -1085,4 +1085,5 @@ Chcete se přepnout na tento účet? Přihlašujte se rychle a snadno napříč zařízeními Bitwarden Vás může upozornit pokaždé, když obdržíte novou žádost o přihlášení z jiného zařízení. Prozatím přeskočit + Hotovo diff --git a/app/src/main/res/values-cy-rGB/strings.xml b/app/src/main/res/values-cy-rGB/strings.xml index 45b2f57f6..e2eda764a 100644 --- a/app/src/main/res/values-cy-rGB/strings.xml +++ b/app/src/main/res/values-cy-rGB/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 69d9156a3..82e778a05 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -21,7 +21,7 @@ E-mailadresse E-mail os Email os direkte for at få hjælp eller give feedback. - Indtast din pinkode. + Angiv PIN-koden. Favoritter Indsend en fejlrapport Opret en problematik i vores GitHub-repo. @@ -87,15 +87,15 @@ Din nye konto er oprettet! Tilføj et emne App-udvidelse - Brug Bitwardens hjælpefunktion til autoudfyldning af dine logins på tværs af apps og nettet. + Brug Bitwardens hjælpefunktion til autoudfyldning af logins på tværs af apps og internet. Autoudfyldningstjeneste Angiv Bitwarden som aktuel adgangsnøgleudbyder i enhedsindstillingerne. Undgå tvetydige tegn Bitwarden app-udvidelse Den letteste måde at tilføje nye logins til boksen er fra Bitwarden app-udvidelsen. Læs mere om brugen af Bitwarden app-udvidelsen ved at gå til skærmen \"Indstillinger\". - Benyt Bitwarden i Safari og andre apps til autoudfyldelse af dine logins. - Bitwarden autoudfyldningstjeneste - Benyt Bitwardens hjælpefunktion til autoudfyldning af dine logins. + Benyt Bitwarden i Safari og andre apps til autoudfyldelse af logins. + Bitwarden Autoudfyldningstjeneste + Benyt Bitwardens tilgængelighedstjeneste til autoudfyldning af logins. Skift e-mail Man kan skifte sin e-mailadresse via bitwarden.com web-boksen. Besøg webstedet nu? Skift hovedadgangskode @@ -197,8 +197,8 @@ Der er ingen emner i boksen til %1$s. Der er ingen emner i boksen matchende \"%1$s\" Søg efter eller tilføj et nyt login - Når du vælger et inputfelt og ser en Bitwarden-autoudfyldningoverlejring, kan du trykke på den for at starte autoudfyldningstjenesten. - Tryk på denne notifikation for at autoudfylde med et element fra din boks. + Når et inputfelt vælges, og en Bitwarden-autoudfyldningsoverlejring vises, kan man trykker på den for at starte autoudfyldningstjenesten. + Tryk på denne notifikation for at autoudfylde et emne fra boksen. Åbn Tilgængelighedsindstillinger 1. I Androids Tilgængelighedsindstillinger, tryk på \"Bitwarden\" under afsnittet Tjenester. 2. Slå kontakten til og tryk på OK for at acceptere. @@ -207,10 +207,10 @@ Fra Til Status - Den letteste måde at tilføje nye logins til din boks er fra Bitwarden Autoudfyldningstjenesten. Få mere at vide om brugen af Bitwarden Autoudfyldningstjenesten ved at gå til skærmen \"Indstillinger\". + Den letteste måde at føje nye logins til boksen er fra Bitwarden Autoudfyldningstjeneste. Læs mere om brugen af Bitwarden Autoudfyldningstjeneste ved at gå til skærmen \"Indstillinger\". Autoudfyld - Vil du autoudfylde eller se dette element? - Sikker på, at dette emne skal autoudfyldes? Det er ikke et identisk match med \"%1$s\". + Skal dette emne autoudfyldes eller vises? + Sikker på, at dette emne skal autoudfyldes? Det matcher ikke \"%1$s\" fuldstændigt. Matchende emner Mulige matchende emner Søg @@ -376,13 +376,13 @@ Skanning vil ske automatisk. Logins kan nu nemt tilgås direkte fra tastaturet, når der logges ind på apps og websteder. Vi anbefaler, at andre Autoudfyld-apps deaktiveres under Indstillinger, hvis det ikke er planen at bruge dem. Få adgang til boksen direkte fra tastaturet for hurtigt at autoudfylde adgangskoder. - Følg disse instruktioner for at aktivere autoudfyldning af adgangskoder på din enhed: + Følg denne vejledning for at aktivere autoudfyldning af adgangskoder på enheden: 1. Gå til iOS-appen \"Indstillinger\" 2. Tryk på \"Adgangskoder\" 3. Tryk på \"Autoudfyld adgangskoder\" 4. Slå Autoudfyld til 5. Vælg \"Bitwarden\" - Adgangskode autoudfyld + Adgangskodeautoudfyldning Den letteste måde at tilføje nye logins til boksen er ved at bruge Bitwardens Adgangskode Autoudfyldning-udvidelse. Læs mere om brugen af Bitwarden Adgangskode Autoudfyldnig-udvidelsen ved at gå til skærmbilledet \"Indstillinger\". Ugyldig e-mailadresse. Kort @@ -478,7 +478,7 @@ Skanning vil ske automatisk. Biometrisk bekræftelse Biometri Benyt biometri til oplåsning - Bitwarden har brug for opmærksomhed - se \"Autoudfyld hjælpefunktion\" i Bitwarden-indstillinger + Bitwarden kræver opmærksomhed - se \"Autoudfyld-tilgængelighedstjeneste\" i Bitwarden-indstillinger 3. På Android app-indstillingsskærmen for Bitwarden, gå til indstillingen \"Vis oven på andre apps\" (under Avanceret) og tryk på knappen for at aktivere overlejringsunderstøttelse. Tilladelse Åbn Indstillinger for Overlejringstilladelse @@ -501,7 +501,7 @@ Skanning vil ske automatisk. Åbn Et problem opstod med at gemme denne vedhæftning. Fortsætter problemet, gem den i stedet fra web-boksen. Vedhæftning gemt - Aktivér \"Autoudfyld Tilgængelighedstjeneste\" fra Bitwarden-indstillinger for at benytte flisen Autoudfyld. + Aktivér \"Autoudfyld-tilgængelighedstjeneste\" fra Bitwarden-indstillinger for at benytte flisen Autoudfyld. Ingen adgangskodefelter fundet Sender til papirkurven... Emne er sendt til papirkurven. @@ -589,7 +589,7 @@ Skanning vil ske automatisk. Private notater om denne Send. Deaktivér denne Send, så ingen kan tilgå den Kontoen har ingen Sends. - Tilføj en Send + Ny Send Kopiér link Del link Send link @@ -952,7 +952,7 @@ Skift til denne konto? Eller log ind, da en konto muligvis allerede eksisterer. Få e-mails fra Bitwarden vedr. bekendtgørelser, råd og forskningsmuligheder. Afmeld når som helst. Få råd, bekendtgørelser og forskningsmuligheder fra Bitwarden i indbakken. Afmeld til enhver tid. - Fortrolighed, prioriteret + Fortrolighed, prioriteret Gem logins, kort og identiteter i den sikre boks. Bitwarden bruger nul-viden, ende-til-ende kryptering til at beskytte brugerens vigtigt ting. Opsæt biometrisk oplåsning og autofyld for at logge ind på konti uden at skrive ét enkelt bogstav. Hurtig og nem indlogning @@ -1008,7 +1008,7 @@ Skift til denne konto? Slå til senere Slå autofyld til senere? Dette trin kan når som helst fuldføres via Indstillinger. - Man kan nu bruge autofyld til at logge ind på apps og websteder med sine gemte adgangskoder. Nu kan man tjekke alt andet, Bitwarden har at tilbyde. + Man kan nu tjekke alt det andet, Bitwarden har at tilbyde, og begynde at håndtere sine adgangskoder sikkert. Så er alt klar! Fejl under oprettelse af forbindelse til Duo-tjenesten. Brug en anden totrins-indlogningsmetode eller kontakt Duo for hjælp. Hovedadgangskodetip @@ -1040,8 +1040,8 @@ Skift til denne konto? Trin 1 af 3 Eksportere gemte logins Slet denne fil, når importen er færdig. - Åbn en ny webbrowserfane på computeren, og gå til vault.bitwarden.com - gå til vault.bitwarden.com + Åbn en ny webbrowserfane på computeren, og gå til %1$s + gå til %1$s Log ind på Bitwardens web-app. Trin 2 af 3 Log ind på Bitwarden @@ -1086,4 +1086,5 @@ Skift til denne konto? Log ind hurtigt og nemt på tværs af enheder Bitwarden kan give besked, hver gang man modtager en ny login-anmodning fra en anden enhed. Overspring for nu + Færdig diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 28ed26561..860d45383 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -951,7 +951,7 @@ Möchtest du zu diesem Konto wechseln? Oder melde dich an, du hast möglicherweise bereits ein Konto. Erhalte E-Mails von Bitwarden für Ankündigungen, Ratschläge und Marktforschungsumfragen. Melde dich jederzeit wieder ab. Erhalte Ratschläge, Ankündigungen und Marktforschungsumfragen von Bitwarden in deinem Posteingang. Melde dich jederzeit wieder ab. - Datenschutz, priorisiert + Datenschutz, priorisiert Speichere Zugangsdaten, Karten und Identitäten in deinem sicheren Tresor. Bitwarden verwendet Zero-Knowledge und Ende-zu-Ende-Verschlüsselung, um das zu schützen, was für dich wichtig ist. Richte biometrisches Entsperren und Auto-Ausfüllen ein, um dich ohne einen einzigen Buchstaben einzugeben bei deinen Konten anzumelden. Schnelle und einfache Anmeldung @@ -1083,6 +1083,7 @@ Möchtest du zu diesem Konto wechseln? Fingerabdruck kopieren Benachrichtigungen aktivieren Schnelle und einfache Anmeldung über verschiedene Geräte hinweg - Bitwarden kann Sie jedes Mal benachrichtigen, wenn Sie eine neue Anmeldeanforderung von einem anderen Gerät erhalten. - Erst einmal überspringen + Bitwarden kann dich jedes Mal benachrichtigen, wenn du eine neue Anmeldeanfrage von einem anderen Gerät erhältst. + Vorerst überspringen + Fertig diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index c44abb910..679b44a3b 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -241,7 +241,7 @@ Κωδικός επαλήθευσης (TOTP) Το κλειδί επαλήθευσης προστέθηκε. Αδυναμία ανάγνωσης κλειδιού επαλήθευσης. - Σημαδέψτε με την κάμερα σας τον κωδικό QR. + Σημαδέψτε με την κάμερα σας τον κωδικό QR. Η σάρωση θα γίνει αυτόματα. Σάρωση κώδικα QR Κάμερα @@ -952,7 +952,7 @@ Ή συνδεθείτε, μπορεί να έχετε ήδη λογαριασμό. Λάβετε μηνύματα ηλ. ταχυδρομείου από το Bitwarden για ανακοινώσεις, συμβουλές και ερευνητικές ευκαιρίες. Ακυρώστε τη συνδρομή σας ανα πάσα στιγμή. Λάβετε συμβουλές, ανακοινώσεις και ευκαιρίες έρευνας από το Bitwarden στα εισερχόμενά σας. Μπορείτε να απεγγραφείτε ανα πάσα στιγμή. - Προταιρεότητα, η ιδιωτικότητα + Προταιρεότητα, η ιδιωτικότητα Αποθηκεύστε συνδέσεις, κάρτες και ταυτότητες στο ασφαλές θησαυ/κιό σας. Το Bitwarden χρησιμοποιεί κρυπτογράφηση από άκρο σε άκρο και μηδενικής γνώσης για να προστατεύσει ό,τι είναι σημαντικό για εσάς. Ρυθμίστε το βιομετρικό ξεκλείδωμα και την αυτόματη συμπλήρωση για σύνδεση στους λογαριασμούς σας χωρίς να πληκτρολογήσετε ούτε ένα γράμμα. Γρήγορη και εύκολη σύνδεση @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Βήμα 1 από 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 21dc4dc24..7690f8b89 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritised + Privacy, prioritised Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-en-rIN/strings.xml b/app/src/main/res/values-en-rIN/strings.xml index 83af2e503..b1357e285 100644 --- a/app/src/main/res/values-en-rIN/strings.xml +++ b/app/src/main/res/values-en-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritised + Privacy, prioritised Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 8f6d62890..8b56b38c6 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -953,7 +953,7 @@ seleccione Agregar TOTP para almacenar la clave de forma segura Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1036,7 +1036,7 @@ seleccione Agregar TOTP para almacenar la clave de forma segura log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1087,4 +1087,5 @@ seleccione Agregar TOTP para almacenar la clave de forma segura Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index d941a5a28..c65ab49eb 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -952,7 +952,7 @@ Soovid selle konto peale lülituda? Või logi sisse, sul võib olla konto juba olemas. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Soovid selle konto peale lülituda? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Soovid selle konto peale lülituda? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml index 11ef86e0b..93677fd70 100644 --- a/app/src/main/res/values-eu-rES/strings.xml +++ b/app/src/main/res/values-eu-rES/strings.xml @@ -950,7 +950,7 @@ Kontu honetara aldatu nahi duzu? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1033,7 +1033,7 @@ Kontu honetara aldatu nahi duzu? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1084,4 +1084,5 @@ Kontu honetara aldatu nahi duzu? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index aefb8e8c7..46a3996ac 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -952,7 +952,7 @@ Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index f0022ee97..e1b456332 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -952,7 +952,7 @@ Haluatko vaihtaa tähän tiliin? Tai kirjaudu sisään. Sinulla saattaa jo olla tili. Vastaanota Bitwardenilta uutiskirjeitä julkaisuista, tukiresursseista ja tutkimusmahdollisuuksista. Lopeta tilaus milloin tahansa. Vastaanota Bitwardenilta postilaatikkoosi vinkkejä, uutisia ja tutkimusmahdollisuuksia. Lopeta tilaus milloin tahansa. - Yksityisyys etusijalla + Yksityisyys etusijalla Tallenna kirjautumistiedot, kortit and henkilöllisyydet suojattuun holviisi. Bitwarden suojaa tärkeät tietosi nollatietoisella päästä pähän -salauksella. Määritä biometrinen avaus ja automaattitäyttö kirjautuaksesi tileillesi kirjoittamatta yhtään kirjainta. Nopea ja helppo kirjautuminen @@ -1035,7 +1035,7 @@ Haluatko vaihtaa tähän tiliin? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Vie salasanasi. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Valitse tuotavat tiedot Vaihe 1/3 Vie tallennetut kirjautumistietosi @@ -1082,8 +1082,9 @@ Haluatko vaihtaa tähän tiliin? SSH-avaimet Kopioi julkinen avain Kopioi sormenjälki - Enable notifications + Ota ilmoitukset käyttöön Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. - Skip for now + Ohita tällä kertaa + Valmis diff --git a/app/src/main/res/values-fil-rPH/strings.xml b/app/src/main/res/values-fil-rPH/strings.xml index 7cc82bcc9..84dd1aa5a 100644 --- a/app/src/main/res/values-fil-rPH/strings.xml +++ b/app/src/main/res/values-fil-rPH/strings.xml @@ -952,7 +952,7 @@ Gusto mo bang pumunta sa account na ito? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Gusto mo bang pumunta sa account na ito? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Gusto mo bang pumunta sa account na ito? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 6b70a9375..3b2a413aa 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -952,7 +952,7 @@ Voulez-vous basculer vers ce compte ? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Connexion rapide et facile @@ -1035,7 +1035,7 @@ Voulez-vous basculer vers ce compte ? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Voulez-vous basculer vers ce compte ? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml index 6e7ecc67a..125e5b857 100644 --- a/app/src/main/res/values-gl-rES/strings.xml +++ b/app/src/main/res/values-gl-rES/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 22195edab..97c6451ef 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -951,7 +951,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1034,7 +1034,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1085,4 +1085,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-hr-rHR/strings.xml b/app/src/main/res/values-hr-rHR/strings.xml index 095470202..fd72ca6f3 100644 --- a/app/src/main/res/values-hr-rHR/strings.xml +++ b/app/src/main/res/values-hr-rHR/strings.xml @@ -950,7 +950,7 @@ Ili se prijavi, ako već imaš račun. Primaj e-poštu od Bitwardena s najavamam savjetima i mogućnostima istraživanja. Otkaži pretplatu u bilo kojem trenutku. Primaj savjete, najave i mogućnosti istraživanja od Bitwardena u svoju Ulaznu poštu. Otkaži pretplatu u bilo kojem trenutku. - Privatnost na prvom mjestu + Privatnost na prvom mjestu Spremi prijave, kartice i identitete u svoj sigurni trezor. Bitwarden koristi end-to-end enkripciju bez znanja kako bi zaštitio ono što ti je važno. Uključi biometrijsko otključavanje i auto-ispunu za prijavu bez utipkavanja ijednog slova. Brza i jednostavna prijava @@ -1033,7 +1033,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1084,4 +1084,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 753bdabd1..ddf751866 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -691,7 +691,7 @@ Kód megadása manuálisan TOTP hozzáadása TOTP beállítása - A kulcs sikeres megadása után válasszuk ki a + A kulcs sikeres megadása után válasszuk ki a TOTP hozzáadása a kulcs biztonságos tárolásához lehetőséget. Ha a zárolási lehetőségeket \"Soha” értékre állítjuk, akkor a széf bárki számára elérhető lesz, aki hozzáfér az eszközhöz. Ha ezt az opciót használjuk, akkor gondoskodni kell arról, hogy az eszköz megfelelően védett legyen. A beírt webcímek közül egy vagy több érvénytelen. Vizsgáljuk át és próbáljuk meg újra elmenteni. @@ -951,7 +951,7 @@ Szeretnénk átváltani erre a fiókra? Vagy Bejelentkezés, ha már van fiók. Emaileket kaphatunk a Bitwardentől bejelentésekről, tanácsokról és kutatási lehetőségekről. Bármikor leiratkozhatunk. Tanácsokat, bejelentéseket és kutatási lehetőségeket kaphatunk a Bitwardentől a postaládába. Bármikor leiratkozhatunk. - Adatvédelem, prioritás + Adatvédelem, prioritás Mentsük el a bejelentkezési adatokat, kártyákat és azonosításokat a biztonságos széfbe. A Bitwarden tudás nélküli, végpontok közötti titkosítást használ a felhasználók számára fontos dolgok védelmére. Állítsuk be a biometrikus feloldást és az automatikus kitöltést, hogy egyetlen betű beírása nélkül jelentkezzünk be a fiókokba. Gyors és könnyű bejelentkezés @@ -1085,4 +1085,5 @@ Szeretnénk átváltani erre a fiókra? Bejelentkezés gyorsan és egyszerűen minden eszközön A Bitwarden minden alkalommal értesíthetést küld, amikor új bejelentkezési kérelem érkezik egy másik eszközről. Kihagyás most + Kész diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 48b886642..61321e485 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 6e5705c74..82cfbae25 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -951,7 +951,7 @@ Vuoi passare a questo account? Oppure effettua l\'accesso se potresti avere già un account. Ricevi email da Bitwarden per annunci, consigli e opportunità di ricerca. Disiscriviti in qualsiasi momento. Ottieni consigli, annunci e opportunità di ricerca da Bitwarden nella tua casella di posta. Disiscriviti in qualsiasi momento. - Privacy, al primo posto + Privacy, al primo posto Salva login, carte e identità nella tua cassaforte sicura. Bitwarden usa la crittografia end-to-end e zero-knowledge per proteggere ciò che è importante. Imposta lo sblocco biometrico e il riempimento automatico per accedere ai tuoi account senza digitare una sola lettera. Accesso facile e veloce @@ -1034,7 +1034,7 @@ Vuoi passare a questo account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1085,4 +1085,5 @@ Vuoi passare a questo account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-iw-rIL/strings.xml b/app/src/main/res/values-iw-rIL/strings.xml index 647906643..a3d1a7743 100644 --- a/app/src/main/res/values-iw-rIL/strings.xml +++ b/app/src/main/res/values-iw-rIL/strings.xml @@ -207,7 +207,7 @@ כבוי פעיל סטטוס - הדרך הקלה ביותר להוספת סיסמאות לכספת היא דרך שירות ההשלמה האוטומטי של Bitwarden. למד עוד אודות השימוש ביכולת ההשלמה האוטומטית של + הדרך הקלה ביותר להוספת סיסמאות לכספת היא דרך שירות ההשלמה האוטומטי של Bitwarden. למד עוד אודות השימוש ביכולת ההשלמה האוטומטית של Bitwarden בעזרת פתיחת חלון \"הגדרות\". השלמה אוטומטית האם ברצונך להפעיל את ההשלמה האוטומטית או לצפות בפריט זה? @@ -955,7 +955,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1038,7 +1038,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1089,4 +1089,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 9c079cce3..ff2471cbd 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -952,7 +952,7 @@ またはログインしてください。すでにアカウントを持っている可能性があります。 Bitwarden からメールでお知らせやアドバイス、アンケートを受け取ります。配信停止はいつでもできます。 Bitwarden からメールでアドバイスや告知、アンケートを受け取りましょう。配信停止はいつでもできます。 - プライバシーが最優先 + プライバシーが最優先 ログイン情報、カード、ID を安全な保管庫に保存します。Bitwarden はゼロ知識、エンドツーエンドの暗号化を使用して、あなたにとって重要なものを保護します。 生体認証のロック解除と自動入力を設定して、文字を何も入力せずにアカウントにログインします。 すばやく簡単にログイン @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ka-rGE/strings.xml b/app/src/main/res/values-ka-rGE/strings.xml index 637919302..cfbdae808 100644 --- a/app/src/main/res/values-ka-rGE/strings.xml +++ b/app/src/main/res/values-ka-rGE/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index 76b8b6113..9e4289f8e 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index bfdfcd40a..65b76391c 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -952,7 +952,7 @@ Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml index f6f3255e6..ff286829c 100644 --- a/app/src/main/res/values-lt-rLT/strings.xml +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -952,7 +952,7 @@ Ar norite pereiti prie šios paskyros? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Ar norite pereiti prie šios paskyros? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Ar norite pereiti prie šios paskyros? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-lv-rLV/strings.xml b/app/src/main/res/values-lv-rLV/strings.xml index fcf15a997..2cf06e9bf 100644 --- a/app/src/main/res/values-lv-rLV/strings.xml +++ b/app/src/main/res/values-lv-rLV/strings.xml @@ -952,7 +952,7 @@ Vai pārslēgties uz šo kontu? Vai jāmēģina pieteikties, varbūt Tev jau ir konts. Saņemt e-pasta ziņojumus no Bitwarden par paziņojumiem, padomiem un izpētes iespējām. Abonēšanu atteikt var jebkurā brīdī. Iegūt savā iesūtnē padomus, paziņojumus un izpētes iespējas no Bitwarden. Atrakstīties var jebkurā brīdī. - Privātums pirmajā vietā + Privātums pirmajā vietā Pieteikšanās vienumu, karšu un identitāšu glabāšana savā drošajā glabātavā. Bitwarden izmanto nulles zināšanu pilnīgu šifrēšanu, lai aizsargātu to, kas Tev ir svarīgs. Iestati biometrisko atslēgšanu un automātiski aizpildi, lai pieteiktos savos kontos bez neviena burta ievadīšanas. Ātra un vienkārša pieteikšanās @@ -1086,4 +1086,5 @@ Vai pārslēgties uz šo kontu? Ātra un viegla pieteikšanās dažādās ierīcēs Bitwarden var paziņot par katru reizi, kad tiek saņemts jauns pieteikšanās pieprasījums no citas ierīces. Pagaidām izlaist + Darīts diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 4f42fb83e..9a7dfeb4f 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-mr-rIN/strings.xml b/app/src/main/res/values-mr-rIN/strings.xml index 3a4da68a5..3cc83e5a4 100644 --- a/app/src/main/res/values-mr-rIN/strings.xml +++ b/app/src/main/res/values-mr-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-my-rMM/strings.xml b/app/src/main/res/values-my-rMM/strings.xml index 637919302..cfbdae808 100644 --- a/app/src/main/res/values-my-rMM/strings.xml +++ b/app/src/main/res/values-my-rMM/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 717d5df1d..7fb76203c 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -952,7 +952,7 @@ Vil du bytte til denne kontoen? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Vil du bytte til denne kontoen? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Vil du bytte til denne kontoen? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ne-rNP/strings.xml b/app/src/main/res/values-ne-rNP/strings.xml index 637919302..cfbdae808 100644 --- a/app/src/main/res/values-ne-rNP/strings.xml +++ b/app/src/main/res/values-ne-rNP/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index ebea4edc3..d0ab9ef0d 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -952,7 +952,7 @@ Wilt u naar dit account wisselen? Of ga naar inloggen, je hebt mogelijk al een account. Ontvang e-mailberichten van Bitwarden voor aankondigingen, advies en onderzoeksmogelijkheden. Afmelden kan op ieder moment. Ontvang advies, aankondigingen en onderzoeksmogelijkheden van Bitwarden in je inbox. Je kunt je op ieder moment uitschrijven. - Privacy, geprioriteerd + Privacy, geprioriteerd Logins, kaarten en identiteiten in je beveiligde kluis opslaan. Bitwarden gebruikt zero-knowledge, end-to-end versleuteling om te beschermen wat belangrijk voor jou is. Biometrische ontgrendelen en automatisch invullen instellen zodat je kunt inloggen op je accounts zonder één letter te typen. Snel en eenvoudig inloggen @@ -1086,4 +1086,5 @@ Wilt u naar dit account wisselen? Snel en gemakkelijk inloggen op verschillende apparaten Bitwarden kan je iedere keer dat je een nieuw inlogverzoek ontvangt van een ander apparaat laten weten. Voorlopig overslaan + Klaar diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index a1a399786..e0a07d395 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Eller logg inn, det kan hende du allereie har ein konto. Få e-post frå Bitwarden for kunngjeringar, råd og forskingsmoglegheiter. Slutt å abonnere på det når som helst. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Ferdig diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index c10b7e0ba..1573680da 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 0138fe768..8a60f8790 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -952,7 +952,7 @@ Czy chcesz przełączyć się na to konto? Lub zaloguj się, jeśli już posiadasz konto. Otrzymuj wiadomości e-mail od Bitwarden z ogłoszeniami, poradami i możliwościami badawczymi. Możesz anulować subskrypcję w dowolnym momencie. Otrzymuj porady, ogłoszenia i możliwości badawcze od Bitwarden w swojej skrzynce odbiorczej. Możesz anulować subskrypcję w dowolnym momencie. - Privacy, prioritized + Privacy, prioritized Zapisz dane logowania, karty i tożsamości w bezpiecznym sejfie. Bitwarden używa szyfrowania end-to-end w celu ochrony tego, co jest dla Ciebie ważne. Skonfiguruj odblokowanie i autouzupełnianie biometryczne, aby zalogować się na swoje konta bez wpisywania nawet pojedynczej litery. Szybkie i łatwe logowanie @@ -1035,7 +1035,7 @@ Czy chcesz przełączyć się na to konto? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Czy chcesz przełączyć się na to konto? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2581bf6b2..9d58e26a6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -952,7 +952,7 @@ Você deseja mudar para esta conta? Ou faça o login, você pode já ter uma conta. Obtenha e-mails do Bitwarden para anúncios, conselhos e oportunidades de pesquisa. Cancele a inscrição a qualquer momento. Receba conselhos, anúncios e oportunidades de pesquisa da Bitwarden na sua caixa de entrada. Cancele a assinatura a qualquer momento. - Privacidade, priorizado + Privacidade, priorizado Salve suas credenciais, cartões e identidades no seu cofre seguro. O Bitwarden usa criptografia de zero conhecimento, de ponta a ponta para proteger o que é importante para você. Configure o desbloqueio biométrico e preenchimento automático para acessar suas contas sem digitar uma única letra. Login rápido e fácil @@ -1031,12 +1031,12 @@ Você deseja mudar para esta conta? Dê ao seu cofre uma entrada de cabeça O desbloqueio com dados biométricos requer uma autenticação biométrica forte e pode não ser compatível com todas as opções biométricas deste dispositivo. O desbloqueio com dados biométricos requer uma autenticação biométrica forte e não é compatível com as opções de dados biométricos disponíveis neste dispositivo. - On your computer, log in to your current browser or password manager. - log in to your current browser or password manager. - Export your passwords. This option is usually found in your settings. + No seu computador, faça login no seu navegador atual ou gerenciador de senhas. + faça login no seu navegador atual ou gerenciador de senhas. + Exporte suas senhas. Esta opção geralmente é encontrada nas suas configurações. Exporte suas senhas. - Select Import data in the web app, then Done to finish syncing. - Select Import data + Selecione Importar dados para o aplicativo web, depois finalizado abaixo para concluir a sincronização. + Selecionar dados de importação Etapa 1 de 3 Exportar seus logins salvos Você vai excluir esse arquivo depois que a importação estiver completa. @@ -1047,43 +1047,44 @@ Você deseja mudar para esta conta? Inicie a sessão no Bitwarden Etapa 3 de 3 Importar credenciais para o Bitwarden - In the Bitwarden navigation, find the Tools option and select Import data. - find the Tools - select Import data. - Fill out the form and import your saved password file. - import your saved password file. - then Done - For your security, be sure to delete your saved password file. - delete your saved password file. - Need help? Check out import help. - import help - Save the exported file somewhere on your computer you can find easily. - Save the exported file - This is not a recognized Bitwarden server. You may need to check with your provider or update your server. + Na navegação do Bitwarden, encontre a opção de Ferramentas e selecione os dados de importação. + encontrar as Ferramentas + selecionar importar dados. + Preencha o formulário e importe o seu arquivo de senha salva. + importar seu arquivo de senha salva. + então Concluído + Para sua segurança, certifique-se de excluir o arquivo de senha salvo. + excluir o arquivo de senha salva. + Precisa de ajuda? Confira a ajuda de importação. + importar ajuda + Salve o arquivo exportado em algum lugar no seu computador que você encontrar facilmente. + Salve o arquivo exportado + Este não é um servidor do Bitwarden reconhecido. Você pode precisar verificar com o seu provedor ou atualizar o seu servidor. Syncing logins... - SSH Key Cipher Item Types - Download the browser extension - Go to bitwarden.com/download to integrate Bitwarden into your favorite browser for a seamless experience. - Use the web app - Log in at bitwarden.com to easily manage your account and update settings. - Autofill passwords - Set up autofill on all your devices to login with a single tap anywhere. - Import Successful! - Manage your logins from anywhere with Bitwarden tools for web and desktop. - Bitwarden Tools - Got it - No logins were imported - Verified SSO Domain Endpoint - Logins imported - Remember to delete your imported password file from your computer - SSH key - Public key - Private key - SSH keys - Copy public key - Copy fingerprint - Enable notifications - Log in quickly and easily across devices - Bitwarden can notify you each time you receive a new login request from another device. - Skip for now + Tipos de itens cifrados por chave SSH + Baixar a extensão do navegador + Vá para bitwarden.com/download para integrar o Bitwarden em seu navegador favorito para uma experiência perfeita. + Usar o aplicativo web + Entre em bitwarden.com para gerenciar facilmente sua conta e atualizar as configurações. + Senhas de preenchimento automático + Configure o preenchimento automático em todos os seus dispositivos para fazer login com um único toque em qualquer lugar. + Importação Realizada com Sucesso! + Gerencie suas credenciais a partir de qualquer lugar com ferramentas do Bitwarden para web e desktop. + Ferramentas do Bitwarden + Entendi + Nenhum login foi importado + Endpoint de domínio SSO verificado + Logins importados + Lembre-se de excluir o arquivo de senha importada do seu computador + Chave SSH + Chave pública + Chave privada + Chaves SSH + Copiar chave pública + Copiar impressão digital + Ativar notificações + Faça o login rápida e facilmente entre dispositivos + O Bitwarden pode notificá-lo toda vez que receber uma nova solicitação de login de outro dispositivo. + Pular por enquanto + Concluído diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index a0fc04b44..e150d8dd5 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -952,7 +952,7 @@ Pretende mudar para esta conta? Receba e-mails do Bitwarden com anúncios, conselhos e oportunidades de investigação. Anule a subscrição em qualquer altura. Receba conselhos, anúncios e oportunidades de investigação do Bitwarden na sua caixa de entrada. Anule a subscrição a qualquer altura. - Privacidade, priorizada + Privacidade, priorizada Guarde credenciais, cartões e identidades no seu cofre seguro. O Bitwarden utiliza encriptação de conhecimento zero e de ponto a ponto para proteger o que é importante para si. Configure o desbloqueio biométrico e o preenchimento automático para iniciar sessão nas suas contas sem escrever uma única letra. Início de sessão rápido e fácil @@ -1001,7 +1001,7 @@ Anule a subscrição em qualquer altura. Por favor, reinicie o registo ou tente iniciar sessão. É possível que já tenha uma conta. Reiniciar registo Sincronização do autenticador - Permitir a sincronização do Bitwarden Authenticator + Permitir a sincronização do autenticador Ocorreu um problema com a validação do token de registo. Ativar o preenchimento automático Utilize o preenchimento automático para iniciar sessão nas suas contas com um simples toque. @@ -1035,13 +1035,13 @@ Anule a subscrição em qualquer altura. inicie sessão no seu navegador atual ou gestor de palavras-passe. Exporte as suas palavras-passe. Esta opção encontra-se normalmente nas suas definições. Exporte as suas palavras-passe. - Selecione Importar dados na aplicação Web e, em seguida, Concluído para terminar a sincronização. + Selecione Importar dados na aplicação Web e, em seguida, Concluído abaixo para terminar a sincronização. Selecione Importar dados Passo 1 de 3 Exporte as suas credenciais guardadas Eliminará este ficheiro após concluir a importação. - No seu computador, abra um novo separador do navegador e vá a vault.bitwarden.com - vá a vault.bitwarden.com + No seu computador, abra um novo separador do navegador e vá a %1$s + vá a %1$s Iniciar sessão na aplicação Web do Bitwarden. Passo 2 de 3 Iniciar sessão no Bitwarden @@ -1080,10 +1080,11 @@ Anule a subscrição em qualquer altura. Chave pública Chave privada Chaves SSH - Copy public key - Copy fingerprint + Copiar chave pública + Copiar impressão digital Ativar notificações Iniciar sessão de forma rápida e fácil em todos os dispositivos O Bitwarden pode notificá-lo sempre que receber um novo pedido de início de sessão de outro dispositivo. Saltar por agora + Concluído diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index 64d9ec5ec..9aa7396d5 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -952,7 +952,7 @@ Doriți să comutați la acest cont? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Autentificare rapidă și ușoară @@ -1035,7 +1035,7 @@ Doriți să comutați la acest cont? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Doriți să comutați la acest cont? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 10d2554c6..69e67145a 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -954,7 +954,7 @@ Или авторизуйтесь, возможно, у вас уже есть аккаунт. Получайте электронные письма от Bitwarden с анонсами, советами и возможностями для исследований. Отписаться можно в любое время. Получайте советы, объявления и исследования от Bitwarden в свой почтовый ящик. Отписаться можно в любое время. - Конфиденциальность, приоритет + Конфиденциальность, приоритет Сохраняйте логины, карты и личные данные в своем защищенном хранилище. Bitwarden использует сквозное шифрование, чтобы защитить то, что для вас важно. Настройте биометрическую разблокировку и автозаполнение, чтобы входить в свои аккаунты, не набирая ни одной буквы. Быстрая и простая авторизация @@ -1088,4 +1088,5 @@ Быстро и легко авторизуйтесь на разных устройствах Bitwarden может уведомлять вас каждый раз, когда вы получаете новый запрос на вход с другого устройства. Пока пропустить + Готово diff --git a/app/src/main/res/values-si-rLK/strings.xml b/app/src/main/res/values-si-rLK/strings.xml index e398c3f76..d175078c3 100644 --- a/app/src/main/res/values-si-rLK/strings.xml +++ b/app/src/main/res/values-si-rLK/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 04619d0f6..3bb358fb5 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -952,7 +952,7 @@ Chcete prepnúť na tento účet? Alebo sa prihláste, možno už účet máte. Dostávať e-maily od Bitwardenu s oznámeniami, radami a možnosťami výskumu. Odhlásiť dá môžete kedykoľvek. Dostávajte do schránky rady, oznámenia a príležitosti na výskum od spoločnosti Bitwarden. Kedykoľvek sa môžete odhlásiť z odberu. - Súkromie na prvom mieste + Súkromie na prvom mieste Ukladajte prihlasovacie údaje, karty a identity do zabezpečeného trezoru. Bitwarden používa šifrovanie s nulovou znalosťou od začiatku do konca na ochranu toho, čo je pre vás dôležité. Nastavte si odomykanie biometrickými údajmi a automatické vypĺňanie na prihlasovanie do účtov bez zadávania jediného písmena. Rýchle a jednoduché prihlásenie @@ -1061,7 +1061,7 @@ Chcete prepnúť na tento účet? Uložte exportovaný súbor Toto nie je známy Bitwarden server. Možno bude potrebné overiť si to u svojho poskytovateľa alebo aktualizovať server. Synchronizácia prihlásení... - Typy šifrovania pre SSH kľúče + Typy šifrovania pre kľúče SSH Stiahnuť rozšírenie pre prehliadač Pre pohodlné použivanie prejdite na stránku bitwarden.com/download a integrujte Bitwarden do svojho obľúbeného prehliadača. Použiť webovú aplikáciu @@ -1076,14 +1076,15 @@ Chcete prepnúť na tento účet? Overený koncový bod domény SSO Prihlasovacie údaje boli importované Nezabudnite z počítača odstrániť importovaný súbor s heslami - SSH kľúč + Kľúč SSH Verejný kľúč Súkromný kľúč - SSH kľúče + Kľúče SSH Kopírovať verejný kľúč Kopírovať odtlačok prsta Povoliť upozornenia Rýchle a jednoduché prihlásenie v rôznych zariadeniach Bitwarden vás môže upozorniť vždy, keď dostanete novú žiadosť o prihlásenie z iného zariadenia. Teraz preskočiť + Hotovo diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml index 65622aeeb..835860092 100644 --- a/app/src/main/res/values-sl-rSI/strings.xml +++ b/app/src/main/res/values-sl-rSI/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-sr-rSP/strings.xml b/app/src/main/res/values-sr-rSP/strings.xml index 5e6327fb8..cda33d7c5 100644 --- a/app/src/main/res/values-sr-rSP/strings.xml +++ b/app/src/main/res/values-sr-rSP/strings.xml @@ -953,7 +953,7 @@ Или пријавите се, можда већ имате налог. Добијајте имјл од Bitwarden-а за најаве, савете и могућности истраживања. Откажите претплату у било ком тренутку. Добијајте савете, најаве и могућности истраживања од Bitwarden-а у пријемном сандучету. Откажите претплату у било ком тренутку. - Приватност, приоритет + Приватност, приоритет Сачувајте пријаве, картице и идентитете у свој безбедни сеф. Bitwarden користи шифровање од почетка-до-краја да заштити оно што вам је важно. Подесите биометријско откључавање и аутоматско попуњавање да бисте се пријавили на своје налоге без уноса ниједног слова. Брза и лака пријава @@ -1083,8 +1083,9 @@ SSH кључеве Копирај јавни кључ Копирати отисак - Enable notifications - Log in quickly and easily across devices - Bitwarden can notify you each time you receive a new login request from another device. - Skip for now + Омогући обавештења + Пријавите се брзо и лако на више уређаја + Bitwarden може да вас обавести сваки пут када добијете нови захтев за пријаву са другог уређаја. + Прескочи за сада + Готово diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 74240a2ca..3a51e29c2 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -1,9 +1,9 @@ Om - Skapa - Skapa mapp - Skapa objekt + Lägg till + Lägg till mapp + Lägg till objekt Ett fel har inträffat. Tillbaka Bitwarden @@ -13,7 +13,7 @@ Kopiera användarnamn Credits Radera - Raderar… + Raderar... Vill du verkligen radera? Detta går inte att ångra. Redigera Redigera mapp @@ -21,7 +21,7 @@ E-postadress Mejla oss Mejla oss direkt för att få hjälp eller för att lämna återkoppling. - Ange din PIN-kod. + Ange din PIN-kod Favoriter Skicka en felrapport Öppna ett problemärende på vårt GitHub-arkiv. @@ -35,7 +35,7 @@ Gå till webbplats Hjälp & Feedback Dölj - Vänligen anslut till internet innan du fortsätter. + Anslut till internet innan du fortsätter. Internetanslutning krävs Ogiltigt huvudlösenord. Försök igen. Ogiltig PIN-kod. Försök igen. @@ -76,7 +76,7 @@ %1$s har kopierats Bekräfta fingeravtryck Bekräfta huvudlösenord - Verifiera PIN-kod + Bekräfta PIN-kod Version Visa Besök vår webbplats @@ -85,7 +85,7 @@ Konto Ditt nya konto har skapats! Du kan nu logga in. Ditt nya konto har skapats! - Lägg till objekt + Lägg till ett objekt Apptillägg Använd Bitwardens tillgänglighetstjänst för att automatiskt fylla i dina inloggningar i applikationer och på webben. Hjälpmedelsservice för automatisk ifyllnad @@ -135,7 +135,7 @@ Åtgärd när valvets tidsgräns överskrids Genom att logga ut upphör all åtkomst till valvet och onlineautentisering krävs efter att tidsgränsen överskridits. Är du säker på att du vill använda denna inställning? Loggar in... - Logga in eller skapa ett nytt konto för att få tillgång till ditt säkra valv. + Logga in eller skapa ett nytt konto för tillgång till ditt säkra valv. Hantera Bekräftelsen för huvudlösenordet stämde ej. Huvudlösenordet är det lösenord som du använder för att komma åt ditt valv. Det är väldigt viktigt att du inte glömmer bort ditt huvudlösenord, eftersom det inte går att återställa lösenordet om du skulle glömma bort det. @@ -210,18 +210,18 @@ Det lättaste sättet att lägga till nya inloggningar till ditt valv är från Bitwardens hjälpmedelsservice för automatisk ifyllnad. Läs mer om hur man använder Bitwardens hjälpmedelsservice för automatisk ifyllnad genom att navigera till fliken \"Inställningar\". Autofyll Vill du automatiskt fylla i den här inloggningen eller visa den? - Är du säker på att du vill automatiskt fylla i den här inloggningen? Det är inte en fullständig träff för \"%1$s\". + Är du säker på att du vill att detta objekt ska autofyllas? Det matchar inte \"%1$s\" helt. Matchande objekt Möjliga matchande objekt Sök Du letar efter en inloggning som kan automatiskt fyllas i för \"%1$s\". - Lär dig om organisationer + Läs mer om organisationer Kan inte öppna appen \"%1$s\". Autentiseringsapp Ange den 6-siffriga verifieringskoden från din autentiseringsapp. Ange den 6-siffriga verifieringskoden som skickades till %1$s. Inloggning ej tillgänglig - Detta konto har tvåstegsverifiering aktiverat, men ingen av de konfigurerade metoderna stöds av den här enheten. + Detta konto har tvåstegsverifiering aktiverat, men ingen av de konfigurerade metoderna stöds av den här enheten. Vänligen använd en enhet som stöds och/eller lägg till fler metoder som har bättre stöd bland enheter (t.ex. en autentiseringsapp). Återställningskod Kom ihåg mig @@ -231,7 +231,7 @@ Vänligen använd en enhet som stöds och/eller lägg till fler metoder som har Kunde inte skicka verifierings-mejl. Försök igen. Verifieringsmeddelande har skickats För att fortsätta, håll din YubiKey NEO mot baksidan av enheten eller sätt i din YubiKey i USB-porten på din enhet, rör sedan knappen. - YubiKey säkerhetsnyckel + YubiKey-säkerhetsnyckel Lägg till ny bilaga Bilagor Kunde ej hämta fil. @@ -366,7 +366,7 @@ Skanningen sker automatiskt. Ja, och spara Fyll i automatiskt och spara Organisation - Håll din Yubikey nära ovansidan av enheten. + Håll din Yubikey nära toppen av enheten. Försök igen För att fortsätta, håll din YubiKey NEO mot baksidan av enheten. Tillgänglighetstjänsten kan vara användbar när appar inte stöder standardvarianten av ifyllnad. @@ -422,7 +422,7 @@ Skanningen sker automatiskt. Inga organisationer att lista. Välj en organisation som du vill flytta detta objektet till. Flytt till en organisation överför ägandet av objektet till den organisationen. Du kommer inte längre att vara direkt ägare till detta objekt när det har flyttats. Antal ord - Lösenordsfras + Lösenfras Ordavgränsare Rensa Generator @@ -438,8 +438,8 @@ Skanningen sker automatiskt. 30 minuter Ange en PIN-kod att låsa upp Bitwarden med. Dina PIN-kodsinställningar återställs om du någonsin loggar ut helt från appen. Inloggad som %1$s på %2$s. - Valvet är låst. Bekräfta ditt huvudlösenord för att fortsätta. - Valvet är låst. Bekräfta din PIN-kod för att fortsätta. + Ditt valv är låst. Bekräfta ditt huvudlösenord för att fortsätta. + Ditt valv är låst. Bekräfta din PIN-kod för att fortsätta. Ditt valv är låst. Bekräfta din identitet för att fortsätta. Mörkt Ljust @@ -456,7 +456,7 @@ Skanningen sker automatiskt. Ändra appens färgtema. Standard (System) Standard mörkt tema - Kopiera anteckningar + Kopiera anteckning Avsluta Är du säker på att du vill avsluta Bitwarden? Require master password on app restart? @@ -522,7 +522,7 @@ Skanningen sker automatiskt. Logga in snabbt genom organisationens inloggningsportal. Ange organisationens identifierare för att börja. Organisationens identifierare Det gick för närvarande inte att logga in med SSO - Ange huvudlösenord + Ställ in huvudlösenord För att slutföra inloggning med SSO, ange ett huvudlösenord för att komma åt och skydda ditt valv. En eller flera organisationspolicyer kräver att ditt huvudlösenord uppfyller följande krav: Minsta komplexitetspoäng på %1$s @@ -590,13 +590,13 @@ Skanningen sker automatiskt. Privata anteckningar om denna Send. Inaktivera denna Send så att ingen kan komma åt den Det finns inga Send på ditt konto. - Lägg till en Send + Ny Send Kopiera länk Dela länk Send-länk Sök bland Send Redigera Send - Skapa ny Send + Ny Send Är du säker på att du vill radera denna Send? Send har raderats Send har sparats @@ -611,7 +611,7 @@ Skanningen sker automatiskt. Add this authenticator key to an existing login, or create a new login. På grund av en företagspolicy kan du bara radera en befintlig Send. Om Send - Dölj min e-postadress för mottagare. + Dölj min e-postadress för mottagare En eller flera organisationsriktlinjer påverkar dina Send-inställningar. Gratiskonton är begränsade till att endast dela text. Ett premium-medlemskap krävs för att använda filer med Send. Du måste verifiera din e-postadress för att använda filer med Send. Du kan verifiera din e-postadress i webbvalvet. @@ -619,7 +619,7 @@ Skanningen sker automatiskt. Bekräfta huvudlösenord Denna åtgärd är skyddad. För att fortsätta, vänligen verifiera din identitet genom att ange ditt huvudlösenord. Captcha krävs - Captcha misslyckades. Vänligen försök igen. + Captcha misslyckades. Försök igen. Huvudlösenord uppdaterades Uppdatera huvudlösenord Ditt huvudlösenord ändrades nyligen av en administratör i din organisation. För att få tillgång till valvet måste du uppdatera ditt huvudlösenord nu. Om du fortsätter kommer du att loggas ut från din nuvarande session, vilket kräver att du loggar in igen. Aktiva sessioner på andra enheter kan komma att vara aktiva i upp till en timme. @@ -667,12 +667,12 @@ Skanningen sker automatiskt. En verifieringskod skickades till din e-postadress Ett fel inträffade när en verifieringskod skulle skickas till din e-post. Försök igen Ange verifieringskoden som skickades till din e-postadress - Skicka in kraschloggar + Skicka kraschloggar Hjälp Bitwarden att förbättra appens stabilitet genom att skicka in kraschrapporter. Alternativen är utvidgade, tryck för att dölja. Alternativen är dolda, tryck för att utvidga. - Versaler (A till Ö) - Gemener (A till Ö) + Versaler (A till Z) + Gemener (A till Z) Siffror (0 till 9) Specialtecken (!@#$%^&*) Tryck för att gå tillbaka @@ -703,13 +703,13 @@ välj Lägg till TOTP för att lagra nyckeln på ett säkert sätt Inloggning begärd Försöker du logga in? Inloggningsförsök av %1$s på %2$s - Typ av enhet + Enhetstyp IP-adress Tid Nära Bekräfta inloggning Neka inloggning - Nyss + Just nu %1$s minuter sedan Inloggning bekräftad Inloggning nekad @@ -729,7 +729,7 @@ välj Lägg till TOTP för att lagra nyckeln på ett säkert sätt Slumpmässigt ord E-post (obligatoriskt) Domännamn (obligatoriskt) - API-nyckel (krävs) + API-nyckel (obligatoriskt) Tjänst addy.io Firefox Relay @@ -739,9 +739,9 @@ välj Lägg till TOTP för att lagra nyckeln på ett säkert sätt ForwardEmail API-åtkomsttoken Är du säker på att du vill ersätta nuvarande användarnamn? - Skapa användarnamn + Generera användarnamn E-posttyp - Webbplats (krävs) + Webbplats (obligatoriskt) Okänt %1$s fel uppstod. Använd din e-postleverantörs funktioner för underadress Använd din domäns konfigurerade catch-all inkorg. @@ -761,7 +761,7 @@ Vill du byta till detta konto? Loggar in som %1$s på %2$s Är det inte du? Logga in med huvudlösenord - Logga in med en annan enhet + Logga in med enhet Inloggning påbörjad En avisering har skickats till din enhet. Se till att ditt valv är upplåst och att fingeravtrycksfrasen matchar på den andra enheten. @@ -849,7 +849,7 @@ Vill du byta till detta konto? Utseende Kontosäkerhet Bitwarden Hjälpcenter - Kontakta Bitwarden support + Kontakta Bitwarden-support Kopiera appinformation Synkronisera nu Upplåsning @@ -861,7 +861,7 @@ Vill du byta till detta konto? %1$s timmar och en minut %1$s timmar och %2$s minuter %1$s timmar - Använd Bitwarden för att spara nya lösennycklar och logga in med lösennycklar sparade i ditt valv. + Använd Bitwarden för att spara nya nycklar och logga in med nycklar lagrade i ditt valv. Androids autofyll-ramverk används för att fylla i inloggningsuppgifter i appar på din enhet. Använd autofyll inbyggt i tangentbordet om ditt tangentbord stöder det. Annars, använd popup-fönstret. Ytterligare alternativ @@ -869,7 +869,7 @@ Vill du byta till detta konto? Fortsätt till %1$s? Fortsätt till Hjälpcenter? Fortsätt att kontakta supporten? - Fortsätt till integritetspolicy? + Fortsätt till Integritetspolicy? Fortsätt till appbutiken? Fortsätt till enhetsinställningar? Gör ditt konto säkrare genom att konfigurera tvåstegsverifiering i Bitwardens webbapp. @@ -953,7 +953,7 @@ Vill du byta till detta konto? Eller logga in, du kanske redan har ett konto. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -961,7 +961,7 @@ Vill du byta till detta konto? Use the generator to create and save strong, unique passwords for all your accounts. Your data, when and where you need it Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps. - Remove passkey + Ta bort nyckel Passkey removed What makes a password strong? The longer your password, the more difficult it is to hack. The minimum for account creation is 12 characters but if you do 14 characters, the time to crack your password would be centuries! @@ -972,9 +972,9 @@ Vill du byta till detta konto? Use the generator to create a strong, unique password Try it out Account setup - Set up unlock + Ställ in upplåsning Ställ in senare - Set up unlock later? + Ställ in upplåsning senare? You can return to complete this step anytime from Account Security in Settings. Bekräfta Set up biometrics or choose a PIN code to quickly access your vault and AutoFill your logins. @@ -1023,11 +1023,11 @@ Vill du byta till detta konto? Importera sparade inloggningar Use a computer to import logins from an existing password manager You can return to complete this step anytime in Vault Settings. - Import logins later - Import logins later? + Importera inloggningar senare + Importera inloggningar senare? Do you have a computer available? The following instructions will guide you through importing logins from your desktop or laptop computer - Import Logins + Importera inloggningar From your computer, follow these instructions to export saved passwords from your browser or other password manager. Then, safely import them to Bitwarden. Give your vault a head start Unlock with biometrics requires strong biometric authentication and may not be compatible with all biometric options on this device. @@ -1035,13 +1035,13 @@ Vill du byta till detta konto? On your computer, log in to your current browser or password manager. log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. - Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Exportera dina lösenord. + Select Import data in the web app, then Done below to finish syncing. Select Import data Steg 1 av 3 Export your saved logins You’ll delete this file after import is complete. - On your computer, open a new browser tab and go to %1$s + Öppna en ny webbläsarflik på din dator och gå till %1$s gå till %1$s Logga in på Bitwardens webbapp. Steg 2 av 3 @@ -1078,13 +1078,14 @@ Vill du byta till detta konto? Logins imported Remember to delete your imported password file from your computer SSH-nyckel - Public key + Offentlig nyckel Privat nyckel SSH-nycklar - Copy public key + Kopiera offentlig nyckel Kopiera fingeravtryck Enable notifications Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Klar diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml index 186301d30..5e491206a 100644 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -952,7 +952,7 @@ Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1017,8 +1017,8 @@ Save and protect your data The vault protects more than just passwords. Store secure logins, IDs, cards and notes securely here. New login - Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure. - Send sensitive information, safely + கோப்புகள் மற்றும் தரவை யாருடனும், எத்தளத்திலும் பாதுகாப்பாகப் பகிர்க. வெளிப்படுதலைக் கட்டுப்படுத்தி உமது தகவல் இருமுனை மறையாக்கம் செய்யப்பட்டிருக்கும். + பாதுகாப்பாக மறைமுகத் தகவலை அனுப்புக Import saved logins Use a computer to import logins from an existing password manager You can return to complete this step anytime in Vault Settings. @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-te-rIN/strings.xml b/app/src/main/res/values-te-rIN/strings.xml index 637919302..cfbdae808 100644 --- a/app/src/main/res/values-te-rIN/strings.xml +++ b/app/src/main/res/values-te-rIN/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-th-rTH/strings.xml b/app/src/main/res/values-th-rTH/strings.xml index 6a06adcf5..24617d9a3 100644 --- a/app/src/main/res/values-th-rTH/strings.xml +++ b/app/src/main/res/values-th-rTH/strings.xml @@ -952,7 +952,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ Do you want to switch to this account? log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Do you want to switch to this account? Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 4a0172079..90f0f0cb3 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -951,7 +951,7 @@ Bu hesaba geçmek ister misiniz? Veya zaten hesabınız varsa giriş yapın. Bitwarden\'dan duyurular, öneriler ve araştırmalarla ilgili e-postalar alın. İstediğiniz zaman aboneliğinizi iptal edebilirsiniz. Bitwarden\'dan öneriler, duyurular ve araştırma fırsatları e-posta adresinize gelsin. İstediğiniz zaman aboneliğinizi iptal edebilirsiniz. - Önce gizlilik + Önce gizlilik Hesaplarınızı, kartlarınızı ve kimliklerinizi güvenli kasanıza kaydedin. Bitwarden\'ın sıfır bilgi ispatlı uçtan uca şifrelemesi sizin için önemli olan her şeyi korur. Hesaplarınıza parola yazmadan giriş yapmak için biyometrik kilit açmayı ayarlayabilirsiniz. Hızlı ve kolay giriş @@ -1034,7 +1034,7 @@ Bu hesaba geçmek ister misiniz? mevcut tarayıcınızı veya parola yöneticinizi açın. Parolalarınızı dışa aktarın. Bu seçenek genellikle ayarlarınızda bulunur. Parolalarınızı dışa aktarın. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1085,4 +1085,5 @@ Bu hesaba geçmek ister misiniz? Farklı cihazlarda hızlıca ve kolayca oturum açın Başka bir cihazdan yeni bir giriş isteği aldığınızda Bitwarden size bildirim gönderebilir. Şimdilik geç + Tamam diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index 9401eba57..7176cc5e4 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -952,7 +952,7 @@ Або увійдіть, якщо вже маєте обліковий запис. Отримуйте електронні листи від Bitwarden з оголошеннями, порадами та інформацією про нові можливості. Відписатися можна будь-коли. Отримуйте поради, оголошення та інформацію щодо можливості участі в дослідженнях від Bitwarden у вашій поштовій скриньці. Відпишіться коли завгодно. - Приватність понад усе + Приватність понад усе Зберігайте записи входу, картки та посвідчення в захищеному сховищі. Для захисту ваших даних Bitwarden використовує наскрізне шифрування з нульовим рівнем доступу. Налаштуйте біометричне розблокування та автозаповнення, щоб миттєво входити в облікові записи. Швидкий і легкий вхід @@ -1086,4 +1086,5 @@ Швидко й легко виконуйте вхід на різних пристроях Bitwarden може сповіщати вас щоразу, коли ви отримуєте новий запит на вхід з іншого пристрою. Наразі пропустити + Виконано diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index 1ef7ba1d8..2358b529e 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -952,7 +952,7 @@ Bạn có muốn chuyển sang tài khoản này không? Hoặc đăng nhập, nếu bạn đã có tài khoản. Nhận email từ Bitwarden để nhận thông báo, đề xuất và cơ hội nghiên cứu. Hủy đăng ký bất cứ lúc nào. Nhận đề xuất, thông báo và cơ hội nghiên cứu từ Bitwarden trong hộp thư đến của bạn. Hủy đăng ký bất cứ lúc nào. - Bảo mật, riêng tư + Bảo mật, riêng tư Lưu thông tin đăng nhập, thẻ tín dụng và danh tính vào kho lưu trữ an toàn của bạn. Bitwarden sử dụng mã hóa đầu cuối không cần kiến ​​thức để bảo vệ những gì quan trọng với bạn. Thiết lập mở khóa sinh trắc học và tự động điền để đăng nhập vào tài khoản của bạn mà không cần nhập tay. Đăng nhập nhanh và dễ dàng @@ -1086,4 +1086,5 @@ Bạn có muốn chuyển sang tài khoản này không? Đăng nhập nhanh và dễ dàng giữa các thiết bị Bitwarden có thể thông báo cho bạn mỗi khi bạn nhận được yêu cầu đăng nhập mới từ thiết bị khác. Bỏ qua + Xong diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 37654f8b4..41ef820a9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -952,7 +952,7 @@ 或者登录,您可能已经有一个账户了。 获取来自 Bitwarden 的建议、公告和调研电子邮件。随时退订。 获取来自 Bitwarden 的建议、公告和调研电子邮件。随时退订。 - 隐私优先 + 隐私优先 将登录、支付卡和身份保存到您的安全密码库。Bitwarden 使用零知识、端到端的加密来保护您的重要信息。 设置生物识别解锁和自动填充,无需输入任何字符即可登录您的账户。 快速便捷的登录 @@ -1086,4 +1086,5 @@ 快速和轻松地跨设备登录 Bitwarden 可以在每次收到来自其他设备的新登录请求时通知您。 暂时跳过 + 完成 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 04fd3d030..2e2802d17 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -952,7 +952,7 @@ Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Privacy, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1035,7 +1035,7 @@ log in to your current browser or password manager. Export your passwords. This option is usually found in your settings. Export your passwords. - Select Import data in the web app, then Done to finish syncing. + Select Import data in the web app, then Done below to finish syncing. Select Import data Step 1 of 3 Export your saved logins @@ -1086,4 +1086,5 @@ Log in quickly and easily across devices Bitwarden can notify you each time you receive a new login request from another device. Skip for now + Done diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 934e4da65..426bcbcce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,7 +40,6 @@ Invalid master password. Try again. Invalid PIN. Try again. Launch - Log In Log in Login Log out @@ -952,7 +951,7 @@ Do you want to switch to this account? Or log in, you may already have an account. Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time. Get advice, announcements, and research opportunities from Bitwarden in your inbox. Unsubscribe at any time. - Privacy, prioritized + Security, prioritized Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you. Set up biometric unlock and autofill to log into your accounts without typing a single letter. Quick and easy login @@ -1087,4 +1086,13 @@ Do you want to switch to this account? Bitwarden can notify you each time you receive a new login request from another device. Skip for now Done + %1$s of %2$s + Copy identity name + Copy company + Copy social security number + Copy passport number + Copy license number + Copy email + Copy phone number + Copy address diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 3d6072287..716b0ae6a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -244,6 +244,8 @@ class AuthDiskSourceTest { val pendingAuthRequestJson = PendingAuthRequestJson( requestId = "12345", requestPrivateKey = "67890", + requestFingerprint = "fingerprint", + requestAccessCode = "accessCode", ) authDiskSource.storePendingAuthRequest( userId = userId, @@ -594,15 +596,22 @@ class AuthDiskSourceTest { pendingAdminAuthRequestKey, """ { - "Id": "12345", - "PrivateKey": "67890" + "id": "12345", + "privateKey": "67890", + "fingerprint": "fingerprint", + "accessCode": "accessCode" } """, ) } val actual = authDiskSource.getPendingAuthRequest(userId = mockUserId) assertEquals( - PendingAuthRequestJson(requestId = "12345", requestPrivateKey = "67890"), + PendingAuthRequestJson( + requestId = "12345", + requestPrivateKey = "67890", + requestFingerprint = "fingerprint", + requestAccessCode = "accessCode", + ), actual, ) } @@ -615,6 +624,8 @@ class AuthDiskSourceTest { val pendingAdminAuthRequest = PendingAuthRequestJson( requestId = "12345", requestPrivateKey = "67890", + requestFingerprint = "fingerprint", + requestAccessCode = "accessCode", ) authDiskSource.storePendingAuthRequest( userId = mockUserId, @@ -628,8 +639,10 @@ class AuthDiskSourceTest { json.parseToJsonElement( """ { - "Id": "12345", - "PrivateKey": "67890" + "id": "12345", + "privateKey": "67890", + "fingerprint": "fingerprint", + "accessCode": "accessCode" } """ .trimIndent(), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt index 90d64d205..30d4fa39b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson @@ -370,14 +371,19 @@ class IdentityServiceTest : BaseServiceTest() { runTest { server.enqueue(MockResponse().setResponseCode(200).setBody(EMAIL_TOKEN)) val result = identityService.sendVerificationEmail(SEND_VERIFICATION_EMAIL_REQUEST) - assertEquals(JsonPrimitive(EMAIL_TOKEN).content.asSuccess(), result) + assertEquals( + SendVerificationEmailResponseJson + .Success(JsonPrimitive(EMAIL_TOKEN).content) + .asSuccess(), + result, + ) } @Test fun `sendVerificationEmail should return null when response is empty success`() = runTest { server.enqueue(MockResponse().setResponseCode(204)) val result = identityService.sendVerificationEmail(SEND_VERIFICATION_EMAIL_REQUEST) - assertEquals(null.asSuccess(), result) + assertEquals(SendVerificationEmailResponseJson.Success(null).asSuccess(), result) } @Test @@ -422,7 +428,6 @@ class IdentityServiceTest : BaseServiceTest() { ) } - @Suppress("MaxLineLength") @Test fun `verifyEmailToken should return Invalid when response message is non expired error`() = runTest { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt index 3afd85cba..88b5ca677 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthRequestManagerTest.kt @@ -162,7 +162,8 @@ class AuthRequestManagerTest { assertEquals( CreateAuthRequestResult.Success( authRequest = authRequest.copy(requestApproved = true), - authRequestResponse = authRequestResponse, + privateKey = authRequestResponse.privateKey, + accessCode = authRequestResponse.accessCode, ), awaitItem(), ) @@ -170,6 +171,87 @@ class AuthRequestManagerTest { } } + @Suppress("MaxLineLength") + @Test + fun `createAuthRequestWithUpdates with a pending admin request Success and getAuthRequestUpdate with approval should emit Success`() = + runTest { + val email = "email@email.com" + val authRequestResponse = AUTH_REQUEST_RESPONSE + val authRequestResponseJson = AuthRequestsResponseJson.AuthRequest( + id = "1", + publicKey = PUBLIC_KEY, + platform = "Android", + ipAddress = "192.168.0.1", + key = "public", + masterPasswordHash = "verySecureHash", + creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"), + responseDate = null, + requestApproved = false, + originUrl = "www.bitwarden.com", + ) + fakeAuthDiskSource.userState = SINGLE_USER_STATE + fakeAuthDiskSource.storePendingAuthRequest( + userId = USER_ID, + pendingAuthRequest = PendingAuthRequestJson( + requestId = authRequestResponseJson.id, + requestPrivateKey = authRequestResponse.privateKey, + requestAccessCode = authRequestResponse.accessCode, + requestFingerprint = authRequestResponse.fingerprint, + ), + ) + val updatedAuthRequestResponseJson = authRequestResponseJson.copy( + requestApproved = true, + ) + val authRequest = AuthRequest( + id = authRequestResponseJson.id, + publicKey = authRequestResponseJson.publicKey, + platform = authRequestResponseJson.platform, + ipAddress = authRequestResponseJson.ipAddress, + key = authRequestResponseJson.key, + masterPasswordHash = authRequestResponseJson.masterPasswordHash, + creationDate = authRequestResponseJson.creationDate, + responseDate = authRequestResponseJson.responseDate, + requestApproved = authRequestResponseJson.requestApproved ?: false, + originUrl = authRequestResponseJson.originUrl, + fingerprint = authRequestResponse.fingerprint, + ) + coEvery { + authRequestsService.getAuthRequest( + requestId = authRequest.id, + ) + } returns authRequestResponseJson.asSuccess() + coEvery { + newAuthRequestService.getAuthRequestUpdate( + requestId = authRequest.id, + accessCode = authRequestResponse.accessCode, + isSso = true, + ) + } returnsMany listOf( + authRequestResponseJson.asSuccess(), + updatedAuthRequestResponseJson.asSuccess(), + ) + + repository + .createAuthRequestWithUpdates( + email = email, + authRequestType = AuthRequestType.SSO_ADMIN_APPROVAL, + ) + .test { + assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem()) + assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem()) + assertEquals( + CreateAuthRequestResult.Success( + authRequest = authRequest.copy(requestApproved = true), + privateKey = authRequestResponse.privateKey, + accessCode = authRequestResponse.accessCode, + ), + awaitItem(), + ) + awaitComplete() + } + coVerify(exactly = 0) { authSdkSource.getNewAuthRequest(any()) } + } + @Suppress("MaxLineLength") @Test fun `createAuthRequestWithUpdates with createNewAuthRequest Success and getAuthRequestUpdate with response date and no approval should emit Declined`() = @@ -305,6 +387,8 @@ class AuthRequestManagerTest { pendingAuthRequest = PendingAuthRequestJson( requestId = authRequestResponseJson.id, requestPrivateKey = authRequestResponse.privateKey, + requestAccessCode = authRequestResponse.accessCode, + requestFingerprint = authRequestResponse.fingerprint, ), ) assertEquals(CreateAuthRequestResult.Expired, awaitItem()) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 887c7139a..3aae102b4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJs import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod @@ -123,7 +124,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult @@ -3566,6 +3566,8 @@ class AuthRepositoryTest { val pendingAuthRequest = PendingAuthRequestJson( requestId = "requestId", requestPrivateKey = "requestPrivateKey", + requestFingerprint = "fingerprint", + requestAccessCode = "accessCode", ) val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy( key = null, @@ -5826,33 +5828,11 @@ class AuthRepositoryTest { ) } - @Test - fun `validatePin returns ValidatePinResult Error when no private key found`() = runTest { - val pin = "PIN" - fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 - fakeAuthDiskSource.storePrivateKey( - userId = SINGLE_USER_STATE_1.activeUserId, - privateKey = null, - ) - - val result = repository.validatePin(pin = pin) - - assertEquals( - ValidatePinResult.Error, - result, - ) - } - @Test fun `validatePin returns ValidatePinResult Error when no pin protected user key found`() = runTest { val pin = "PIN" - val privateKey = "privateKey" fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 - fakeAuthDiskSource.storePrivateKey( - userId = SINGLE_USER_STATE_1.activeUserId, - privateKey = privateKey, - ) fakeAuthDiskSource.storePinProtectedUserKey( userId = SINGLE_USER_STATE_1.activeUserId, pinProtectedUserKey = null, @@ -5867,23 +5847,19 @@ class AuthRepositoryTest { } @Test - fun `validatePin returns ValidatePinResult Error when initialize crypto fails`() = runTest { + fun `validatePin returns ValidatePinResult Error when SDK validatePin fails`() = runTest { val pin = "PIN" - val privateKey = "privateKey" val pinProtectedUserKey = "pinProtectedUserKey" fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 - fakeAuthDiskSource.storePrivateKey( - userId = SINGLE_USER_STATE_1.activeUserId, - privateKey = privateKey, - ) fakeAuthDiskSource.storePinProtectedUserKey( userId = SINGLE_USER_STATE_1.activeUserId, pinProtectedUserKey = pinProtectedUserKey, ) coEvery { - vaultSdkSource.initializeCrypto( + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) } returns Throwable().asFailure() @@ -5893,36 +5869,33 @@ class AuthRepositoryTest { ValidatePinResult.Error, result, ) - coVerify { - vaultSdkSource.initializeCrypto( + coVerify(exactly = 1) { + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) } } @Suppress("MaxLineLength") @Test - fun `validatePin returns ValidatePinResult Success with valid false when initialize cryto returns AuthenticationError`() = + fun `validatePin returns ValidatePinResult Success with valid false when SDK validatePin returns false`() = runTest { val pin = "PIN" - val privateKey = "privateKey" val pinProtectedUserKey = "pinProtectedUserKey" fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 - fakeAuthDiskSource.storePrivateKey( - userId = SINGLE_USER_STATE_1.activeUserId, - privateKey = privateKey, - ) fakeAuthDiskSource.storePinProtectedUserKey( userId = SINGLE_USER_STATE_1.activeUserId, pinProtectedUserKey = pinProtectedUserKey, ) coEvery { - vaultSdkSource.initializeCrypto( + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) - } returns InitializeCryptoResult.AuthenticationError().asSuccess() + } returns false.asSuccess() val result = repository.validatePin(pin = pin) @@ -5930,36 +5903,33 @@ class AuthRepositoryTest { ValidatePinResult.Success(isValid = false), result, ) - coVerify { - vaultSdkSource.initializeCrypto( + coVerify(exactly = 1) { + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) } } @Suppress("MaxLineLength") @Test - fun `validatePin returns ValidatePinResult Success with valid true when initialize cryto returns Success`() = + fun `validatePin returns ValidatePinResult Success with valid true when SDK validatePin returns true`() = runTest { val pin = "PIN" - val privateKey = "privateKey" val pinProtectedUserKey = "pinProtectedUserKey" fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 - fakeAuthDiskSource.storePrivateKey( - userId = SINGLE_USER_STATE_1.activeUserId, - privateKey = privateKey, - ) fakeAuthDiskSource.storePinProtectedUserKey( userId = SINGLE_USER_STATE_1.activeUserId, pinProtectedUserKey = pinProtectedUserKey, ) coEvery { - vaultSdkSource.initializeCrypto( + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) - } returns InitializeCryptoResult.Success.asSuccess() + } returns true.asSuccess() val result = repository.validatePin(pin = pin) @@ -5967,10 +5937,11 @@ class AuthRepositoryTest { ValidatePinResult.Success(isValid = true), result, ) - coVerify { - vaultSdkSource.initializeCrypto( + coVerify(exactly = 1) { + vaultSdkSource.validatePin( userId = SINGLE_USER_STATE_1.activeUserId, - request = any(), + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, ) } } @@ -6073,7 +6044,7 @@ class AuthRepositoryTest { receiveMarketingEmails = true, ), ) - } returns EMAIL_VERIFICATION_TOKEN.asSuccess() + } returns SendVerificationEmailResponseJson.Success(EMAIL_VERIFICATION_TOKEN).asSuccess() val result = repository.sendVerificationEmail( email = EMAIL, @@ -6086,6 +6057,32 @@ class AuthRepositoryTest { ) } + @Test + fun `sendVerificationEmail success with invalid email should return error`() = runTest { + val errorMessage = "Failure" + coEvery { + identityService.sendVerificationEmail( + SendVerificationEmailRequestJson( + email = EMAIL, + name = NAME, + receiveMarketingEmails = true, + ), + ) + } returns SendVerificationEmailResponseJson + .Invalid(invalidMessage = errorMessage, validationErrors = null) + .asSuccess() + + val result = repository.sendVerificationEmail( + email = EMAIL, + name = NAME, + receiveMarketingEmails = true, + ) + assertEquals( + SendVerificationEmailResult.Error(errorMessage = errorMessage), + result, + ) + } + @Test fun `sendVerificationEmail failure should return success if body null`() = runTest { coEvery { @@ -6096,7 +6093,7 @@ class AuthRepositoryTest { receiveMarketingEmails = true, ), ) - } returns null.asSuccess() + } returns SendVerificationEmailResponseJson.Success(null).asSuccess() val result = repository.sendVerificationEmail( email = EMAIL, @@ -6109,6 +6106,29 @@ class AuthRepositoryTest { ) } + @Test + fun `sendVerificationEmail with empty name should use null and return success`() = runTest { + coEvery { + identityService.sendVerificationEmail( + SendVerificationEmailRequestJson( + email = EMAIL, + name = null, + receiveMarketingEmails = true, + ), + ) + } returns SendVerificationEmailResponseJson.Success(EMAIL_VERIFICATION_TOKEN).asSuccess() + + val result = repository.sendVerificationEmail( + email = EMAIL, + name = "", + receiveMarketingEmails = true, + ) + assertEquals( + SendVerificationEmailResult.Success(EMAIL_VERIFICATION_TOKEN), + result, + ) + } + @Test fun `sendVerificationEmail failure should return error`() = runTest { coEvery { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerTest.kt deleted file mode 100644 index d8c5e63bd..000000000 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityActivityManagerTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.x8bit.bitwarden.data.autofill.accessibility.manager - -import android.content.Context -import androidx.lifecycle.LifecycleCoroutineScope -import app.cash.turbine.test -import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled -import com.x8bit.bitwarden.data.platform.manager.AppStateManager -import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class AccessibilityActivityManagerTest { - private val context: Context = mockk() - private val accessibilityEnabledManager: AccessibilityEnabledManager = - AccessibilityEnabledManagerImpl() - private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) - private val appStateManager: AppStateManager = mockk { - every { appForegroundStateFlow } returns mutableAppForegroundStateFlow - } - private val lifecycleScope = mockk { - every { coroutineContext } returns UnconfinedTestDispatcher() - } - - // We will construct an instance here just to hook the various dependencies together internally - private lateinit var autofillActivityManager: AccessibilityActivityManager - - @BeforeEach - fun setup() { - mockkStatic(Context::isAccessibilityServiceEnabled) - every { context.isAccessibilityServiceEnabled } returns false - autofillActivityManager = AccessibilityActivityManagerImpl( - context = context, - accessibilityEnabledManager = accessibilityEnabledManager, - appStateManager = appStateManager, - lifecycleScope = lifecycleScope, - ) - } - - @AfterEach - fun tearDown() { - unmockkStatic(Context::isAccessibilityServiceEnabled) - } - - @Test - fun `changes in app foreground status should update the AutofillEnabledManager as necessary`() = - runTest { - accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test { - assertFalse(awaitItem()) - - // An update is received when both the accessibility state and foreground state - // change - every { context.isAccessibilityServiceEnabled } returns true - mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED - assertTrue(awaitItem()) - - // An update is not received when only the foreground state changes - mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED - expectNoEvents() - - // An update is not received when only the accessibility state changes - every { context.isAccessibilityServiceEnabled } returns false - expectNoEvents() - - // An update is received after both states have changed - mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED - assertFalse(awaitItem()) - } - } -} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt index 716b27f0e..77a7ab45d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityEnabledManagerTest.kt @@ -1,6 +1,10 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager +import android.view.accessibility.AccessibilityManager import app.cash.turbine.test +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -8,23 +12,33 @@ import org.junit.jupiter.api.Test class AccessibilityEnabledManagerTest { + private val accessibilityStateChangeListener = + slot() + private val accessibilityManager = mockk { + every { + addAccessibilityStateChangeListener(capture(accessibilityStateChangeListener)) + } returns true + } + private val accessibilityEnabledManager: AccessibilityEnabledManager = - AccessibilityEnabledManagerImpl() + AccessibilityEnabledManagerImpl( + accessibilityManager = accessibilityManager, + ) @Suppress("MaxLineLength") @Test - fun `isAccessibilityEnabledStateFlow should emit whenever isAccessibilityEnabled is set to a unique value`() = + fun `isAccessibilityEnabledStateFlow should emit whenever accessibilityStateChangeListener emits a unique value`() = runTest { accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test { assertFalse(awaitItem()) - accessibilityEnabledManager.isAccessibilityEnabled = true + accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true) assertTrue(awaitItem()) - accessibilityEnabledManager.isAccessibilityEnabled = true + accessibilityStateChangeListener.captured.onAccessibilityStateChanged(true) expectNoEvents() - accessibilityEnabledManager.isAccessibilityEnabled = false + accessibilityStateChangeListener.captured.onAccessibilityStateChanged(false) assertFalse(awaitItem()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt new file mode 100644 index 000000000..d35d1dab1 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/FakeAccessibilityEnabledManager.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeAccessibilityEnabledManager : AccessibilityEnabledManager { + + private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false) + + override val isAccessibilityEnabledStateFlow: StateFlow + get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() + + var isAccessibilityEnabled: Boolean + get() = mutableIsAccessibilityEnabledStateFlow.value + set(value) { + mutableIsAccessibilityEnabledStateFlow.value = value + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt index 19c682ecc..c7ed53e8c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor import android.content.Context import android.net.Uri import android.os.PowerManager +import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.widget.Toast import com.bitwarden.vault.CipherView @@ -74,10 +75,12 @@ class BitwardenAccessibilityProcessorTest { } @Test - fun `processAccessibilityEvent with null rootNode should return`() { - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = null, - ) + fun `processAccessibilityEvent with null event source should return`() { + val event = mockk { + every { source } returns null + } + + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 0) { powerManager.isInteractive @@ -86,12 +89,12 @@ class BitwardenAccessibilityProcessorTest { @Test fun `processAccessibilityEvent with powerManager not interactive should return`() { - val rootNode = mockk() + val event = mockk { + every { source } returns mockk() + } every { powerManager.isInteractive } returns false - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 1) { powerManager.isInteractive @@ -100,87 +103,121 @@ class BitwardenAccessibilityProcessorTest { @Test fun `processAccessibilityEvent with system package should return`() { - val rootNode = mockk { + val node = mockk { every { isSystemPackage } returns true } + val event = mockk { + every { source } returns node + } every { powerManager.isInteractive } returns true - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage + node.isSystemPackage } } @Test fun `processAccessibilityEvent with skippable package should return`() { - val rootNode = mockk { + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns true } + val event = mockk { + every { source } returns node + } every { powerManager.isInteractive } returns true - every { accessibilityAutofillManager.accessibilityAction = null } just runs - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage - accessibilityAutofillManager.accessibilityAction = null + node.isSystemPackage + node.shouldSkipPackage } } @Test fun `processAccessibilityEvent with launcher package should return`() { val testPackageName = "com.google.android.launcher" - val rootNode = mockk { + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + } every { launcherPackageNameManager.launcherPackages } returns listOf(testPackageName) every { powerManager.isInteractive } returns true - every { accessibilityAutofillManager.accessibilityAction = null } just runs - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages - accessibilityAutofillManager.accessibilityAction = null } } @Test fun `processAccessibilityEvent without accessibility action should return`() { val testPackageName = "com.android.chrome" - val rootNode = mockk { + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + } every { launcherPackageNameManager.launcherPackages } returns emptyList() every { accessibilityAutofillManager.accessibilityAction } returns null every { powerManager.isInteractive } returns true - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { null } verify(exactly = 1) { powerManager.isInteractive - rootNode.shouldSkipPackage - rootNode.isSystemPackage + node.shouldSkipPackage + node.isSystemPackage + launcherPackageNameManager.launcherPackages + accessibilityAutofillManager.accessibilityAction + } + } + + @Test + fun `processAccessibilityEvent with mismatched package name should return`() { + val testPackageName = "com.android.chrome" + val rootNode = mockk { + every { packageName } returns "other.package.name" + } + val node = mockk { + every { isSystemPackage } returns false + every { shouldSkipPackage } returns false + every { packageName } returns testPackageName + } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } + every { powerManager.isInteractive } returns true + every { launcherPackageNameManager.launcherPackages } returns emptyList() + every { + accessibilityAutofillManager.accessibilityAction + } returns AccessibilityAction.AttemptParseUri + every { accessibilityAutofillManager.accessibilityAction = null } just runs + every { accessibilityParser.parseForUriOrPackageName(rootNode = node) } returns null + + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } + + verify(exactly = 1) { + powerManager.isInteractive + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages accessibilityAutofillManager.accessibilityAction } @@ -190,30 +227,35 @@ class BitwardenAccessibilityProcessorTest { fun `processAccessibilityEvent with AttemptParseUri and a invalid uri should show a toast`() { val testPackageName = "com.android.chrome" val rootNode = mockk { + every { packageName } returns testPackageName + } + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } every { powerManager.isInteractive } returns true every { launcherPackageNameManager.launcherPackages } returns emptyList() every { accessibilityAutofillManager.accessibilityAction } returns AccessibilityAction.AttemptParseUri every { accessibilityAutofillManager.accessibilityAction = null } just runs - every { accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) } returns null + every { accessibilityParser.parseForUriOrPackageName(rootNode = node) } returns null - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages accessibilityAutofillManager.accessibilityAction accessibilityAutofillManager.accessibilityAction = null - accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) + accessibilityParser.parseForUriOrPackageName(rootNode = node) Toast .makeText(context, R.string.autofill_tile_uri_not_found, Toast.LENGTH_LONG) .show() @@ -222,13 +264,20 @@ class BitwardenAccessibilityProcessorTest { @Suppress("MaxLineLength") @Test - fun `processAccessibilityEvent with AttemptParseUri and a valid uri should start the main activity`() { + fun `processAccessibilityEvent with AttemptParseUri and a valid uri but no fields to fill display toast`() { val testPackageName = "com.android.chrome" val rootNode = mockk { + every { packageName } returns testPackageName + } + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } every { powerManager.isInteractive } returns true every { launcherPackageNameManager.launcherPackages } returns emptyList() every { @@ -243,20 +292,76 @@ class BitwardenAccessibilityProcessorTest { uri = any(), ) } returns mockk() - every { accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) } returns mockk() + val uri = mockk() + every { accessibilityParser.parseForUriOrPackageName(rootNode = node) } returns uri + every { + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) + } returns mockk { every { hasFields } returns false } - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages accessibilityAutofillManager.accessibilityAction accessibilityAutofillManager.accessibilityAction = null - accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) + accessibilityParser.parseForUriOrPackageName(rootNode = node) + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) + Toast + .makeText(context, R.string.autofill_tile_uri_not_found, Toast.LENGTH_LONG) + .show() + } + } + + @Suppress("MaxLineLength") + @Test + fun `processAccessibilityEvent with AttemptParseUri and a valid uri should start the main activity`() { + val testPackageName = "com.android.chrome" + val rootNode = mockk { + every { packageName } returns testPackageName + } + val node = mockk { + every { isSystemPackage } returns false + every { shouldSkipPackage } returns false + every { packageName } returns testPackageName + } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } + every { powerManager.isInteractive } returns true + every { launcherPackageNameManager.launcherPackages } returns emptyList() + every { + accessibilityAutofillManager.accessibilityAction + } returns AccessibilityAction.AttemptParseUri + every { accessibilityAutofillManager.accessibilityAction = null } just runs + every { + createAutofillSelectionIntent( + context = context, + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + type = AutofillSelectionData.Type.LOGIN, + uri = any(), + ) + } returns mockk() + val uri = mockk() + every { accessibilityParser.parseForUriOrPackageName(rootNode = node) } returns uri + every { + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) + } returns mockk { every { hasFields } returns true } + + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } + + verify(exactly = 1) { + powerManager.isInteractive + node.isSystemPackage + node.shouldSkipPackage + launcherPackageNameManager.launcherPackages + accessibilityAutofillManager.accessibilityAction + accessibilityAutofillManager.accessibilityAction = null + accessibilityParser.parseForUriOrPackageName(rootNode = node) + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) createAutofillSelectionIntent( context = context, framework = AutofillSelectionData.Framework.ACCESSIBILITY, @@ -276,23 +381,28 @@ class BitwardenAccessibilityProcessorTest { val uri = mockk() val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri) val rootNode = mockk { + every { packageName } returns testPackageName + } + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } every { powerManager.isInteractive } returns true every { launcherPackageNameManager.launcherPackages } returns emptyList() every { accessibilityAutofillManager.accessibilityAction } returns attemptFill every { accessibilityAutofillManager.accessibilityAction = null } just runs - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages accessibilityAutofillManager.accessibilityAction accessibilityAutofillManager.accessibilityAction = null @@ -325,31 +435,36 @@ class BitwardenAccessibilityProcessorTest { val uri = mockk() val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri) val rootNode = mockk { + every { packageName } returns testPackageName + } + val node = mockk { every { isSystemPackage } returns false every { shouldSkipPackage } returns false every { packageName } returns testPackageName } + val event = mockk { + every { source } returns node + every { packageName } returns testPackageName + } every { powerManager.isInteractive } returns true every { launcherPackageNameManager.launcherPackages } returns emptyList() every { accessibilityAutofillManager.accessibilityAction } returns attemptFill every { accessibilityAutofillManager.accessibilityAction = null } just runs every { - accessibilityParser.parseForFillableFields(rootNode = rootNode, uri = uri) + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) } returns fillableFields - bitwardenAccessibilityProcessor.processAccessibilityEvent( - rootAccessibilityNodeInfo = rootNode, - ) + bitwardenAccessibilityProcessor.processAccessibilityEvent(event = event) { rootNode } verify(exactly = 1) { powerManager.isInteractive - rootNode.isSystemPackage - rootNode.shouldSkipPackage + node.isSystemPackage + node.shouldSkipPackage launcherPackageNameManager.launcherPackages accessibilityAutofillManager.accessibilityAction accessibilityAutofillManager.accessibilityAction = null cipherView.login - accessibilityParser.parseForFillableFields(rootNode = rootNode, uri = uri) + accessibilityParser.parseForFillableFields(rootNode = node, uri = uri) mockUsernameField.fillTextField(testUsername) mockPasswordField.fillTextField(testPassword) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/base/BaseServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/base/BaseServiceTest.kt index 57bcf0e95..5a8e37a44 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/base/BaseServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/base/BaseServiceTest.kt @@ -1,6 +1,6 @@ package com.x8bit.bitwarden.data.platform.base -import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory +import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCallAdapterFactory import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType @@ -24,7 +24,7 @@ abstract class BaseServiceTest { protected val retrofit: Retrofit = Retrofit.Builder() .baseUrl(url.toString()) - .addCallAdapterFactory(ResultCallAdapterFactory()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory()) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 286e4963d..a7f44baf1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -42,6 +42,9 @@ class FakeSettingsDiskSource : SettingsDiskSource { private val mutableScreenCaptureAllowedFlowMap = mutableMapOf>() + private val mutableLastDatabaseSchemeChangeInstant = + bufferedMutableSharedFlow() + private var storedAppTheme: AppTheme = AppTheme.DEFAULT private val storedLastSyncTime = mutableMapOf() private val storedVaultTimeoutActions = mutableMapOf() @@ -141,6 +144,11 @@ class FakeSettingsDiskSource : SettingsDiskSource { get() = storedLastDatabaseSchemeChangeInstant set(value) { storedLastDatabaseSchemeChangeInstant = value } + override val lastDatabaseSchemeChangeInstantFlow: Flow + get() = mutableLastDatabaseSchemeChangeInstant.onSubscription { + emit(lastDatabaseSchemeChangeInstant) + } + override fun getAccountBiometricIntegrityValidity( userId: String, systemBioIntegrityState: String, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterTest.kt similarity index 78% rename from app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterTest.kt rename to app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterTest.kt index e6688270e..a172e377e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/ResultCallAdapterTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/core/NetworkResultCallAdapterTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.core +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -10,14 +11,14 @@ import retrofit2.Retrofit import retrofit2.create import retrofit2.http.GET -class ResultCallAdapterTest { +class NetworkResultCallAdapterTest { private val server: MockWebServer = MockWebServer().apply { start() } private val testService: FakeService = Retrofit.Builder() .baseUrl(server.url("/").toString()) // add the adapter being tested - .addCallAdapterFactory(ResultCallAdapterFactory()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory()) .build() .create() @@ -30,14 +31,14 @@ class ResultCallAdapterTest { fun `when server returns error response code result should be failure`() = runBlocking { server.enqueue(MockResponse().setResponseCode(500)) val result = testService.requestWithUnitData() - assertTrue(result.isFailure) + assertTrue(result is NetworkResult.Failure) } @Test fun `when server returns successful response result should be success`() = runBlocking { server.enqueue(MockResponse()) val result = testService.requestWithUnitData() - assertTrue(result.isSuccess) + assertTrue(result is NetworkResult.Success) } } @@ -46,5 +47,5 @@ class ResultCallAdapterTest { */ private interface FakeService { @GET("/fake") - suspend fun requestWithUnitData(): Result + suspend fun requestWithUnitData(): NetworkResult } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt index 99e02a9ba..fe9cbb4b0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt @@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.Refres import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -39,7 +40,7 @@ class RetrofitsTest { } } private val headersInterceptors = mockk { - mockIntercept { isheadersInterceptorCalled = true } + mockIntercept { isHeadersInterceptorCalled = true } } private val refreshAuthenticator = mockk { mockAuthenticate { isRefreshAuthenticatorCalled = true } @@ -57,7 +58,7 @@ class RetrofitsTest { private var isAuthInterceptorCalled = false private var isApiInterceptorCalled = false - private var isheadersInterceptorCalled = false + private var isHeadersInterceptorCalled = false private var isIdentityInterceptorCalled = false private var isEventsInterceptorCalled = false private var isRefreshAuthenticatorCalled = false @@ -158,7 +159,7 @@ class RetrofitsTest { assertTrue(isAuthInterceptorCalled) assertTrue(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -176,7 +177,7 @@ class RetrofitsTest { assertTrue(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertTrue(isEventsInterceptorCalled) } @@ -194,7 +195,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertTrue(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -212,7 +213,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertTrue(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -231,7 +232,7 @@ class RetrofitsTest { assertTrue(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -250,7 +251,7 @@ class RetrofitsTest { assertFalse(isAuthInterceptorCalled) assertFalse(isApiInterceptorCalled) - assertTrue(isheadersInterceptorCalled) + assertTrue(isHeadersInterceptorCalled) assertFalse(isIdentityInterceptorCalled) assertFalse(isEventsInterceptorCalled) } @@ -264,7 +265,7 @@ class RetrofitsTest { interface TestApi { @GET("/test") - suspend fun test(): Result + suspend fun test(): NetworkResult } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensionsTest.kt index 393c0fd8c..64640abd1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/CallExtensionsTest.kt @@ -1,6 +1,6 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util -import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult import io.mockk.every import io.mockk.mockk import okhttp3.HttpUrl @@ -17,53 +17,53 @@ import java.net.URL class CallExtensionsTest { @Test - fun `executeForResult returns failure when execute throws IOException`() { + fun `executeForNetworkResult returns failure when execute throws IOException`() { val request = createMockkRequest() val call = mockk> { every { request() } returns request every { execute() } throws IOException("Fail") } - val result = call.executeForResult() + val result = call.executeForNetworkResult() - assertTrue(result.isFailure) + assertTrue(result is NetworkResult.Failure) } @Test - fun `executeForResult returns failure when execute throws RuntimeException`() { + fun `executeForNetworkResult returns failure when execute throws RuntimeException`() { val request = createMockkRequest() val call = mockk> { every { request() } returns request every { execute() } throws RuntimeException("Fail") } - val result = call.executeForResult() + val result = call.executeForNetworkResult() - assertTrue(result.isFailure) + assertTrue(result is NetworkResult.Failure) } @Test - fun `executeForResult returns failure when response is failure`() { + fun `executeForNetworkResult returns failure when response is failure`() { val request = createMockkRequest() val call = mockk> { every { request() } returns request every { execute() } returns Response.error(400, "".toResponseBody()) } - val result = call.executeForResult() + val result = call.executeForNetworkResult() - assertTrue(result.isFailure) + assertTrue(result is NetworkResult.Failure) } @Test - fun `executeForResult returns success when response is failure`() { + fun `executeForNetworkResult returns success when response is failure`() { val call = mockk> { every { execute() } returns Response.success(Unit) } - val result = call.executeForResult() + val result = call.executeForNetworkResult() - assertEquals(Unit.asSuccess(), result) + assertEquals(NetworkResult.Success(Unit), result) } private fun createMockkRequest(): Request { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensionsTest.kt new file mode 100644 index 000000000..e21e45406 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkResultExtensionsTest.kt @@ -0,0 +1,29 @@ +package com.x8bit.bitwarden.data.platform.datasource.network.util + +import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult +import com.x8bit.bitwarden.data.platform.util.asFailure +import com.x8bit.bitwarden.data.platform.util.asSuccess +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class NetworkResultExtensionsTest { + @Test + fun `NetworkResult toResult with success should return successful result`() { + val value = "test" + val original = NetworkResult.Success(value) + + val result = original.toResult() + + assertEquals(value.asSuccess(), result) + } + + @Test + fun `NetworkResult toResult with failure should return failure result`() { + val exception = Exception("Failed") + val original = NetworkResult.Failure(exception) + + val result = original.toResult() + + assertEquals(exception.asFailure(), result) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerTest.kt index 3fb01a2f4..c0a469e22 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/DatabaseSchemeManagerTest.kt @@ -1,24 +1,37 @@ package com.x8bit.bitwarden.data.platform.manager +import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import io.mockk.every -import io.mockk.just import io.mockk.mockk -import io.mockk.runs import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals import java.time.Clock import java.time.Instant import java.time.ZoneOffset class DatabaseSchemeManagerTest { + private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow(null) private val mockSettingsDiskSource: SettingsDiskSource = mockk { - every { lastDatabaseSchemeChangeInstant } returns null - every { lastDatabaseSchemeChangeInstant = any() } just runs + every { + lastDatabaseSchemeChangeInstant + } returns mutableLastDatabaseSchemeChangeInstantFlow.value + every { lastDatabaseSchemeChangeInstant = any() } answers { + mutableLastDatabaseSchemeChangeInstantFlow.value = firstArg() + } + every { + lastDatabaseSchemeChangeInstantFlow + } returns mutableLastDatabaseSchemeChangeInstantFlow } + private val dispatcherManager = FakeDispatcherManager() private val databaseSchemeManager = DatabaseSchemeManagerImpl( settingsDiskSource = mockSettingsDiskSource, + dispatcherManager = dispatcherManager, ) @Suppress("MaxLineLength") @@ -30,6 +43,23 @@ class DatabaseSchemeManagerTest { } } + @Test + fun `setLastDatabaseSchemeChangeInstant does emit value`() = runTest { + databaseSchemeManager.lastDatabaseSchemeChangeInstantFlow.test { + // Assert the value is initialized to null + assertEquals( + null, + awaitItem(), + ) + // Assert the new value is emitted + databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant() + assertEquals( + FIXED_CLOCK.instant(), + awaitItem(), + ) + } + } + @Test fun `getLastDatabaseSchemeChangeInstant retrieves stored value from settingsDiskSource`() { databaseSchemeManager.lastDatabaseSchemeChangeInstant diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt index 7e7dd0bb5..41e6d341b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson +import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState @@ -37,17 +38,24 @@ class FirstTimeActionManagerTest { every { getFeatureFlagFlow(FlagKey.ImportLoginsFlow) } returns mutableImportLoginsFlow } + private val mutableAutofillEnabledFlow = MutableStateFlow(false) + private val autofillEnabledManager = mockk { + every { isAutofillEnabledStateFlow } returns mutableAutofillEnabledFlow + every { isAutofillEnabled } returns false + } + private val firstTimeActionManager = FirstTimeActionManagerImpl( authDiskSource = fakeAuthDiskSource, settingsDiskSource = fakeSettingsDiskSource, vaultDiskSource = vaultDiskSource, featureFlagManager = featureFlagManager, dispatcherManager = FakeDispatcherManager(), + autofillEnabledManager = autofillEnabledManager, ) @Suppress("MaxLineLength") @Test - fun `allAutoFillSettingsBadgeCountFlow should emit the value of flags set to true and update when changed`() = + fun `allAutoFillSettingsBadgeCountFlow should emit the value of flags set to true and update when saved value is changed or autofill enabled state changes`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE firstTimeActionManager.allAutofillSettingsBadgeCountFlow.test { @@ -62,6 +70,13 @@ class FirstTimeActionManagerTest { showBadge = false, ) assertEquals(0, awaitItem()) + fakeSettingsDiskSource.storeShowAutoFillSettingBadge( + userId = USER_ID, + showBadge = true, + ) + assertEquals(1, awaitItem()) + mutableAutofillEnabledFlow.update { true } + assertEquals(0, awaitItem()) } } @@ -240,6 +255,46 @@ class FirstTimeActionManagerTest { firstTimeActionManager.storeShowImportLoginsSettingsBadge(showBadge = false) assertFalse(fakeSettingsDiskSource.getShowImportLoginsSettingBadge(userId = USER_ID)!!) } + + @Test + fun `show autofill badge when autofill is already enabled should be false`() { + fakeAuthDiskSource.userState = MOCK_USER_STATE + every { autofillEnabledManager.isAutofillEnabled } returns true + assertFalse(firstTimeActionManager.currentOrDefaultUserFirstTimeState.showSetupAutofillCard) + } + + @Test + fun `first time state flow should update when autofill is enabled`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + firstTimeActionManager.firstTimeStateFlow.test { + assertEquals( + FirstTimeState( + showImportLoginsCard = true, + ), + awaitItem(), + ) + fakeSettingsDiskSource.storeShowAutoFillSettingBadge( + userId = MOCK_USER_STATE.activeUserId, + showBadge = true, + ) + assertEquals( + FirstTimeState( + showImportLoginsCard = true, + showSetupAutofillCard = true, + ), + awaitItem(), + ) + mutableAutofillEnabledFlow.update { true } + assertEquals( + FirstTimeState( + showImportLoginsCard = true, + showSetupAutofillCard = false, + ), + awaitItem(), + ) + } + } } private const val USER_ID: String = "userId" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 7674bd061..c5eb5b885 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -12,8 +12,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl +import com.x8bit.bitwarden.data.autofill.accessibility.manager.FakeAccessibilityEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager @@ -55,8 +54,7 @@ class SettingsRepositoryTest { every { disableAutofillServices() } just runs } private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl() - private val accessibilityEnabledManager: AccessibilityEnabledManager = - AccessibilityEnabledManagerImpl() + private val fakeAccessibilityEnabledManager = FakeAccessibilityEnabledManager() private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeSettingsDiskSource = FakeSettingsDiskSource() private val vaultSdkSource: VaultSdkSource = mockk() @@ -75,7 +73,7 @@ class SettingsRepositoryTest { settingsDiskSource = fakeSettingsDiskSource, vaultSdkSource = vaultSdkSource, biometricsEncryptionManager = biometricsEncryptionManager, - accessibilityEnabledManager = accessibilityEnabledManager, + accessibilityEnabledManager = fakeAccessibilityEnabledManager, dispatcherManager = FakeDispatcherManager(), policyManager = policyManager, ) @@ -691,10 +689,10 @@ class SettingsRepositoryTest { settingsRepository.isAccessibilityEnabledStateFlow.test { assertFalse(awaitItem()) - accessibilityEnabledManager.isAccessibilityEnabled = true + fakeAccessibilityEnabledManager.isAccessibilityEnabled = true assertTrue(awaitItem()) - accessibilityEnabledManager.isAccessibilityEnabled = false + fakeAccessibilityEnabledManager.isAccessibilityEnabled = false assertFalse(awaitItem()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt index a8d3462bb..1d6f4c75d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt @@ -59,6 +59,14 @@ class ResultTest { ) } + @Test + fun `asSuccess returns a success Result with the correct content that is not double-wrapped`() { + assertEquals( + Result.success("Test"), + "Test".asSuccess().asSuccess(), + ) + } + @Test fun `asFailure returns a failure Result with the correct content`() { val throwable = IllegalStateException("Test") diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt index 50c6b07a4..d8036ce44 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/service/CiphersServiceTest.kt @@ -311,7 +311,7 @@ class CiphersServiceTest : BaseServiceTest() { ) assertEquals( createMockAttachment(number = 1), - result.testGetOrThrow(), + result.getOrThrow(), ) } @@ -336,25 +336,6 @@ private fun setupMockUri( return mockUri } -/** - * A helper method to attempt validate that the the value is being boxed and causing test to - * inconsistently fail. - * - * This was modified from the code found here: - * * https://github.com/mockk/mockk/issues/485#issuecomment-1973170516 - */ -@Suppress("INVISIBLE_REFERENCE", "UNCHECKED_CAST") -private fun Result.testGetOrThrow(): T = - when (val unboxed: Any? = value) { - is Result.Failure -> throw unboxed.exception - !is Result<*> -> unboxed as T - else -> { - // This means the result is boxed, we could make this recursive to address the issue. - println("Unboxed value = $unboxed") - unboxed as T - } - } - private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ { "attachmentId":"mockAttachmentId-1", diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt index d4626c2c2..c086ee73e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt @@ -234,6 +234,30 @@ class VaultSdkSourceTest { coVerify { sdkClientManager.getOrCreateClient(userId = userId) } } + @Test + fun `validatePin should call SDK and return a Result with the correct data`() = + runBlocking { + val userId = "userId" + val pin = "pin" + val pinProtectedUserKey = "pinProtectedUserKey" + val expectedResult = true + coEvery { + clientAuth.validatePin(pin = pin, pinProtectedUserKey = pinProtectedUserKey) + } returns expectedResult + + val result = vaultSdkSource.validatePin( + userId = userId, + pin = pin, + pinProtectedUserKey = pinProtectedUserKey, + ) + + assertEquals(expectedResult.asSuccess(), result) + coVerify(exactly = 1) { + clientAuth.validatePin(pin = pin, pinProtectedUserKey = pinProtectedUserKey) + sdkClientManager.getOrCreateClient(userId = userId) + } + } + @Test fun `getAuthRequestKey should call SDK and return a Result with correct data`() = runBlocking { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 8c9213a92..1206f4369 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -193,8 +193,14 @@ class VaultRepositoryTest { mutableUnlockedUserIdsStateFlow.first { userId in it } } } + private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow(null) private val databaseSchemeManager: DatabaseSchemeManager = mockk { - every { lastDatabaseSchemeChangeInstant } returns null + every { + lastDatabaseSchemeChangeInstant + } returns mutableLastDatabaseSchemeChangeInstantFlow.value + every { + lastDatabaseSchemeChangeInstantFlow + } returns mutableLastDatabaseSchemeChangeInstantFlow } private val mutableFullSyncFlow = bufferedMutableSharedFlow() @@ -780,6 +786,36 @@ class VaultRepositoryTest { } } + @Test + fun `lastDatabaseSchemeChangeInstantFlow should trigger sync when new value is not null`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + every { + databaseSchemeManager.lastDatabaseSchemeChangeInstant + } returns mutableLastDatabaseSchemeChangeInstantFlow.value + coEvery { syncService.sync() } just awaits + + mutableLastDatabaseSchemeChangeInstantFlow.value = clock.instant() + + coVerify(exactly = 1) { syncService.sync() } + } + + @Test + fun `lastDatabaseSchemeChangeInstantFlow should not sync when new value is null`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + every { + databaseSchemeManager.lastDatabaseSchemeChangeInstant + } returns mutableLastDatabaseSchemeChangeInstantFlow.value + + coEvery { syncService.sync() } just awaits + + mutableLastDatabaseSchemeChangeInstantFlow.value = null + + coVerify(exactly = 0) { syncService.sync() } + } + @Suppress("MaxLineLength") @Test fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensionsTest.kt index 490cab5f6..90d4796d5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCollectionExtensionsTest.kt @@ -32,6 +32,31 @@ class VaultSdkCollectionExtensionsTest { ) } + @Test + fun `toEncryptedSdkCollection should default manage to !isReadOnly if canManage is null`() { + assertEquals( + Collection( + organizationId = "organizationId", + hidePasswords = true, + name = "name", + externalId = "externalId", + readOnly = false, + id = "id", + manage = true, + ), + SyncResponseJson.Collection( + organizationId = "organizationId", + shouldHidePasswords = true, + name = "name", + externalId = "externalId", + isReadOnly = false, + id = "id", + canManage = null, + ) + .toEncryptedSdkCollection(), + ) + } + @Suppress("MaxLineLength") @Test fun `toEncryptedSdkCollectionList should convert a list of network Collections to a list of SDK Collections`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt index 03ecc23e8..cabde2dbb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt @@ -352,8 +352,23 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { awaitItem(), ) } + verify(exactly = 0) { + firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false) + } } + @Suppress("MaxLineLength") + @Test + fun `CloseClick action should update the first time state to false if continue button is enabled`() = + runTest { + val viewModel = + createViewModel(state = DEFAULT_STATE.copy(isUnlockWithPinEnabled = true)) + viewModel.trySendAction(SetupUnlockAction.CloseClick) + verify { + firstTimeActionManager.storeShowUnlockSettingBadge(showBadge = false) + } + } + private fun createViewModel( state: SetupUnlockState? = null, ): SetupUnlockViewModel = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt index d92e4a965..915746004 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt @@ -60,7 +60,7 @@ class EnterpriseSignOnScreenTest : BaseComposeTest() { @Test fun `app bar log in click should send LogInClick action`() { - composeTestRule.onNodeWithText("Log In").performClick() + composeTestRule.onNodeWithText("Log in").performClick() verify { viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt index 8b37fd22c..8b67e2f60 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.bitwarden.core.AuthRequestResponse import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType @@ -166,7 +165,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) mutableCreateAuthRequestWithUpdatesFlow.tryEmit( - CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE), + CreateAuthRequestResult.Success( + authRequest = AUTH_REQUEST, + privateKey = AUTH_REQUEST_PRIVATE_KEY, + accessCode = AUTH_REQUEST_ACCESS_CODE, + ), ) assertEquals( DEFAULT_STATE.copy( @@ -196,9 +199,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository.login( email = EMAIL, requestId = AUTH_REQUEST.id, - accessCode = AUTH_REQUEST_RESPONSE.accessCode, + accessCode = AUTH_REQUEST_ACCESS_CODE, asymmetricalKey = requireNotNull(AUTH_REQUEST.key), - requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey, + requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, masterPasswordHash = AUTH_REQUEST.masterPasswordHash, captchaToken = null, ) @@ -227,7 +230,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(initialState, awaitItem()) mutableCreateAuthRequestWithUpdatesFlow.tryEmit( - CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE), + CreateAuthRequestResult.Success( + authRequest = AUTH_REQUEST, + privateKey = AUTH_REQUEST_PRIVATE_KEY, + accessCode = AUTH_REQUEST_ACCESS_CODE, + ), ) assertEquals( initialState.copy( @@ -279,7 +286,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() viewModel.eventFlow.test { mutableCreateAuthRequestWithUpdatesFlow.tryEmit( - CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE), + CreateAuthRequestResult.Success( + authRequest = AUTH_REQUEST, + privateKey = AUTH_REQUEST_PRIVATE_KEY, + accessCode = AUTH_REQUEST_ACCESS_CODE, + ), ) assertEquals( LoginWithDeviceEvent.NavigateToTwoFactorLogin(emailAddress = EMAIL), @@ -291,9 +302,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository.login( email = EMAIL, requestId = AUTH_REQUEST.id, - accessCode = AUTH_REQUEST_RESPONSE.accessCode, + accessCode = AUTH_REQUEST_ACCESS_CODE, asymmetricalKey = requireNotNull(AUTH_REQUEST.key), - requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey, + requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, masterPasswordHash = AUTH_REQUEST.masterPasswordHash, captchaToken = null, ) @@ -319,7 +330,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) mutableCreateAuthRequestWithUpdatesFlow.tryEmit( - CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE), + CreateAuthRequestResult.Success( + authRequest = AUTH_REQUEST, + privateKey = AUTH_REQUEST_PRIVATE_KEY, + accessCode = AUTH_REQUEST_ACCESS_CODE, + ), ) assertEquals( DEFAULT_STATE.copy( @@ -353,9 +368,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository.login( email = EMAIL, requestId = AUTH_REQUEST.id, - accessCode = AUTH_REQUEST_RESPONSE.accessCode, + accessCode = AUTH_REQUEST_ACCESS_CODE, asymmetricalKey = requireNotNull(AUTH_REQUEST.key), - requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey, + requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, masterPasswordHash = AUTH_REQUEST.masterPasswordHash, captchaToken = null, ) @@ -382,7 +397,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) mutableCreateAuthRequestWithUpdatesFlow.tryEmit( - CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE), + CreateAuthRequestResult.Success( + authRequest = AUTH_REQUEST, + privateKey = AUTH_REQUEST_PRIVATE_KEY, + accessCode = AUTH_REQUEST_ACCESS_CODE, + ), ) assertEquals( DEFAULT_STATE.copy( @@ -416,9 +435,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository.login( email = EMAIL, requestId = AUTH_REQUEST.id, - accessCode = AUTH_REQUEST_RESPONSE.accessCode, + accessCode = AUTH_REQUEST_ACCESS_CODE, asymmetricalKey = requireNotNull(AUTH_REQUEST.key), - requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey, + requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, masterPasswordHash = AUTH_REQUEST.masterPasswordHash, captchaToken = null, ) @@ -474,9 +493,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository.login( email = EMAIL, requestId = AUTH_REQUEST.id, - accessCode = AUTH_REQUEST_RESPONSE.accessCode, + accessCode = AUTH_REQUEST_ACCESS_CODE, asymmetricalKey = requireNotNull(AUTH_REQUEST.key), - requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey, + requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY, masterPasswordHash = AUTH_REQUEST.masterPasswordHash, captchaToken = captchaToken, ) @@ -505,23 +524,17 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `on createAuthRequestWithUpdates with OTHER_DEVICE, Declined received should show error dialog`() { - val viewModel = createViewModel() - assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) - mutableCreateAuthRequestWithUpdatesFlow.tryEmit(CreateAuthRequestResult.Declined) - assertEquals( - DEFAULT_STATE.copy( - viewState = DEFAULT_CONTENT_VIEW_STATE.copy( - fingerprintPhrase = "", - isResendNotificationLoading = false, - ), - dialogState = LoginWithDeviceState.DialogState.Error( - title = null, - message = R.string.this_request_is_no_longer_valid.asText(), - ), + fun `on createAuthRequestWithUpdates with OTHER_DEVICE, Declined received should show unchanged content`() { + val initialState = DEFAULT_STATE.copy( + loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE, + viewState = DEFAULT_CONTENT_VIEW_STATE.copy( + loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE, ), - viewModel.stateFlow.value, ) + val viewModel = createViewModel(state = initialState) + assertEquals(initialState, viewModel.stateFlow.value) + mutableCreateAuthRequestWithUpdatesFlow.tryEmit(CreateAuthRequestResult.Declined) + assertEquals(initialState, viewModel.stateFlow.value) } @Suppress("MaxLineLength") @@ -603,12 +616,8 @@ private val AUTH_REQUEST = AuthRequest( fingerprint = FINGERPRINT, ) -private val AUTH_REQUEST_RESPONSE = AuthRequestResponse( - privateKey = "private_key", - publicKey = "public_key", - accessCode = "accessCode", - fingerprint = "fingerprint", -) +private const val AUTH_REQUEST_ACCESS_CODE = "accessCode" +private const val AUTH_REQUEST_PRIVATE_KEY = "private_key" private val DEFAULT_LOGIN_DATA = LoginWithDeviceState.LoginData( accessCode = "accessCode", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreenTest.kt index b99a23f4a..c6bcf8fc5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeScreenTest.kt @@ -41,13 +41,13 @@ class WelcomeScreenTest : BaseComposeTest() { @Test fun `pages should display and update according to state`() { composeTestRule - .onNodeWithText("Privacy, prioritized") + .onNodeWithText("Security, prioritized") .assertExists() .assertIsDisplayed() mutableEventFlow.tryEmit(WelcomeEvent.UpdatePager(index = 1)) composeTestRule - .onNodeWithText("Privacy, prioritized") + .onNodeWithText("Security, prioritized") .assertDoesNotExist() composeTestRule .onNodeWithText("Quick and easy login") @@ -65,13 +65,13 @@ class WelcomeScreenTest : BaseComposeTest() { @Test fun `pages should display and update according to state in landscape mode`() { composeTestRule - .onNodeWithText("Privacy, prioritized") + .onNodeWithText("Security, prioritized") .assertExists() .assertIsDisplayed() mutableEventFlow.tryEmit(WelcomeEvent.UpdatePager(index = 1)) composeTestRule - .onNodeWithText("Privacy, prioritized") + .onNodeWithText("Security, prioritized") .assertDoesNotExist() composeTestRule .onNodeWithText("Quick and easy login") @@ -111,7 +111,7 @@ class WelcomeScreenTest : BaseComposeTest() { // will be in view on the UI testing viewport. mutableStateFlow.update { it.copy(pages = emptyList()) } composeTestRule - .onNodeWithText("Log In") + .onNodeWithText("Log in") .performClick() verify { viewModel.trySendAction(WelcomeAction.LoginClick) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt index 0d48a3516..090260451 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onNodeWithContentDescription @@ -227,4 +228,17 @@ class VaultSettingsScreenTest : BaseComposeTest() { mutableEventFlow.tryEmit(VaultSettingsEvent.ShowSnackbar(data)) composeTestRule.onNodeWithText("message").assertIsDisplayed() } + + @Test + fun `when snackbar is displayed clicking on it should dismiss`() { + val data = BitwardenSnackbarData("message".asText()) + mutableEventFlow.tryEmit(VaultSettingsEvent.ShowSnackbar(data)) + composeTestRule + .onNodeWithText("message") + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithText("message") + .assertIsNotDisplayed() + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index 0999c3b57..ca324e073 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -41,7 +41,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { VaultUnlockedNavBarScreen( viewModel = viewModel, navController = fakeNavHostController, - onNavigateToVaultAddItem = {}, + onNavigateToVaultAddItem = { _, _, _ -> }, onNavigateToVaultItem = {}, onNavigateToVaultEditItem = {}, onNavigateToAddSend = {}, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 0d8132adb..ea2959874 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -591,7 +591,7 @@ class GeneratorScreenTest : BaseComposeTest() { verify { viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange( + GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange( avoidAmbiguousChars = true, ), ) @@ -646,7 +646,7 @@ class GeneratorScreenTest : BaseComposeTest() { ), ) viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange( + GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange( avoidAmbiguousChars = true, ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt index 880deb9d5..5517f3edf 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt @@ -1325,7 +1325,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { val avoidAmbiguousChars = true viewModel.trySendAction( - GeneratorAction.MainType.Password.ToggleAvoidAmbigousCharactersChange( + GeneratorAction.MainType.Password.ToggleAvoidAmbiguousCharactersChange( avoidAmbiguousChars = avoidAmbiguousChars, ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 0238980a5..407029d6c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -409,7 +409,8 @@ class AddSendScreenTest : BaseComposeTest() { @Test fun `Text segmented button click should send TextTypeClick`() { composeTestRule - .onAllNodesWithText("Text")[0] + .onAllNodesWithText("Text") + .filterToOne(!isEditableText) // A bug prevents performClick from working here so we // have to perform the semantic action instead. .performSemanticsAction(SemanticsActions.OnClick) @@ -469,9 +470,13 @@ class AddSendScreenTest : BaseComposeTest() { @Test fun `text input change should send TextChange`() { composeTestRule - .onAllNodesWithText("Text")[1] + .onAllNodesWithText("Text") + .filterToOne(isEditableText) + .performScrollTo() .performTextInput("input") - viewModel.trySendAction(AddSendAction.TextChange("input")) + verify(exactly = 1) { + viewModel.trySendAction(AddSendAction.TextChange("input")) + } } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index f7e9d7815..62e3711d1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -25,14 +25,12 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialRe import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState -import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -155,15 +153,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { private val organizationEventManager = mockk { every { trackEvent(event = any()) } just runs } - private val mutableSshVaultItemsFeatureFlagFlow = MutableStateFlow(true) - private val featureFlagManager = mockk { - every { - getFeatureFlagFlow(key = FlagKey.SshKeyCipherItems) - } returns mutableSshVaultItemsFeatureFlagFlow - every { - getFeatureFlag(key = FlagKey.SshKeyCipherItems) - } returns mutableSshVaultItemsFeatureFlagFlow.value - } @BeforeEach fun setup() { @@ -180,6 +169,20 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Test fun `initial state should be correct when state is null`() = runTest { + val expectedState = VaultAddEditState( + vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN), + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + isIndividualVaultDisabled = false, + type = VaultAddEditState.ViewState.Content.ItemType.Login(), + ), + dialog = null, + totpData = null, + shouldShowCloseButton = true, + shouldExitOnSave = false, + supportedItemTypes = VaultAddEditState.ItemTypeOption.entries + .filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS }, + ) val viewModel = createAddVaultItemViewModel( savedStateHandle = createSavedStateHandleWithState( state = null, @@ -188,10 +191,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) viewModel.stateFlow.test { assertEquals( - createVaultAddItemState( - commonContentViewState = VaultAddEditState.ViewState.Content.Common(), - typeContentViewState = createLoginTypeContentViewState(), - ), + expectedState, awaitItem(), ) } @@ -262,7 +262,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { type = VaultAddEditState.ViewState.Content.ItemType.Login(), ), dialog = null, - supportedItemTypes = VaultAddEditState.ItemTypeOption.entries, + supportedItemTypes = VaultAddEditState.ItemTypeOption.entries + .filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS }, ), viewModel.stateFlow.value, ) @@ -378,56 +379,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } - @Test - fun `initial add state should be correct when SSH key feature flag is enabled`() { - mutableSshVaultItemsFeatureFlagFlow.value = true - val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN) - val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType) - val viewModel = createAddVaultItemViewModel( - savedStateHandle = createSavedStateHandleWithState( - state = initState, - vaultAddEditType = vaultAddEditType, - ), - ) - assertEquals( - initState, - viewModel.stateFlow.value, - ) - } - - @Test - fun `initial add state should be correct when SSH key feature flag is disabled`() { - mutableSshVaultItemsFeatureFlagFlow.value = false - every { - featureFlagManager.getFeatureFlag(key = FlagKey.SshKeyCipherItems) - } returns false - val vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN) - val expectedState = VaultAddEditState( - vaultAddEditType = vaultAddEditType, - viewState = VaultAddEditState.ViewState.Content( - common = VaultAddEditState.ViewState.Content.Common(), - isIndividualVaultDisabled = false, - type = VaultAddEditState.ViewState.Content.ItemType.Login(), - ), - dialog = null, - totpData = null, - shouldShowCloseButton = true, - shouldExitOnSave = false, - supportedItemTypes = VaultAddEditState.ItemTypeOption.entries - .filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS }, - ) - val viewModel = createAddVaultItemViewModel( - savedStateHandle = createSavedStateHandleWithState( - state = null, - vaultAddEditType = vaultAddEditType, - ), - ) - assertEquals( - expectedState, - viewModel.stateFlow.value, - ) - } - @Test fun `initial edit state should be correct`() = runTest { val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID) @@ -1308,6 +1259,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { manage = true, readOnly = false, ), + createMockCollectionView( + number = 2, + manage = false, + readOnly = true, + ), ), ), ) @@ -3175,7 +3131,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { resourceManager = resourceManager, clock = fixedClock, organizationEventManager = organizationEventManager, - featureFlagManager = featureFlagManager, ) } @@ -4271,30 +4226,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { ) } } - - @Suppress("MaxLineLength") - @Test - fun `SshKeyCipherItemsFeatureFlagReceive should update supportedItemTypes`() = runTest { - // Verify SSH keys is supported when feature flag is enabled. - viewModel.trySendAction( - VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = true), - ) - assertEquals( - VaultAddEditState.ItemTypeOption.entries, - viewModel.stateFlow.value.supportedItemTypes, - ) - - // Verify SSH keys is not supported when feature flag is disabled. - viewModel.trySendAction( - VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(enabled = false), - ) - assertEquals( - VaultAddEditState.ItemTypeOption.entries.filterNot { - it == VaultAddEditState.ItemTypeOption.SSH_KEYS - }, - viewModel.stateFlow.value.supportedItemTypes, - ) - } } //region Helper functions @@ -4429,7 +4360,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { resourceManager = bitwardenResourceManager, clock = clock, organizationEventManager = organizationEventManager, - featureFlagManager = featureFlagManager, ) private fun createVaultData( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 0c71c774f..f63b90561 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onSiblings import androidx.compose.ui.test.performClick @@ -1257,6 +1258,114 @@ class VaultItemScreenTest : BaseComposeTest() { .filterToOne(hasAnyAncestor(isPopup())) .assertDoesNotExist() } + + @Test + fun `on login copy notes field click should send CopyNotesClick`() { + + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = DEFAULT_LOGIN_VIEW_STATE, + ) + } + composeTestRule.onNodeWithTextAfterScroll("Lots of notes") + composeTestRule + .onNodeWithTag("CipherNotesCopyButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + } + } + + @Test + fun `on identity copy notes field click should send CopyNotesClick`() { + // Adding a custom field so that we can scroll to it + // So we can see the Copy notes button but not have it covered by the FAB + val textField = VaultItemState.ViewState.Content.Common.Custom.TextField( + name = "text", + value = "value", + isCopyable = true, + ) + + EMPTY_VIEW_STATES + .forEach { typeState -> + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = typeState.copy( + type = DEFAULT_IDENTITY, + common = EMPTY_COMMON.copy( + notes = "this is a note", + customFields = listOf(textField), + ), + ), + ) + } + + composeTestRule.onNodeWithTextAfterScroll(textField.name) + composeTestRule + .onNodeWithTag("CipherNotesCopyButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + } + } + } + + @Test + fun `on card copy notes field click should send CopyNotesClick`() { + // Adding a custom field so that we can scroll to it + // So we can see the Copy notes button but not have it covered by the FAB + val textField = VaultItemState.ViewState.Content.Common.Custom.TextField( + name = "text", + value = "value", + isCopyable = true, + ) + + EMPTY_VIEW_STATES + .forEach { typeState -> + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = typeState.copy( + type = DEFAULT_IDENTITY, + common = EMPTY_COMMON.copy( + notes = "this is a note", + customFields = listOf(textField), + ), + ), + ) + } + } + + composeTestRule.onNodeWithTextAfterScroll(textField.name) + + composeTestRule + .onNodeWithTag("CipherNotesCopyButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + } + } + + @Test + fun `on secure note copy notes field click should send CopyNotesClick`() { + + mutableStateFlow.update { currentState -> + currentState.copy( + viewState = DEFAULT_SECURE_NOTE_VIEW_STATE, + ) + } + composeTestRule.onNodeWithTextAfterScroll("Lots of notes") + + composeTestRule + .onNodeWithTag("CipherNotesCopyButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + } + } //endregion common //region login @@ -1927,6 +2036,144 @@ class VaultItemScreenTest : BaseComposeTest() { composeTestRule.assertScrollableNodeDoesNotExist(identityName) } + + @Test + fun `in identity state, on copy identity name field click should send CopyIdentityNameClick`() { + + val identityName = "the identity name" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(identityName) + + composeTestRule + .onNodeWithTag("IdentityCopyNameButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick) + } + } + + @Test + fun `in identity state, on copy username field click should send CopyUsernameClick`() { + val username = "the username" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(username) + + composeTestRule + .onNodeWithTag("IdentityCopyUsernameButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick) + } + } + + @Test + fun `in identity state, on copy company field click should send CopyCompanyClick`() { + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + + // Scroll to ssn so we can see the Copy company button but not have it covered by the FAB + composeTestRule.onNodeWithTextAfterScroll("the SSN") + + composeTestRule + .onNodeWithTag("IdentityCopyCompanyButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick) + } + } + + @Test + fun `in identity state, on copy SSN field click should send CopySsnClick`() { + val ssn = "the SSN" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(ssn) + + composeTestRule + .onNodeWithTag("IdentityCopySsnButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in identity state, on copy passport number field click should send CopyPassportNumberClick`() { + val passportNumber = "the passport number" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(passportNumber) + + composeTestRule + .onNodeWithTag("IdentityCopyPassportNumberButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in identity state, on copy license number field click should send CopyLicenseNumberClick`() { + val licenseNumber = "the license number" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(licenseNumber) + + composeTestRule + .onNodeWithTag("IdentityCopyLicenseNumberButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick) + } + } + + @Test + fun `in identity state, on copy email field click should send CopyEmailClick`() { + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onFirstNodeWithTextAfterScroll("the address") + + composeTestRule + .onNodeWithContentDescriptionAfterScroll("Copy email") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick) + } + } + + @Test + fun `in identity state, on copy phone field click should send CopyPhoneClick`() { + val phone = "the phone number" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(phone) + + composeTestRule + .onNodeWithTag("IdentityCopyPhoneButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick) + } + } + + @Test + fun `in identity state, on copy address field click should send CopyAddressClick`() { + val address = "the address" + mutableStateFlow.update { it.copy(viewState = DEFAULT_IDENTITY_VIEW_STATE) } + composeTestRule.onNodeWithTextAfterScroll(address) + + composeTestRule + .onNodeWithTag("IdentityCopyAddressButton") + .performClick() + + verify { + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick) + } + } //endregion identity //region card diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index dff6740ea..4a82e0c32 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -79,6 +78,9 @@ class VaultItemViewModelTest : BaseViewModelTest() { private val organizationEventManager = mockk { every { trackEvent(event = any()) } just runs } + private val mockCipherView = mockk { + every { collectionIds } returns emptyList() + } @BeforeEach fun setup() { @@ -157,79 +159,11 @@ class VaultItemViewModelTest : BaseViewModelTest() { } @Test - fun `canDelete should be true when collections are empty`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `canDelete should be false when cipher is in a collection that the user cannot manage`() = + fun `DeleteClick should show password dialog when re-prompt is required`() = runTest { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-1", "mockId-2") - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false), - createMockCollectionView(number = 2) - .copy(manage = true), - ), - ) - verify { + val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) + every { mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = true, - ) - } - } - - @Test - fun `canDelete should be true when cipher is not in collections`() { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-3") - every { - toViewState( previousState = null, isPremiumUser = true, hasMasterPassword = true, @@ -238,45 +172,6 @@ class VaultItemViewModelTest : BaseViewModelTest() { canAssignToCollections = true, ) } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false), - createMockCollectionView(number = 2) - .copy(manage = false), - ), - ) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Test - fun `DeleteClick should show password dialog when re-prompt is required`() = - runTest { - val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -311,18 +206,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON .copy(requiresReprompt = false), ) - - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, canAssignToCollections = true, - ) - } returns loginState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginState val expected = DEFAULT_STATE.copy( viewState = DEFAULT_VIEW_STATE.copy( @@ -357,18 +250,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginState val expected = DEFAULT_STATE.copy( viewState = loginState, @@ -392,18 +283,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = DEFAULT_VIEW_STATE.copy( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -438,18 +327,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = DEFAULT_VIEW_STATE.copy( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -487,18 +374,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { ?.copy(deletedDate = Instant.MIN), ), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -528,18 +413,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on RestoreItemClick should prompt for master password when required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -564,18 +447,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { runTest { val viewState = DEFAULT_VIEW_STATE.copy(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -604,18 +485,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded( data = createVerificationCodeItem(), @@ -647,18 +526,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test @Suppress("MaxLineLength") fun `ConfirmRestoreClick with RestoreCipherResult Failure should should Show generic error`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -699,18 +576,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on EditClick should prompt for master password when required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -734,18 +609,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -781,17 +654,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -844,17 +716,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -899,17 +770,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState + val password = "password" coEvery { authRepo.validatePassword(password) @@ -965,17 +835,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyCustomHiddenFieldClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1006,17 +874,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyCustomHiddenFieldClick should call setText on ClipboardManager when re-prompt is not required`() { val field = "field" - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = field) } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) @@ -1065,17 +931,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { isCopyable = true, isVisible = false, ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1114,12 +978,13 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on HiddenFieldVisibilityClicked should update hidden field visibility when re-prompt is not required`() = runTest { - val hiddenField = VaultItemState.ViewState.Content.Common.Custom.HiddenField( - name = "hidden", - value = "value", - isCopyable = true, - isVisible = false, - ) + val hiddenField = + VaultItemState.ViewState.Content.Common.Custom.HiddenField( + name = "hidden", + value = "value", + isCopyable = true, + isVisible = false, + ) val loginViewState = VaultItemState.ViewState.Content( common = createCommonContent( isEmpty = true, @@ -1131,18 +996,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { type = createLoginContent(isEmpty = true), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1186,17 +1049,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on AttachmentsClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1231,17 +1092,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1267,17 +1126,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { ), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1318,17 +1175,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginState = DEFAULT_STATE.copy( viewState = loginViewState, ) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1368,17 +1223,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CloneClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1413,17 +1266,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1446,17 +1297,15 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on MoveToOrganizationClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, totpCodeItemData = null, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1491,17 +1340,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1517,74 +1365,6 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } - @Suppress("MaxLineLength") - @Test - fun `canAssignToCollections should be true when item is in a collection the user can manage and edit`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `canAssignToCollections should be false when item is not in a collection the user can manage and edit`() { - val mockCipherView = mockk { - every { collectionIds } returns listOf("mockId-1", "mockId-2") - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = false, - ) - } returns DEFAULT_VIEW_STATE - } - mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) - mutableCollectionsStateFlow.value = DataState.Loaded( - data = listOf( - createMockCollectionView(number = 1) - .copy(manage = false, readOnly = true), - createMockCollectionView(number = 2) - .copy(manage = true), - ), - ) - verify { - mockCipherView.toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = false, - canAssignToCollections = false, - ) - } - } - @Test fun `on CollectionsClick should emit NavigateToCollections`() = runTest { val viewModel = createViewModel(state = DEFAULT_STATE) @@ -1603,17 +1383,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = true), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1661,17 +1440,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1728,17 +1506,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { val loginViewState = createViewState( common = DEFAULT_COMMON.copy(requiresReprompt = false), ) - val mockCipherView = mockk { - every { - toViewState( - previousState = any(), - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = null, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = any(), + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = null, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -1893,6 +1670,33 @@ class VaultItemViewModelTest : BaseViewModelTest() { coVerify { mockFileManager.delete(file) } } + + @Test + fun `on CopyNotesFieldClick should call setText on ClipboardManager`() { + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE + + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + + val notes = "Lots of notes" + every { clipboardManager.setText(text = notes) } just runs + + viewModel.trySendAction(VaultItemAction.Common.CopyNotesClick) + + verify(exactly = 1) { + clipboardManager.setText(text = notes) + } + } } @Nested @@ -1908,20 +1712,18 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CheckForBreachClick should process a password`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = - DataState.Loaded(data = createVerificationCodeItem()) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) @@ -1957,7 +1759,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } coVerify(exactly = 1) { @@ -1969,17 +1772,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyPasswordClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2004,7 +1806,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } } @@ -2012,17 +1815,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopyPasswordClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2039,7 +1841,8 @@ class VaultItemViewModelTest : BaseViewModelTest() { isPremiumUser = true, hasMasterPassword = true, canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), ) } } @@ -2074,20 +1877,20 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyUsernameClick should call setText on ClipboardManager`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - canDelete = true, - canAssignToCollections = true, totpCodeItemData = createTotpCodeData(), - ) - } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + canDelete = true, + canAssignToCollections = true, + totpCodeItemData = createTotpCodeData(), + ) + } returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false)) every { clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME) } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) - mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) + mutableAuthCodeItemFlow.value = + DataState.Loaded(data = createVerificationCodeItem()) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick) @@ -2118,18 +1921,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on PasswordHistoryClick should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2162,23 +1963,21 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on PasswordHistoryClick should emit NavigateToPasswordHistory when re-prompt is not required`() = runTest { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } - .returns( - createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - ), - ) + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) } + .returns( + createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + ), + ) mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2209,18 +2008,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on PasswordVisibilityClicked should show password dialog when re-prompt is required`() = runTest { val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns DEFAULT_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns DEFAULT_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2261,18 +2058,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val loginState = DEFAULT_STATE.copy(viewState = loginViewState) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = createTotpCodeData(), - canDelete = true, - canAssignToCollections = true, - ) - } returns loginViewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = createTotpCodeData(), + canDelete = true, + canAssignToCollections = true, + ) + } returns loginViewState mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem()) @@ -2331,18 +2126,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopyNumberClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2375,21 +2168,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopyNumberClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2414,18 +2205,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on NumberVisibilityClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2458,21 +2247,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on NumberVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "12345436") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2503,18 +2290,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CopySecurityCodeClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2547,21 +2332,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CopySecurityCodeClick should call setText on the ClipboardManager when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) @@ -2586,18 +2369,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { fun `on CodeVisibilityClick should show password dialog when re-prompt is required`() = runTest { val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns CARD_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns CARD_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2630,21 +2411,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test fun `on CodeVisibilityClick should call trackEvent on the OrganizationEventManager and update the ViewState when re-prompt is not required`() { - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns createViewState( - common = DEFAULT_COMMON.copy(requiresReprompt = false), - type = DEFAULT_CARD_TYPE, + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, ) - } + } returns createViewState( + common = DEFAULT_COMMON.copy(requiresReprompt = false), + type = DEFAULT_CARD_TYPE, + ) every { clipboardManager.setText(text = "987") } just runs mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2688,18 +2467,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyPublicKeyClick should copy public key to clipboard`() = runTest { every { clipboardManager.setText("mockPublicKey") } just runs - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2719,18 +2496,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON.copy(requiresReprompt = false), ) val sshKeyState = DEFAULT_STATE.copy(viewState = SSH_KEY_VIEW_STATE) - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2757,18 +2532,16 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on CopyFingerprintClick should copy fingerprint to clipboard`() = runTest { every { clipboardManager.setText("mockFingerprint") } just runs - val mockCipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns SSH_KEY_VIEW_STATE - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns SSH_KEY_VIEW_STATE mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) @@ -2781,6 +2554,143 @@ class VaultItemViewModelTest : BaseViewModelTest() { } } + @Nested + inner class IdentityActions { + private lateinit var viewModel: VaultItemViewModel + + @BeforeEach + fun setup() { + viewModel = createViewModel( + state = DEFAULT_STATE.copy( + viewState = IDENTITY_VIEW_STATE, + ), + ) + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns IDENTITY_VIEW_STATE + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) + mutableAuthCodeItemFlow.value = DataState.Loaded(data = null) + mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) + } + + @Test + fun `on CopyIdentityNameClick should copy fingerprint to clipboard`() = + runTest { + val username = "the username" + every { clipboardManager.setText(text = username) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyUsernameClick) + + verify(exactly = 1) { + clipboardManager.setText(text = username) + } + } + + @Test + fun `on CopyUsernameClick should copy fingerprint to clipboard`() = + runTest { + val identityName = "the identity name" + every { clipboardManager.setText(text = identityName) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyIdentityNameClick) + + verify(exactly = 1) { + clipboardManager.setText(text = identityName) + } + } + + @Test + fun `on CopyCompanyClick should copy company to clipboard`() = runTest { + val company = "the company name" + every { clipboardManager.setText(text = company) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyCompanyClick) + + verify(exactly = 1) { + clipboardManager.setText(text = company) + } + } + + @Test + fun `on CopySsnClick should copy SSN to clipboard`() = runTest { + val ssn = "the SSN" + every { clipboardManager.setText(text = ssn) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopySsnClick) + + verify(exactly = 1) { + clipboardManager.setText(text = ssn) + } + } + + @Test + fun `on CopyPassportNumberClick should copy passport number to clipboard`() = runTest { + val passportNumber = "the passport number" + every { clipboardManager.setText(text = passportNumber) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPassportNumberClick) + + verify(exactly = 1) { + clipboardManager.setText(text = passportNumber) + } + } + + @Test + fun `on CopyLicenseNumberClick should copy license number to clipboard`() = runTest { + val licenseNumber = "the license number" + every { clipboardManager.setText(text = licenseNumber) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyLicenseNumberClick) + + verify(exactly = 1) { + clipboardManager.setText(text = licenseNumber) + } + } + + @Test + fun `on CopyEmailClick should copy email to clipboard`() = runTest { + val email = "the email address" + every { clipboardManager.setText(text = email) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyEmailClick) + + verify(exactly = 1) { + clipboardManager.setText(text = email) + } + } + + @Test + fun `on CopyPhoneClick should copy phone to clipboard`() = runTest { + val phone = "the phone number" + every { clipboardManager.setText(text = phone) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyPhoneClick) + + verify(exactly = 1) { + clipboardManager.setText(text = phone) + } + } + + @Test + fun `on CopyAddressClick should copy address to clipboard`() = runTest { + val address = "the address" + every { clipboardManager.setText(text = address) } just runs + + viewModel.trySendAction(VaultItemAction.ItemType.Identity.CopyAddressClick) + + verify(exactly = 1) { + clipboardManager.setText(text = address) + } + } + } + @Nested inner class VaultItemFlow { @BeforeEach @@ -2804,24 +2714,25 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Loaded and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Loaded(data = cipherView) + mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) - assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) + assertEquals( + DEFAULT_STATE.copy(viewState = viewState), + viewModel.stateFlow.value, + ) } @Test @@ -2844,21 +2755,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Pending and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Pending(data = cipherView) + mutableVaultItemFlow.value = DataState.Pending(data = mockCipherView) mutableCollectionsStateFlow.value = DataState.Loaded(emptyList()) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) @@ -2885,21 +2794,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with Error and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.Error(error = Throwable(), data = cipherView) + mutableVaultItemFlow.value = DataState.Error(error = Throwable(), data = mockCipherView) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -2923,21 +2830,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { @Test fun `on VaultDataReceive with NoNetwork and nonnull data should update the ViewState`() { val viewState = mockk() - val cipherView = mockk { - every { - toViewState( - previousState = null, - isPremiumUser = true, - hasMasterPassword = true, - totpCodeItemData = null, - canDelete = true, - canAssignToCollections = true, - ) - } returns viewState - } + every { + mockCipherView.toViewState( + previousState = null, + isPremiumUser = true, + hasMasterPassword = true, + totpCodeItemData = null, + canDelete = true, + canAssignToCollections = true, + ) + } returns viewState val viewModel = createViewModel(state = null) - mutableVaultItemFlow.value = DataState.NoNetwork(data = cipherView) + mutableVaultItemFlow.value = DataState.NoNetwork(data = mockCipherView) assertEquals(DEFAULT_STATE.copy(viewState = viewState), viewModel.stateFlow.value) } @@ -3103,6 +3008,19 @@ class VaultItemViewModelTest : BaseViewModelTest() { showPrivateKey = false, ) + private val DEFAULT_IDENTITY_TYPE: VaultItemState.ViewState.Content.ItemType.Identity = + VaultItemState.ViewState.Content.ItemType.Identity( + username = "the username", + identityName = "the identity name", + company = "the company name", + ssn = "the SSN", + passportNumber = "the passport number", + licenseNumber = "the license number", + email = "the email address", + phone = "the phone number", + address = "the address", + ) + private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common = VaultItemState.ViewState.Content.Common( name = "login cipher", @@ -3167,5 +3085,11 @@ class VaultItemViewModelTest : BaseViewModelTest() { common = DEFAULT_COMMON, type = DEFAULT_SSH_KEY_TYPE, ) + + private val IDENTITY_VIEW_STATE: VaultItemState.ViewState.Content = + VaultItemState.ViewState.Content( + common = DEFAULT_COMMON, + type = DEFAULT_IDENTITY_TYPE, + ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index f84921b60..52642bffa 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -117,7 +117,9 @@ class VaultItemListingScreenTest : BaseComposeTest() { biometricsManager = biometricsManager, onNavigateBack = { onNavigateBackCalled = true }, onNavigateToVaultItem = { onNavigateToVaultItemId = it }, - onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true }, + onNavigateToVaultAddItemScreen = { _, _, _ -> + onNavigateToVaultAddItemScreenCalled = true + }, onNavigateToAddSendItem = { onNavigateToAddSendScreenCalled = true }, onNavigateToEditSendItem = { onNavigateToEditSendItemId = it }, onNavigateToSearch = { onNavigateToSearchType = it }, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 591a0f1e4..a7769df7a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -281,6 +281,23 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Test + fun `BackClick with AutofillSelectionData should emit ExitApp`() = runTest { + specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSelection( + autofillSelectionData = AutofillSelectionData( + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + type = AutofillSelectionData.Type.LOGIN, + uri = null, + ), + shouldFinishWhenComplete = false, + ) + val viewModel = createVaultItemListingViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(VaultItemListingsAction.BackClick) + assertEquals(VaultItemListingEvent.ExitApp, awaitItem()) + } + } + @Test fun `BackClick should emit NavigateBack`() = runTest { val viewModel = createVaultItemListingViewModel() @@ -942,6 +959,27 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `AddVaultItemClick inside a folder should emit NavigateToAddVaultItem with a selected folder id`() = + runTest { + val viewModel = createVaultItemListingViewModel( + savedStateHandle = createSavedStateHandleWithVaultItemListingType( + vaultItemListingType = VaultItemListingType.Folder(folderId = "id"), + ), + ) + viewModel.eventFlow.test { + viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick) + assertEquals( + VaultItemListingEvent.NavigateToAddVaultItem( + VaultItemCipherType.LOGIN, + selectedFolderId = "id", + ), + awaitItem(), + ) + } + } + @Test fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest { val viewModel = createVaultItemListingViewModel() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt index 211d6dc58..0b2129f87 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingStateExtensionsTest.kt @@ -130,6 +130,7 @@ class VaultItemListingStateExtensionsTest { VaultItemListingState.ItemListingType.Vault.Login, VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId"), VaultItemListingState.ItemListingType.Vault.SshKey, + VaultItemListingState.ItemListingType.Vault.Folder(folderId = "mockId"), ) val result = itemListingTypes.map { it.toVaultItemCipherType() } @@ -142,6 +143,7 @@ class VaultItemListingStateExtensionsTest { VaultItemCipherType.LOGIN, VaultItemCipherType.LOGIN, VaultItemCipherType.SSH_KEY, + VaultItemCipherType.LOGIN, ), result, ) @@ -149,15 +151,8 @@ class VaultItemListingStateExtensionsTest { @Test fun `toVaultItemCipherType should throw an exception for unsupported ItemListingTypes`() { - val itemListingTypes = listOf( - VaultItemListingState.ItemListingType.Vault.Trash, - VaultItemListingState.ItemListingType.Vault.Folder( - folderId = "mockId", - ), - ) - - itemListingTypes.forEach { - assertThrows { it.toVaultItemCipherType() } + assertThrows { + VaultItemListingState.ItemListingType.Vault.Trash.toVaultItemCipherType() } } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModelTests.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModelTests.kt index 585c1d7af..d7f76d3aa 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModelTests.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryViewModelTests.kt @@ -36,8 +36,7 @@ class ManualCodeEntryViewModelTests : BaseViewModelTest() { @Test fun `CodeSubmit should emit new code and NavigateBack`() = runTest { - val viewModel = - createViewModel(initialState = ManualCodeEntryState("TestCode")) + val viewModel = createViewModel(initialState = ManualCodeEntryState(" TestCode ")) viewModel.eventFlow.test { viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt index acbf4f18f..50538d7bd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/CollectionViewExtensionsTest.kt @@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.vault.feature.util import com.bitwarden.vault.CollectionView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class CollectionViewExtensionsTest { @@ -66,4 +68,102 @@ class CollectionViewExtensionsTest { collectionName.toCollectionDisplayName(collectionList), ) } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the user has manage permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + + val collectionIds = listOf("mockId-1", "mockId-2") + + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return false if the user does not have manage permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = false), + createMockCollectionView(number = 2, manage = false), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertFalse(collectionList.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is null`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(null.hasDeletePermissionInAtLeastOneCollection(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is null`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(null)) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionIds list is empty`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.hasDeletePermissionInAtLeastOneCollection(emptyList())) + } + + @Suppress("MaxLineLength") + @Test + fun `hasDeletePermissionInAtLeastOneCollection should return true if the collectionView list is empty`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue( + emptyList().hasDeletePermissionInAtLeastOneCollection( + collectionIds, + ), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `canAssociateToCollections should return true if the user has edit and manage permission`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true, readOnly = false), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(collectionList.canAssignToCollections(collectionIds)) + } + + @Suppress("MaxLineLength") + @Test + fun `canAssociateToCollections should return false if the user does not have manage or edit permission in at least one collection`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = false, readOnly = true), + createMockCollectionView(number = 2, manage = false, readOnly = true), + ) + val collectionIds = listOf("mockId-1", "mockId-2") + assertFalse(collectionList.canAssignToCollections(collectionIds)) + } + + @Test + fun `canAssociateToCollections should return true if the collectionView list is null`() { + val collectionIds = listOf("mockId-1", "mockId-2") + assertTrue(null.canAssignToCollections(collectionIds)) + } + + @Test + fun `canAssociateToCollections should return true if the collectionIds list is null`() { + val collectionList: List = listOf( + createMockCollectionView(number = 1, manage = true, readOnly = false), + createMockCollectionView(number = 2, manage = false), + ) + assertTrue(collectionList.canAssignToCollections(null)) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index ef752981f..33c4bcddb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -1206,6 +1206,19 @@ class VaultScreenTest : BaseComposeTest() { composeTestRule.onNodeWithText("message").assertIsDisplayed() } + @Test + fun `when snackbar is displayed clicking on it should dismiss`() { + val data = BitwardenSnackbarData("message".asText()) + mutableEventFlow.tryEmit(VaultEvent.ShowSnackbar(data)) + composeTestRule + .onNodeWithText("message") + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithText("message") + .assertIsNotDisplayed() + } + @Test fun `SSH key group header should display correctly based on state`() { val count = 1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5acfbc730..de5e88caf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ [versions] # SDK Versions -compileSdk = "34" -targetSdk = "34" +compileSdk = "35" +targetSdk = "35" minSdk = "29" # Dependency Versions @@ -12,22 +12,22 @@ androidxActivity = "1.9.3" androidXBiometrics = "1.2.0-alpha05" androidxBrowser = "1.8.0" androidxCamera = "1.4.0" -androidxComposeBom = "2024.10.01" -androidxCore = "1.13.1" +androidxComposeBom = "2024.11.00" +androidxCore = "1.15.0" androidxCredentials = "1.3.0" androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.8.6" +androidxLifecycle = "2.8.7" androidxNavigation = "2.8.0" androidxRoom = "2.6.1" androidXSecurityCrypto = "1.1.0-alpha06" androidxSplash = "1.1.0-rc01" androidXAppCompat = "1.7.0" androdixAutofill = "1.1.0" -androidxWork = "2.9.1" +androidxWork = "2.10.0" bitwardenSdk = "1.0.0-20241030.101847-8" crashlytics = "3.0.2" detekt = "1.23.7" -firebaseBom = "33.5.1" +firebaseBom = "33.6.0" glide = "1.0.0-beta01" googleServices = "4.4.2" hilt = "2.52" @@ -39,7 +39,7 @@ kotlinxCollectionsImmutable = "0.3.8" kotlinxCoroutines = "1.9.0" kotlinxSerialization = "1.7.3" kotlinxKover = "0.8.3" -ksp = "2.0.21-1.0.26" +ksp = "2.0.21-1.0.27" mockk = "1.13.13" okhttp = "4.12.0" retrofitBom = "2.11.0" diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh new file mode 100755 index 000000000..63dec953a --- /dev/null +++ b/scripts/release-notes.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Release Notes Generator +# +# Generates release notes when GitHub's automated system fails, specifically for: +# - Releases containing cherry-picked commits +# - Changes without associated Pull Requests + +# Prerequisites: +# - GitHub CLI (gh) installed and authenticated +# - Git command line tools installed + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "E.g: $0 v2024.10.2 origin/release/hotfix-v2024.10.2" + exit 1 +fi + +TAG1="$1" +TAG2="$2" + +echo "## What's Changed" +echo + +git log "$TAG1..$TAG2" --pretty=format:"%an|%ae|%s|%b" --reverse | +while IFS='|' read -r name email commit_title commit_body; do + echo $name $email + continue + if [ -z "$email" ]; then + continue + fi + + # Extract GitHub username from email + if [[ "$email" == *"@users.noreply.github.com" ]]; then + author=${email##*+} + author=${author%@*} + else + # For other emails, look up GitHub username using gh cli + author=$(gh api -q '.items[0].login' "search/users?q=$email") + fi + + cherry_picked_hash=$(echo "$commit_body" | grep 'cherry picked' | sed 's/(cherry picked from commit \(.*\))/\1/') + changelog="* $commit_title by @$author" + if [[ "$commit_body" == *"cherry picked"* ]]; then + changelog="$changelog 🍒 $cherry_picked_hash" + fi + + echo "$changelog" +done