Merge branch 'bitwarden:main' into main

This commit is contained in:
SymphonicDeviation 2024-11-20 14:58:48 -05:00 committed by GitHub
commit 0772b74b25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
310 changed files with 5749 additions and 3661 deletions

View file

@ -68,7 +68,7 @@ jobs:
java-version: ${{ env.JAVA_VERSION }} java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby - name: Configure Ruby
uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with: with:
bundler-cache: true bundler-cache: true
@ -106,7 +106,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby - name: Configure Ruby
uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with: with:
bundler-cache: true bundler-cache: true
@ -396,7 +396,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby - name: Configure Ruby
uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with: with:
bundler-cache: true bundler-cache: true
@ -464,10 +464,17 @@ jobs:
- name: Increment version - name: Increment version
run: | run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER)) DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \ bundle exec fastlane setBuildVersionInfo \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \ versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }} 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 - name: Generate F-Droid artifacts
env: env:
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }} FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
@ -528,11 +535,11 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Install Firebase app distribution plugin - 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 run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase - 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: env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: | run: |

View file

@ -2,7 +2,7 @@ name: Crowdin Sync
on: on:
workflow_dispatch: workflow_dispatch:
inputs: { } inputs: {}
schedule: schedule:
- cron: '0 0 * * 5' - cron: '0 0 * * 5'
@ -28,10 +28,17 @@ jobs:
keyvault: "bitwarden-ci" keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" 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 - name: Download translations
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0 uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with: with:
config: crowdin.yml config: crowdin.yml

127
.github/workflows/github-release.yml vendored Normal file
View file

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

56
.github/workflows/release-branch.yml vendored Normal file
View file

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

View file

@ -33,7 +33,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx - name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 uses: checkmarx/ast-github-action@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37
env: env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with: with:
@ -48,7 +48,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }} --output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub - 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: with:
sarif_file: cx_result.sarif sarif_file: cx_result.sarif

View file

@ -60,7 +60,7 @@ jobs:
${{ runner.os }}-build- ${{ runner.os }}-build-
- name: Configure Ruby - name: Configure Ruby
uses: ruby/setup-ruby@7bae1d00b5db9166f4f0fc47985a3a5702cb58f0 # v1.197.0 uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with: with:
bundler-cache: true bundler-cache: true

View file

@ -10,8 +10,8 @@ GEM
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.996.0) aws-partitions (1.1003.0)
aws-sdk-core (3.211.0) aws-sdk-core (3.212.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
@ -19,7 +19,7 @@ GEM
aws-sdk-kms (1.95.0) aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) 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-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
@ -32,7 +32,7 @@ GEM
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
date (3.3.4) date (3.4.0)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
@ -162,7 +162,7 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.4) json (2.8.1)
jwt (2.9.3) jwt (2.9.3)
base64 base64
mini_magick (4.13.2) mini_magick (4.13.2)
@ -172,7 +172,7 @@ GEM
nanaimo (0.4.0) nanaimo (0.4.0)
naturally (2.2.1) naturally (2.2.1)
nkf (0.2.0) nkf (0.2.0)
optparse (0.5.0) optparse (0.6.0)
os (1.1.4) os (1.1.4)
plist (3.7.1) plist (3.7.1)
public_suffix (6.0.1) public_suffix (6.0.1)
@ -199,7 +199,7 @@ GEM
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
time (0.4.0) time (0.4.1)
date date
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
@ -209,7 +209,7 @@ GEM
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unicode-display_width (2.6.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.26.0) xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)

View file

@ -9,7 +9,7 @@
## Compatibility ## Compatibility
- **Minimum SDK**: 29 - **Minimum SDK**: 29
- **Target SDK**: 34 - **Target SDK**: 35
- **Device Types Supported**: Phone and Tablet - **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape - **Orientations Supported**: Portrait and Landscape

View file

@ -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')"
]
}
}

View file

@ -15,7 +15,6 @@ import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController 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.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
@ -39,9 +38,6 @@ class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels()
@Inject
lateinit var accessibilityActivityManager: AccessibilityActivityManager
@Inject @Inject
lateinit var autofillActivityManager: AutofillActivityManager lateinit var autofillActivityManager: AutofillActivityManager

View file

@ -7,13 +7,21 @@ import kotlinx.serialization.Serializable
* Container for the user's API tokens. * Container for the user's API tokens.
* *
* @property requestId The ID of the pending Auth Request. * @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 @Serializable
data class PendingAuthRequestJson( data class PendingAuthRequestJson(
@SerialName("Id") @SerialName("id")
val requestId: String, val requestId: String,
@SerialName("PrivateKey") @SerialName("privateKey")
val requestPrivateKey: String, val requestPrivateKey: String,
@SerialName("accessCode")
val requestAccessCode: String,
@SerialName("fingerprint")
val requestFingerprint: String,
) )

View file

@ -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.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson 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.auth.datasource.network.model.VerifyOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.HTTP import retrofit2.http.HTTP
import retrofit2.http.POST import retrofit2.http.POST
@ -18,43 +19,43 @@ interface AuthenticatedAccountsApi {
* Converts the currently active account to a key-connector account. * Converts the currently active account to a key-connector account.
*/ */
@POST("/accounts/convert-to-key-connector") @POST("/accounts/convert-to-key-connector")
suspend fun convertToKeyConnector(): Result<Unit> suspend fun convertToKeyConnector(): NetworkResult<Unit>
/** /**
* Creates the keys for the current account. * Creates the keys for the current account.
*/ */
@POST("/accounts/keys") @POST("/accounts/keys")
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result<Unit> suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
/** /**
* Deletes the current account. * Deletes the current account.
*/ */
@HTTP(method = "DELETE", path = "/accounts", hasBody = true) @HTTP(method = "DELETE", path = "/accounts", hasBody = true)
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit> suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult<Unit>
@POST("/accounts/request-otp") @POST("/accounts/request-otp")
suspend fun requestOtp(): Result<Unit> suspend fun requestOtp(): NetworkResult<Unit>
@POST("/accounts/verify-otp") @POST("/accounts/verify-otp")
suspend fun verifyOtp( suspend fun verifyOtp(
@Body body: VerifyOtpRequestJson, @Body body: VerifyOtpRequestJson,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Resets the temporary password. * Resets the temporary password.
*/ */
@HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true) @HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true)
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result<Unit> suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
/** /**
* Resets the password. * Resets the password.
*/ */
@HTTP(method = "POST", path = "/accounts/password", hasBody = true) @HTTP(method = "POST", path = "/accounts/password", hasBody = true)
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit> suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
/** /**
* Sets the password. * Sets the password.
*/ */
@POST("/accounts/set-password") @POST("/accounts/set-password")
suspend fun setPassword(@Body body: SetPasswordRequestJson): Result<Unit> suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult<Unit>
} }

View file

@ -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.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson 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.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -22,7 +23,7 @@ interface AuthenticatedAuthRequestsApi {
suspend fun createAdminAuthRequest( suspend fun createAdminAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String, @Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson, @Body body: AuthRequestRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest> ): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/** /**
* Updates an authentication request. * Updates an authentication request.
@ -31,13 +32,13 @@ interface AuthenticatedAuthRequestsApi {
suspend fun updateAuthRequest( suspend fun updateAuthRequest(
@Path("id") userId: String, @Path("id") userId: String,
@Body body: AuthRequestUpdateRequestJson, @Body body: AuthRequestUpdateRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest> ): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/** /**
* Gets a list of auth requests for this device. * Gets a list of auth requests for this device.
*/ */
@GET("/auth-requests") @GET("/auth-requests")
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> suspend fun getAuthRequests(): NetworkResult<AuthRequestsResponseJson>
/** /**
* Retrieves an existing authentication request by ID. * Retrieves an existing authentication request by ID.
@ -45,5 +46,5 @@ interface AuthenticatedAuthRequestsApi {
@GET("/auth-requests/{requestId}") @GET("/auth-requests/{requestId}")
suspend fun getAuthRequest( suspend fun getAuthRequest(
@Path("requestId") requestId: String, @Path("requestId") requestId: String,
): Result<AuthRequestsResponseJson.AuthRequest> ): NetworkResult<AuthRequestsResponseJson.AuthRequest>
} }

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson 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.auth.datasource.network.model.TrustedDeviceKeysResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
@ -16,5 +17,5 @@ interface AuthenticatedDevicesApi {
suspend fun updateTrustedDeviceKeys( suspend fun updateTrustedDeviceKeys(
@Path(value = "appId") appId: String, @Path(value = "appId") appId: String,
@Body request: TrustedDeviceKeysRequestJson, @Body request: TrustedDeviceKeysRequestJson,
): Result<TrustedDeviceKeysResponseJson> ): NetworkResult<TrustedDeviceKeysResponseJson>
} }

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson 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.Body
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Url import retrofit2.http.Url
@ -15,5 +16,5 @@ interface AuthenticatedKeyConnectorApi {
suspend fun storeMasterKeyToKeyConnector( suspend fun storeMasterKeyToKeyConnector(
@Url url: String, @Url url: String,
@Body body: KeyConnectorMasterKeyRequestJson, @Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit> ): NetworkResult<Unit>
} }

View file

@ -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.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson 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.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.PUT import retrofit2.http.PUT
@ -20,7 +21,7 @@ interface AuthenticatedOrganizationApi {
@Path("orgId") organizationId: String, @Path("orgId") organizationId: String,
@Path("userId") userId: String, @Path("userId") userId: String,
@Body body: OrganizationResetPasswordEnrollRequestJson, @Body body: OrganizationResetPasswordEnrollRequestJson,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Checks whether this organization auto enrolls users in password reset. * Checks whether this organization auto enrolls users in password reset.
@ -28,7 +29,7 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{identifier}/auto-enroll-status") @GET("/organizations/{identifier}/auto-enroll-status")
suspend fun getOrganizationAutoEnrollResponse( suspend fun getOrganizationAutoEnrollResponse(
@Path("identifier") organizationIdentifier: String, @Path("identifier") organizationIdentifier: String,
): Result<OrganizationAutoEnrollStatusResponseJson> ): NetworkResult<OrganizationAutoEnrollStatusResponseJson>
/** /**
* Gets the public and private keys for this organization. * Gets the public and private keys for this organization.
@ -36,5 +37,5 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{id}/keys") @GET("/organizations/{id}/keys")
suspend fun getOrganizationKeys( suspend fun getOrganizationKeys(
@Path("id") organizationId: String, @Path("id") organizationId: String,
): Result<OrganizationKeysResponseJson> ): NetworkResult<OrganizationKeysResponseJson>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
@ -14,5 +15,5 @@ interface HaveIBeenPwnedApi {
suspend fun fetchBreachedPasswords( suspend fun fetchBreachedPasswords(
@Path("hashPrefix") @Path("hashPrefix")
hashPrefix: String, hashPrefix: String,
): Result<ResponseBody> ): NetworkResult<ResponseBody>
} }

View file

@ -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.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson 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.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 com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Header import retrofit2.http.Header
@ -15,16 +16,16 @@ interface UnauthenticatedAccountsApi {
@POST("/accounts/password-hint") @POST("/accounts/password-hint")
suspend fun passwordHintRequest( suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson, @Body body: PasswordHintRequestJson,
): Result<Unit> ): NetworkResult<Unit>
@POST("/two-factor/send-email-login") @POST("/two-factor/send-email-login")
suspend fun resendVerificationCodeEmail( suspend fun resendVerificationCodeEmail(
@Body body: ResendEmailRequestJson, @Body body: ResendEmailRequestJson,
): Result<Unit> ): NetworkResult<Unit>
@POST("/accounts/set-key-connector-key") @POST("/accounts/set-key-connector-key")
suspend fun setKeyConnectorKey( suspend fun setKeyConnectorKey(
@Body body: KeyConnectorKeyRequestJson, @Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<Unit> ): NetworkResult<Unit>
} }

View file

@ -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.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson 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.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -21,7 +22,7 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun createAuthRequest( suspend fun createAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String, @Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson, @Body body: AuthRequestRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest> ): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/** /**
* Queries for updates to a given auth request. * Queries for updates to a given auth request.
@ -30,5 +31,5 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun getAuthRequestUpdate( suspend fun getAuthRequestUpdate(
@Path("requestId") requestId: String, @Path("requestId") requestId: String,
@Query("code") accessCode: String, @Query("code") accessCode: String,
): Result<AuthRequestsResponseJson.AuthRequest> ): NetworkResult<AuthRequestsResponseJson.AuthRequest>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api 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.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -11,5 +12,5 @@ interface UnauthenticatedDevicesApi {
suspend fun getIsKnownDevice( suspend fun getIsKnownDevice(
@Header(value = "X-Request-Email") emailAddress: String, @Header(value = "X-Request-Email") emailAddress: String,
@Header(value = "X-Device-Identifier") deviceId: String, @Header(value = "X-Device-Identifier") deviceId: String,
): Result<Boolean> ): NetworkResult<Boolean>
} }

View file

@ -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.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson 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.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
@ -46,12 +47,12 @@ interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorProvider") twoFactorMethod: String?, @Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?, @Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?, @Field(value = "authRequest") authRequestId: String?,
): Result<GetTokenResponseJson.Success> ): NetworkResult<GetTokenResponseJson.Success>
@GET("/sso/prevalidate") @GET("/sso/prevalidate")
suspend fun prevalidateSso( suspend fun prevalidateSso(
@Query("domainHint") organizationIdentifier: String, @Query("domainHint") organizationIdentifier: String,
): Result<PrevalidateSsoResponseJson> ): NetworkResult<PrevalidateSsoResponseJson>
/** /**
* This call needs to be synchronous so we need it to return a [Call] directly. The identity * This call needs to be synchronous so we need it to return a [Call] directly. The identity
@ -66,23 +67,25 @@ interface UnauthenticatedIdentityApi {
): Call<RefreshTokenResponseJson> ): Call<RefreshTokenResponseJson>
@POST("/accounts/prelogin") @POST("/accounts/prelogin")
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson> suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult<PreLoginResponseJson>
@POST("/accounts/register") @POST("/accounts/register")
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success> suspend fun register(
@Body body: RegisterRequestJson,
): NetworkResult<RegisterResponseJson.Success>
@POST("/accounts/register/finish") @POST("/accounts/register/finish")
suspend fun registerFinish( suspend fun registerFinish(
@Body body: RegisterFinishRequestJson, @Body body: RegisterFinishRequestJson,
): Result<RegisterResponseJson.Success> ): NetworkResult<RegisterResponseJson.Success>
@POST("/accounts/register/send-verification-email") @POST("/accounts/register/send-verification-email")
suspend fun sendVerificationEmail( suspend fun sendVerificationEmail(
@Body body: SendVerificationEmailRequestJson, @Body body: SendVerificationEmailRequestJson,
): Result<JsonPrimitive?> ): NetworkResult<JsonPrimitive?>
@POST("/accounts/register/verification-email-clicked") @POST("/accounts/register/verification-email-clicked")
suspend fun verifyEmailToken( suspend fun verifyEmailToken(
@Body body: VerifyEmailTokenRequestJson, @Body body: VerifyEmailTokenRequestJson,
): Result<Unit> ): NetworkResult<Unit>
} }

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson 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.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 com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
@ -20,11 +21,11 @@ interface UnauthenticatedKeyConnectorApi {
@Url url: String, @Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
@Body body: KeyConnectorMasterKeyRequestJson, @Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit> ): NetworkResult<Unit>
@GET @GET
suspend fun getMasterKeyFromKeyConnector( suspend fun getMasterKeyFromKeyConnector(
@Url url: String, @Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String, @Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<KeyConnectorMasterKeyResponseJson> ): NetworkResult<KeyConnectorMasterKeyResponseJson>
} }

View file

@ -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.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest 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.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST import retrofit2.http.POST
@ -17,7 +18,7 @@ interface UnauthenticatedOrganizationApi {
@POST("/organizations/domain/sso/details") @POST("/organizations/domain/sso/details")
suspend fun getClaimedDomainOrganizationDetails( suspend fun getClaimedDomainOrganizationDetails(
@Body body: OrganizationDomainSsoDetailsRequestJson, @Body body: OrganizationDomainSsoDetailsRequestJson,
): Result<OrganizationDomainSsoDetailsResponseJson> ): NetworkResult<OrganizationDomainSsoDetailsResponseJson>
/** /**
* Checks for the verfied organization domains of an email for SSO purposes. * Checks for the verfied organization domains of an email for SSO purposes.
@ -25,5 +26,5 @@ interface UnauthenticatedOrganizationApi {
@POST("/organizations/domain/sso/verified") @POST("/organizations/domain/sso/verified")
suspend fun getVerifiedOrganizationDomainsByEmail( suspend fun getVerifiedOrganizationDomainsByEmail(
@Body body: VerifiedOrganizationDomainSsoDetailsRequest, @Body body: VerifiedOrganizationDomainSsoDetailsRequest,
): Result<VerifiedOrganizationDomainSsoDetailsResponse> ): NetworkResult<VerifiedOrganizationDomainSsoDetailsResponse>
} }

View file

@ -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.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX 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.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/** /**
@ -37,18 +38,22 @@ class AccountsServiceImpl(
* Converts the currently active account to a key-connector account. * Converts the currently active account to a key-connector account.
*/ */
override suspend fun convertToKeyConnector(): Result<Unit> = override suspend fun convertToKeyConnector(): Result<Unit> =
authenticatedAccountsApi.convertToKeyConnector() authenticatedAccountsApi
.convertToKeyConnector()
.toResult()
override suspend fun createAccountKeys( override suspend fun createAccountKeys(
publicKey: String, publicKey: String,
encryptedPrivateKey: String, encryptedPrivateKey: String,
): Result<Unit> = ): Result<Unit> =
authenticatedAccountsApi.createAccountKeys( authenticatedAccountsApi
body = CreateAccountKeysRequest( .createAccountKeys(
publicKey = publicKey, body = CreateAccountKeysRequest(
encryptedPrivateKey = encryptedPrivateKey, publicKey = publicKey,
), encryptedPrivateKey = encryptedPrivateKey,
) ),
)
.toResult()
override suspend fun deleteAccount( override suspend fun deleteAccount(
masterPasswordHash: String?, masterPasswordHash: String?,
@ -61,9 +66,8 @@ class AccountsServiceImpl(
oneTimePassword = oneTimePassword, oneTimePassword = oneTimePassword,
), ),
) )
.map { .toResult()
DeleteAccountResponseJson.Success .map { DeleteAccountResponseJson.Success }
}
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
.toBitwardenError() .toBitwardenError()
@ -75,20 +79,25 @@ class AccountsServiceImpl(
} }
override suspend fun requestOneTimePasscode(): Result<Unit> = override suspend fun requestOneTimePasscode(): Result<Unit> =
authenticatedAccountsApi.requestOtp() authenticatedAccountsApi
.requestOtp()
.toResult()
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> = override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
authenticatedAccountsApi.verifyOtp( authenticatedAccountsApi
VerifyOtpRequestJson( .verifyOtp(
oneTimePasscode = passcode, VerifyOtpRequestJson(
), oneTimePasscode = passcode,
) ),
)
.toResult()
override suspend fun requestPasswordHint( override suspend fun requestPasswordHint(
email: String, email: String,
): Result<PasswordHintResponseJson> = ): Result<PasswordHintResponseJson> =
unauthenticatedAccountsApi unauthenticatedAccountsApi
.passwordHintRequest(PasswordHintRequestJson(email)) .passwordHintRequest(PasswordHintRequestJson(email))
.toResult()
.map { PasswordHintResponseJson.Success } .map { PasswordHintResponseJson.Success }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
@ -101,54 +110,70 @@ class AccountsServiceImpl(
} }
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> = override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body) unauthenticatedAccountsApi
.resendVerificationCodeEmail(body = body)
.toResult()
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> { override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
return if (body.currentPasswordHash == null) { if (body.currentPasswordHash == null) {
authenticatedAccountsApi.resetTempPassword(body = body) authenticatedAccountsApi
.resetTempPassword(body = body)
.toResult()
} else { } else {
authenticatedAccountsApi.resetPassword(body = body) authenticatedAccountsApi
.resetPassword(body = body)
.toResult()
} }
}
override suspend fun setKeyConnectorKey( override suspend fun setKeyConnectorKey(
accessToken: String, accessToken: String,
body: KeyConnectorKeyRequestJson, body: KeyConnectorKeyRequestJson,
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey( ): Result<Unit> =
body = body, unauthenticatedAccountsApi
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", .setKeyConnectorKey(
) body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
override suspend fun setPassword( override suspend fun setPassword(
body: SetPasswordRequestJson, body: SetPasswordRequestJson,
): Result<Unit> = authenticatedAccountsApi.setPassword(body) ): Result<Unit> = authenticatedAccountsApi
.setPassword(body)
.toResult()
override suspend fun getMasterKeyFromKeyConnector( override suspend fun getMasterKeyFromKeyConnector(
url: String, url: String,
accessToken: String, accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson> = ): Result<KeyConnectorMasterKeyResponseJson> =
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector( unauthenticatedKeyConnectorApi
url = "$url/user-keys", .getMasterKeyFromKeyConnector(
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", url = "$url/user-keys",
) bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
override suspend fun storeMasterKeyToKeyConnector( override suspend fun storeMasterKeyToKeyConnector(
url: String, url: String,
masterKey: String, masterKey: String,
): Result<Unit> = ): Result<Unit> =
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector( authenticatedKeyConnectorApi
url = "$url/user-keys", .storeMasterKeyToKeyConnector(
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), url = "$url/user-keys",
) body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
override suspend fun storeMasterKeyToKeyConnector( override suspend fun storeMasterKeyToKeyConnector(
url: String, url: String,
accessToken: String, accessToken: String,
masterKey: String, masterKey: String,
): Result<Unit> = ): Result<Unit> =
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector( unauthenticatedKeyConnectorApi
url = "$url/user-keys", .storeMasterKeyToKeyConnector(
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken", url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey), bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
) body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
} }

View file

@ -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.api.AuthenticatedAuthRequestsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson 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.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class AuthRequestsServiceImpl( class AuthRequestsServiceImpl(
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi, private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
) : AuthRequestsService { ) : AuthRequestsService {
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> = override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
authenticatedAuthRequestsApi.getAuthRequests() authenticatedAuthRequestsApi
.getAuthRequests()
.toResult()
override suspend fun getAuthRequest( override suspend fun getAuthRequest(
requestId: String, requestId: String,
): Result<AuthRequestsResponseJson.AuthRequest> = ): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId) authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
override suspend fun updateAuthRequest( override suspend fun updateAuthRequest(
requestId: String, requestId: String,
@ -22,13 +27,15 @@ class AuthRequestsServiceImpl(
deviceId: String, deviceId: String,
isApproved: Boolean, isApproved: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> = ): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi.updateAuthRequest( authenticatedAuthRequestsApi
userId = requestId, .updateAuthRequest(
body = AuthRequestUpdateRequestJson( userId = requestId,
key = key, body = AuthRequestUpdateRequestJson(
masterPasswordHash = masterPasswordHash, key = key,
deviceId = deviceId, masterPasswordHash = masterPasswordHash,
isApproved = isApproved, deviceId = deviceId,
), isApproved = isApproved,
) ),
)
.toResult()
} }

View file

@ -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.TrustedDeviceKeysRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson 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.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class DevicesServiceImpl( class DevicesServiceImpl(
private val authenticatedDevicesApi: AuthenticatedDevicesApi, private val authenticatedDevicesApi: AuthenticatedDevicesApi,
@ -13,22 +14,26 @@ class DevicesServiceImpl(
override suspend fun getIsKnownDevice( override suspend fun getIsKnownDevice(
emailAddress: String, emailAddress: String,
deviceId: String, deviceId: String,
): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice( ): Result<Boolean> = unauthenticatedDevicesApi
emailAddress = emailAddress.base64UrlEncode(), .getIsKnownDevice(
deviceId = deviceId, emailAddress = emailAddress.base64UrlEncode(),
) deviceId = deviceId,
)
.toResult()
override suspend fun trustDevice( override suspend fun trustDevice(
appId: String, appId: String,
encryptedUserKey: String, encryptedUserKey: String,
encryptedDevicePublicKey: String, encryptedDevicePublicKey: String,
encryptedDevicePrivateKey: String, encryptedDevicePrivateKey: String,
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys( ): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi
appId = appId, .updateTrustedDeviceKeys(
request = TrustedDeviceKeysRequestJson( appId = appId,
encryptedUserKey = encryptedUserKey, request = TrustedDeviceKeysRequestJson(
encryptedDevicePublicKey = encryptedDevicePublicKey, encryptedUserKey = encryptedUserKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey, encryptedDevicePublicKey = encryptedDevicePublicKey,
), encryptedDevicePrivateKey = encryptedDevicePrivateKey,
) ),
)
.toResult()
} }

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import java.security.MessageDigest import java.security.MessageDigest
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService { class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
@ -17,6 +18,7 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
return api return api
.fetchBreachedPasswords(hashPrefix = hashPrefix) .fetchBreachedPasswords(hashPrefix = hashPrefix)
.toResult()
.mapCatching { responseBody -> .mapCatching { responseBody ->
responseBody.string() responseBody.string()
// First split the response by newline: each hashed password is on a new line. // First split the response by newline: each hashed password is on a new line.

View file

@ -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.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson 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.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.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson 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.auth.datasource.network.model.VerifyEmailTokenResponseJson
@ -68,7 +69,7 @@ interface IdentityService {
*/ */
suspend fun sendVerificationEmail( suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson, body: SendVerificationEmailRequestJson,
): Result<String?> ): Result<SendVerificationEmailResponseJson>
/** /**
* Register a new account to Bitwarden using email verification flow. * Register a new account to Bitwarden using email verification flow.

View file

@ -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.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson 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.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.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson 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.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError 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.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.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -28,12 +30,15 @@ class IdentityServiceImpl(
) : IdentityService { ) : IdentityService {
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> = override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email)) unauthenticatedIdentityApi
.preLogin(PreLoginRequestJson(email = email))
.toResult()
@Suppress("MagicNumber") @Suppress("MagicNumber")
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> = override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
unauthenticatedIdentityApi unauthenticatedIdentityApi
.register(body) .register(body)
.toResult()
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError bitwardenError
@ -75,6 +80,7 @@ class IdentityServiceImpl(
captchaResponse = captchaToken, captchaResponse = captchaToken,
authRequestId = authModel.authRequestId, authRequestId = authModel.authRequestId,
) )
.toResult()
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>( bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
@ -95,6 +101,7 @@ class IdentityServiceImpl(
.prevalidateSso( .prevalidateSso(
organizationIdentifier = organizationIdentifier, organizationIdentifier = organizationIdentifier,
) )
.toResult()
override fun refreshTokenSynchronously( override fun refreshTokenSynchronously(
refreshToken: String, refreshToken: String,
@ -104,7 +111,8 @@ class IdentityServiceImpl(
grantType = "refresh_token", grantType = "refresh_token",
refreshToken = refreshToken, refreshToken = refreshToken,
) )
.executeForResult() .executeForNetworkResult()
.toResult()
@Suppress("MagicNumber") @Suppress("MagicNumber")
override suspend fun registerFinish( override suspend fun registerFinish(
@ -112,6 +120,7 @@ class IdentityServiceImpl(
): Result<RegisterResponseJson> = ): Result<RegisterResponseJson> =
unauthenticatedIdentityApi unauthenticatedIdentityApi
.registerFinish(body) .registerFinish(body)
.toResult()
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError bitwardenError
@ -124,10 +133,20 @@ class IdentityServiceImpl(
override suspend fun sendVerificationEmail( override suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson, body: SendVerificationEmailRequestJson,
): Result<String?> { ): Result<SendVerificationEmailResponseJson> {
return unauthenticatedIdentityApi return unauthenticatedIdentityApi
.sendVerificationEmail(body = body) .sendVerificationEmail(body = body)
.map { it?.content } .toResult()
.map { SendVerificationEmailResponseJson.Success(it?.content) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
}
} }
override suspend fun verifyEmailRegistrationToken( override suspend fun verifyEmailRegistrationToken(
@ -136,9 +155,8 @@ class IdentityServiceImpl(
.verifyEmailToken( .verifyEmailToken(
body = body, body = body,
) )
.map { .toResult()
VerifyEmailTokenResponseJson.Valid .map { VerifyEmailTokenResponseJson.Valid }
}
.recoverCatching { throwable -> .recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError() val bitwardenError = throwable.toBitwardenError()
bitwardenError bitwardenError

View file

@ -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.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson 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.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asFailure
/** /**
@ -24,17 +25,19 @@ class NewAuthRequestServiceImpl(
): Result<AuthRequestsResponseJson.AuthRequest> = ): Result<AuthRequestsResponseJson.AuthRequest> =
when (authRequestType) { when (authRequestType) {
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> { AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
unauthenticatedAuthRequestsApi.createAuthRequest( unauthenticatedAuthRequestsApi
deviceIdentifier = deviceId, .createAuthRequest(
body = AuthRequestRequestJson( deviceIdentifier = deviceId,
email = email, body = AuthRequestRequestJson(
publicKey = publicKey, email = email,
deviceId = deviceId, publicKey = publicKey,
accessCode = accessCode, deviceId = deviceId,
fingerprint = fingerprint, accessCode = accessCode,
type = authRequestType, fingerprint = fingerprint,
), type = authRequestType,
) ),
)
.toResult()
} }
AuthRequestTypeJson.UNLOCK -> { AuthRequestTypeJson.UNLOCK -> {
@ -43,17 +46,19 @@ class NewAuthRequestServiceImpl(
} }
AuthRequestTypeJson.ADMIN_APPROVAL -> { AuthRequestTypeJson.ADMIN_APPROVAL -> {
authenticatedAuthRequestsApi.createAdminAuthRequest( authenticatedAuthRequestsApi
deviceIdentifier = deviceId, .createAdminAuthRequest(
body = AuthRequestRequestJson( deviceIdentifier = deviceId,
email = email, body = AuthRequestRequestJson(
publicKey = publicKey, email = email,
deviceId = deviceId, publicKey = publicKey,
accessCode = accessCode, deviceId = deviceId,
fingerprint = fingerprint, accessCode = accessCode,
type = authRequestType, fingerprint = fingerprint,
), type = authRequestType,
) ),
)
.toResult()
} }
} }
@ -63,11 +68,15 @@ class NewAuthRequestServiceImpl(
isSso: Boolean, isSso: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> = ): Result<AuthRequestsResponseJson.AuthRequest> =
if (isSso) { if (isSso) {
authenticatedAuthRequestsApi.getAuthRequest(requestId) authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
} else { } else {
unauthenticatedAuthRequestsApi.getAuthRequestUpdate( unauthenticatedAuthRequestsApi
requestId = requestId, .getAuthRequestUpdate(
accessCode = accessCode, requestId = requestId,
) accessCode = accessCode,
)
.toResult()
} }
} }

View file

@ -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.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest 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.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/** /**
* Default implementation of [OrganizationService]. * Default implementation of [OrganizationService].
@ -31,6 +32,7 @@ class OrganizationServiceImpl(
resetPasswordKey = resetPasswordKey, resetPasswordKey = resetPasswordKey,
), ),
) )
.toResult()
override suspend fun getOrganizationDomainSsoDetails( override suspend fun getOrganizationDomainSsoDetails(
email: String, email: String,
@ -40,6 +42,7 @@ class OrganizationServiceImpl(
email = email, email = email,
), ),
) )
.toResult()
override suspend fun getOrganizationAutoEnrollStatus( override suspend fun getOrganizationAutoEnrollStatus(
organizationIdentifier: String, organizationIdentifier: String,
@ -47,6 +50,7 @@ class OrganizationServiceImpl(
.getOrganizationAutoEnrollResponse( .getOrganizationAutoEnrollResponse(
organizationIdentifier = organizationIdentifier, organizationIdentifier = organizationIdentifier,
) )
.toResult()
override suspend fun getOrganizationKeys( override suspend fun getOrganizationKeys(
organizationId: String, organizationId: String,
@ -54,6 +58,7 @@ class OrganizationServiceImpl(
.getOrganizationKeys( .getOrganizationKeys(
organizationId = organizationId, organizationId = organizationId,
) )
.toResult()
override suspend fun getVerifiedOrganizationDomainSsoDetails( override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String, email: String,
@ -63,4 +68,5 @@ class OrganizationServiceImpl(
email = email, email = email,
), ),
) )
.toResult()
} }

View file

@ -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.isSso
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.util.asFailure 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.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
@ -65,7 +66,7 @@ class AuthRequestManagerImpl(
email: String, email: String,
authRequestType: AuthRequestType, authRequestType: AuthRequestType,
): Flow<CreateAuthRequestResult> = flow { ): Flow<CreateAuthRequestResult> = flow {
val initialResult = createNewAuthRequest( val initialResult = createNewAuthRequestIfNecessary(
email = email, email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(), authRequestType = authRequestType.toAuthRequestTypeJson(),
) )
@ -74,7 +75,6 @@ class AuthRequestManagerImpl(
emit(CreateAuthRequestResult.Error) emit(CreateAuthRequestResult.Error)
return@flow return@flow
} }
val authRequestResponse = initialResult.authRequestResponse
var authRequest = initialResult.authRequest var authRequest = initialResult.authRequest
emit(CreateAuthRequestResult.Update(authRequest)) emit(CreateAuthRequestResult.Update(authRequest))
@ -84,7 +84,7 @@ class AuthRequestManagerImpl(
newAuthRequestService newAuthRequestService
.getAuthRequestUpdate( .getAuthRequestUpdate(
requestId = authRequest.id, requestId = authRequest.id,
accessCode = authRequestResponse.accessCode, accessCode = initialResult.accessCode,
isSso = authRequestType.isSso, isSso = authRequestType.isSso,
) )
.map { request -> .map { request ->
@ -112,7 +112,8 @@ class AuthRequestManagerImpl(
emit( emit(
CreateAuthRequestResult.Success( CreateAuthRequestResult.Success(
authRequest = updateAuthRequest, 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<NewAuthRequestData> {
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] * Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
* with the [AuthRequest] and [AuthRequestResponse]. * with the [AuthRequest] and [AuthRequestResponse].
@ -381,6 +428,8 @@ class AuthRequestManagerImpl(
pendingAuthRequest = PendingAuthRequestJson( pendingAuthRequest = PendingAuthRequestJson(
requestId = it.id, requestId = it.id,
requestPrivateKey = authRequestResponse.privateKey, requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
), ),
) )
} }
@ -400,7 +449,13 @@ class AuthRequestManagerImpl(
fingerprint = authRequestResponse.fingerprint, fingerprint = authRequestResponse.fingerprint,
) )
} }
.map { NewAuthRequestData(it, authRequestResponse) } .map {
NewAuthRequestData(
authRequest = it,
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
)
}
} }
private suspend fun getFingerprintPhrase( private suspend fun getFingerprintPhrase(
@ -420,5 +475,6 @@ class AuthRequestManagerImpl(
*/ */
private data class NewAuthRequestData( private data class NewAuthRequestData(
val authRequest: AuthRequest, val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse, val privateKey: String,
val accessCode: String,
) )

View file

@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.auth.manager.model package com.x8bit.bitwarden.data.auth.manager.model
import com.bitwarden.core.AuthRequestResponse
/** /**
* Models result of creating a new login approval request. * Models result of creating a new login approval request.
*/ */
@ -18,7 +16,8 @@ sealed class CreateAuthRequestResult {
*/ */
data class Success( data class Success(
val authRequest: AuthRequest, val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse, val privateKey: String,
val accessCode: String,
) : CreateAuthRequestResult() ) : CreateAuthRequestResult()
/** /**

View file

@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.repository
import com.bitwarden.core.AuthRequestMethod import com.bitwarden.core.AuthRequestMethod
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.crypto.HashPurpose import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource 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.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson 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.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.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson 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.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.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson 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.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.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
@ -1249,41 +1248,17 @@ class AuthRepositoryImpl(
?.activeAccount ?.activeAccount
?.profile ?.profile
?: return ValidatePinResult.Error ?: return ValidatePinResult.Error
val privateKey = authDiskSource
.getPrivateKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error
val pinProtectedUserKey = authDiskSource val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId) .getPinProtectedUserKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error ?: 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 return vaultSdkSource
.initializeCrypto( .validatePin(
userId = activeAccount.userId, userId = activeAccount.userId,
request = InitUserCryptoRequest( pin = pin,
kdfParams = activeAccount.toSdkParams(), pinProtectedUserKey = pinProtectedUserKey,
email = activeAccount.email,
privateKey = privateKey,
method = InitUserCryptoMethod.Pin(
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
),
),
) )
.fold( .fold(
onSuccess = { onSuccess = { ValidatePinResult.Success(isValid = it) },
when (it) {
InitializeCryptoResult.Success -> {
ValidatePinResult.Success(isValid = true)
}
is InitializeCryptoResult.AuthenticationError -> {
ValidatePinResult.Success(isValid = false)
}
}
},
onFailure = { ValidatePinResult.Error }, onFailure = { ValidatePinResult.Error },
) )
} }
@ -1302,13 +1277,21 @@ class AuthRepositoryImpl(
.sendVerificationEmail( .sendVerificationEmail(
SendVerificationEmailRequestJson( SendVerificationEmailRequestJson(
email = email, email = email,
name = name, name = name.takeUnless { it.isBlank() },
receiveMarketingEmails = receiveMarketingEmails, receiveMarketingEmails = receiveMarketingEmails,
), ),
) )
.fold( .fold(
onSuccess = { onSuccess = {
SendVerificationEmailResult.Success(it) when (it) {
is SendVerificationEmailResponseJson.Invalid -> {
SendVerificationEmailResult.Error(it.message)
}
is SendVerificationEmailResponseJson.Success -> {
SendVerificationEmailResult.Success(it.emailVerificationToken)
}
}
}, },
onFailure = { onFailure = {
SendVerificationEmailResult.Error(null) SendVerificationEmailResult.Error(null)

View file

@ -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.auth.repository.model.JwtTokenDataJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber
/** /**
* Internal, generally basic [Json] instance for JWT parsing purposes. * 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. * 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? { fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? {
val parts = jwtToken.split(".") 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 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 { return try {
json.decodeFromString<JwtTokenDataJson>(decodedDataJson) json.decodeFromString<JwtTokenDataJson>(decodedDataJson)
} catch (_: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "Failed to decode JwtTokenDataJson")
null null
} }
} }

View file

@ -22,8 +22,7 @@ class BitwardenAccessibilityService : AccessibilityService() {
lateinit var processor: BitwardenAccessibilityProcessor lateinit var processor: BitwardenAccessibilityProcessor
override fun onAccessibilityEvent(event: AccessibilityEvent) { override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (rootInActiveWindow?.packageName != event.packageName) return processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source)
} }
override fun onInterrupt() = Unit override fun onInterrupt() = Unit

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.PowerManager 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.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
@ -55,8 +56,12 @@ object AccessibilityModule {
@Singleton @Singleton
@Provides @Provides
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager = fun providesAccessibilityEnabledManager(
AccessibilityEnabledManagerImpl() accessibilityManager: AccessibilityManager,
): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
)
@Singleton @Singleton
@Provides @Provides
@ -110,6 +115,12 @@ object AccessibilityModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
): PackageManager = context.packageManager ): PackageManager = context.packageManager
@Singleton
@Provides
fun provideAccessibilityManager(
@ApplicationContext context: Context,
): AccessibilityManager = context.getSystemService(AccessibilityManager::class.java)
@Singleton @Singleton
@Provides @Provides
fun providesPowerManager( fun providesPowerManager(

View file

@ -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,
)
}

View file

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

View file

@ -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)
}
}

View file

@ -7,15 +7,7 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
interface AccessibilityEnabledManager { interface AccessibilityEnabledManager {
/** /**
* Whether or not the accessibility service should be considered enabled. * Emits updates that track whether the accessibility autofill service is 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.
*/ */
val isAccessibilityEnabledStateFlow: StateFlow<Boolean> val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -7,14 +8,18 @@ import kotlinx.coroutines.flow.asStateFlow
/** /**
* The default implementation of [AccessibilityEnabledManager]. * The default implementation of [AccessibilityEnabledManager].
*/ */
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager { class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager,
) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false) private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
override var isAccessibilityEnabled: Boolean init {
get() = mutableIsAccessibilityEnabledStateFlow.value accessibilityManager.addAccessibilityStateChangeListener(
set(value) { AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
mutableIsAccessibilityEnabledStateFlow.value = value mutableIsAccessibilityEnabledStateFlow.value = isEnabled
} },
)
}
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean> override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow() get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()

View file

@ -8,4 +8,6 @@ import android.view.accessibility.AccessibilityNodeInfo
data class FillableFields( data class FillableFields(
val usernameField: AccessibilityNodeInfo?, val usernameField: AccessibilityNodeInfo?,
val passwordFields: List<AccessibilityNodeInfo>, val passwordFields: List<AccessibilityNodeInfo>,
) ) {
val hasFields: Boolean = usernameField != null || passwordFields.isNotEmpty()
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
/** /**
@ -7,7 +8,12 @@ import android.view.accessibility.AccessibilityNodeInfo
*/ */
interface BitwardenAccessibilityProcessor { 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?,
)
} }

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.content.Context import android.content.Context
import android.os.PowerManager import android.os.PowerManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast import android.widget.Toast
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
@ -25,37 +26,48 @@ class BitwardenAccessibilityProcessorImpl(
private val launcherPackageNameManager: LauncherPackageNameManager, private val launcherPackageNameManager: LauncherPackageNameManager,
private val powerManager: PowerManager, private val powerManager: PowerManager,
) : BitwardenAccessibilityProcessor { ) : BitwardenAccessibilityProcessor {
override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) { override fun processAccessibilityEvent(
val rootNode = rootAccessibilityNodeInfo ?: return event: AccessibilityEvent,
rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?,
) {
val eventNode = event.source ?: return
// Ignore the event when the phone is inactive // Ignore the event when the phone is inactive
if (!powerManager.isInteractive) return if (!powerManager.isInteractive) return
// We skip if the system package // We skip if the system package
if (rootNode.isSystemPackage) return if (eventNode.isSystemPackage) return
// We skip any package that is a launcher or unsupported // We skip any package that is unsupported
if (rootNode.shouldSkipPackage || if (eventNode.shouldSkipPackage) return
launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName } // We skip any package that is a launcher
) { if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) {
// Clear the action since this event needs to be ignored completely
accessibilityAutofillManager.accessibilityAction = null
return return
} }
// Only process the event if the tile was clicked // Only process the event if the tile was clicked
val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return 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 accessibilityAutofillManager.accessibilityAction = null
when (accessibilityAction) { when (accessibilityAction) {
is AccessibilityAction.AttemptFill -> { 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) { private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
accessibilityParser accessibilityParser
.parseForUriOrPackageName(rootNode = rootNode) .parseForUriOrPackageName(rootNode = rootNode)
?.takeIf {
accessibilityParser
.parseForFillableFields(rootNode = rootNode, uri = it)
.hasFields
}
?.let { uri -> ?.let { uri ->
context.startActivity( context.startActivity(
createAutofillSelectionIntent( createAutofillSelectionIntent(

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api 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.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Url import retrofit2.http.Url
@ -15,5 +16,5 @@ interface DigitalAssetLinkApi {
@GET @GET
suspend fun getDigitalAssetLinks( suspend fun getDigitalAssetLinks(
@Url url: String, @Url url: String,
): Result<List<DigitalAssetLinkResponseJson>> ): NetworkResult<List<DigitalAssetLinkResponseJson>>
} }

View file

@ -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.api.DigitalAssetLinkApi
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson 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]. * Primary implementation of [DigitalAssetLinkService].
@ -18,4 +19,5 @@ class DigitalAssetLinkServiceImpl(
.getDigitalAssetLinks( .getDigitalAssetLinks(
url = "$scheme$relyingParty/.well-known/assetlinks.json", url = "$scheme$relyingParty/.well-known/assetlinks.json",
) )
.toResult()
} }

View file

@ -74,6 +74,11 @@ interface SettingsDiskSource {
*/ */
var lastDatabaseSchemeChangeInstant: Instant? var lastDatabaseSchemeChangeInstant: Instant?
/**
* Emits updates that track [lastDatabaseSchemeChangeInstant].
*/
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
/** /**
* Clears all the settings data for the given user. * Clears all the settings data for the given user.
*/ */

View file

@ -75,6 +75,8 @@ class SettingsDiskSourceImpl(
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>() private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
private val mutableScreenCaptureAllowedFlowMap = private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>() mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@ -158,7 +160,14 @@ class SettingsDiskSourceImpl(
override var lastDatabaseSchemeChangeInstant: Instant? override var lastDatabaseSchemeChangeInstant: Instant?
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) } 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<Instant?>
get() = mutableLastDatabaseSchemeChangeInstantFlow
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
override fun clearData(userId: String) { override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null) storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api 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.ConfigResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET import retrofit2.http.GET
/** /**
@ -9,5 +10,5 @@ import retrofit2.http.GET
interface ConfigApi { interface ConfigApi {
@GET("config") @GET("config")
suspend fun getConfig(): Result<ConfigResponseJson> suspend fun getConfig(): NetworkResult<ConfigResponseJson>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api 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 com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST import retrofit2.http.POST
@ -9,5 +10,7 @@ import retrofit2.http.POST
*/ */
interface EventApi { interface EventApi {
@POST("/collect") @POST("/collect")
suspend fun collectOrganizationEvents(@Body events: List<OrganizationEventJson>): Result<Unit> suspend fun collectOrganizationEvents(
@Body events: List<OrganizationEventJson>,
): NetworkResult<Unit>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api 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 com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.PUT import retrofit2.http.PUT
@ -13,5 +14,5 @@ interface PushApi {
suspend fun putDeviceToken( suspend fun putDeviceToken(
@Path("appId") appId: String, @Path("appId") appId: String,
@Body body: PushTokenRequest, @Body body: PushTokenRequest,
): Result<Unit> ): NetworkResult<Unit>
} }

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core package com.x8bit.bitwarden.data.platform.datasource.network.core
import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.util.asSuccess
import okhttp3.Request import okhttp3.Request
import okio.IOException import okio.IOException
import okio.Timeout import okio.Timeout
@ -18,33 +17,36 @@ import java.lang.reflect.Type
private const val NO_CONTENT_RESPONSE_CODE: Int = 204 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") @Suppress("TooManyFunctions")
class ResultCall<T>( class NetworkResultCall<T>(
private val backingCall: Call<T>, private val backingCall: Call<T>,
private val successType: Type, private val successType: Type,
) : Call<Result<T>> { ) : Call<NetworkResult<T>> {
override fun cancel(): Unit = backingCall.cancel() override fun cancel(): Unit = backingCall.cancel()
override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType) override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(backingCall, successType)
override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue( override fun enqueue(callback: Callback<NetworkResult<T>>): Unit = backingCall.enqueue(
object : Callback<T> { object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) { override fun onResponse(call: Call<T>, response: Response<T>) {
callback.onResponse(this@ResultCall, Response.success(response.toResult())) callback.onResponse(
this@NetworkResultCall,
Response.success(response.toNetworkResult()),
)
} }
override fun onFailure(call: Call<T>, t: Throwable) { override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResultCall, Response.success(t.toFailure())) callback.onResponse(this@NetworkResultCall, Response.success(t.toFailure()))
} }
}, },
) )
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
override fun execute(): Response<Result<T>> = override fun execute(): Response<NetworkResult<T>> =
try { try {
Response.success(backingCall.execute().toResult()) Response.success(backingCall.execute().toNetworkResult())
} catch (ioException: IOException) { } catch (ioException: IOException) {
Response.success(ioException.toFailure()) Response.success(ioException.toFailure())
} catch (runtimeException: RuntimeException) { } catch (runtimeException: RuntimeException) {
@ -60,20 +62,18 @@ class ResultCall<T>(
override fun timeout(): Timeout = backingCall.timeout() 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<T> = requireNotNull(execute().body()) fun executeForResult(): NetworkResult<T> = requireNotNull(execute().body())
private fun Throwable.toFailure(): Result<T> = private fun Throwable.toFailure(): NetworkResult<T> {
this // We rebuild the URL without query params, we do not want to log those
.also { val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
// We rebuild the URL without query params, we do not want to log those Timber.w(this, "Network Error: $url")
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" } return NetworkResult.Failure(this)
Timber.w(it, "Network Error: $url") }
}
.asFailure()
private fun Response<T>.toResult(): Result<T> = private fun Response<T>.toNetworkResult(): NetworkResult<T> =
if (!this.isSuccessful) { if (!this.isSuccessful) {
HttpException(this).toFailure() HttpException(this).toFailure()
} else { } else {
@ -81,11 +81,11 @@ class ResultCall<T>(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
when { when {
// We got a nonnull T as the body, just return it. // 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. // 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. // 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. // All other null bodies result in an error.
else -> IllegalStateException("Unexpected null body!").toFailure() else -> IllegalStateException("Unexpected null body!").toFailure()
} }

View file

@ -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<T>(
private val successType: Type,
) : CallAdapter<T, Call<NetworkResult<T>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<NetworkResult<T>> = NetworkResultCall(call, successType)
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.core package com.x8bit.bitwarden.data.platform.datasource.network.core
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.Call import retrofit2.Call
import retrofit2.CallAdapter import retrofit2.CallAdapter
import retrofit2.Retrofit import retrofit2.Retrofit
@ -7,9 +8,9 @@ import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type 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( override fun get(
returnType: Type, returnType: Type,
annotations: Array<out Annotation>, annotations: Array<out Annotation>,
@ -18,13 +19,13 @@ class ResultCallAdapterFactory : CallAdapter.Factory() {
check(returnType is ParameterizedType) { "$returnType must be parameterized" } check(returnType is ParameterizedType) { "$returnType must be parameterized" }
val containerType = getParameterUpperBound(0, returnType) 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" } check(containerType is ParameterizedType) { "$containerType must be parameterized" }
val requestType = getParameterUpperBound(0, containerType) val requestType = getParameterUpperBound(0, containerType)
return if (getRawType(returnType) == Call::class.java) { return if (getRawType(returnType) == Call::class.java) {
ResultCallAdapter<Any>(successType = requestType) NetworkResultCallAdapter<Any>(successType = requestType)
} else { } else {
null null
} }

View file

@ -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<T>(
private val successType: Type,
) : CallAdapter<T, Call<Result<T>>> {
override fun responseType(): Type = successType
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
}

View file

@ -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<out T> {
/**
* A successful network result with the relevant [T] data.
*/
data class Success<T>(val value: T) : NetworkResult<T>()
/**
* A failed network result with the relevant [throwable] error.
*/
data class Failure(val throwable: Throwable) : NetworkResult<Nothing>()
}

View file

@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit 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.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.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
@ -105,7 +105,7 @@ class RetrofitsImpl(
private val baseRetrofitBuilder: Retrofit.Builder by lazy { private val baseRetrofitBuilder: Retrofit.Builder by lazy {
Retrofit.Builder() Retrofit.Builder()
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addCallAdapterFactory(ResultCallAdapterFactory()) .addCallAdapterFactory(NetworkResultCallAdapterFactory())
.client(baseOkHttpClient) .client(baseOkHttpClient)
} }

View file

@ -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.api.ConfigApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson 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 { class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig() override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig().toResult()
} }

View file

@ -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.api.EventApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson 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]. * The default implementation of the [EventService].
@ -11,5 +12,5 @@ class EventServiceImpl(
) : EventService { ) : EventService {
override suspend fun sendOrganizationEvents( override suspend fun sendOrganizationEvents(
events: List<OrganizationEventJson>, events: List<OrganizationEventJson>,
): Result<Unit> = eventApi.collectOrganizationEvents(events = events) ): Result<Unit> = eventApi.collectOrganizationEvents(events = events).toResult()
} }

View file

@ -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.api.PushApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class PushServiceImpl( class PushServiceImpl(
private val pushApi: PushApi, private val pushApi: PushApi,
@ -15,4 +16,5 @@ class PushServiceImpl(
appId = appId, appId = appId,
body = body, body = body,
) )
.toResult()
} }

View file

@ -1,21 +1,22 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util 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 import retrofit2.Call
/** /**
* Synchronously executes the [Call] and returns the [Result]. * Synchronously executes the [Call] and returns the [NetworkResult].
*/ */
inline fun <reified T : Any> Call<T>.executeForResult(): Result<T> = inline fun <reified T : Any> Call<T>.executeForNetworkResult(): NetworkResult<T> =
this this
.toResultCall() .toNetworkResultCall()
.executeForResult() .executeForResult()
/** /**
* Wraps the existing [Call] in a [ResultCall]. * Wraps the existing [Call] in a [NetworkResultCall].
*/ */
inline fun <reified T : Any> Call<T>.toResultCall(): ResultCall<T> = inline fun <reified T : Any> Call<T>.toNetworkResultCall(): NetworkResultCall<T> =
ResultCall( NetworkResultCall(
backingCall = this, backingCall = this,
successType = T::class.java, successType = T::class.java,
) )

View file

@ -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 <T> NetworkResult<T>.toResult(): Result<T> =
when (this) {
is NetworkResult.Failure -> this.throwable.asFailure()
is NetworkResult.Success -> this.value.asSuccess()
}

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager package com.x8bit.bitwarden.data.platform.manager
import kotlinx.coroutines.flow.Flow
import java.time.Instant 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. * that a scheme change to any database will update this value and trigger a sync.
*/ */
var lastDatabaseSchemeChangeInstant: Instant? var lastDatabaseSchemeChangeInstant: Instant?
/**
* A flow of the last database schema change instant.
*/
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
} }

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.data.platform.manager package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource 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 import java.time.Instant
/** /**
@ -8,10 +12,23 @@ import java.time.Instant
*/ */
class DatabaseSchemeManagerImpl( class DatabaseSchemeManagerImpl(
val settingsDiskSource: SettingsDiskSource, val settingsDiskSource: SettingsDiskSource,
val dispatcherManager: DispatcherManager,
) : DatabaseSchemeManager { ) : DatabaseSchemeManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
override var lastDatabaseSchemeChangeInstant: Instant? override var lastDatabaseSchemeChangeInstant: Instant?
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
set(value) { set(value) {
settingsDiskSource.lastDatabaseSchemeChangeInstant = value settingsDiskSource.lastDatabaseSchemeChangeInstant = value
} }
override val lastDatabaseSchemeChangeInstantFlow =
settingsDiskSource
.lastDatabaseSchemeChangeInstantFlow
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = settingsDiskSource.lastDatabaseSchemeChangeInstant,
)
} }

View file

@ -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.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow 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.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
@ -30,6 +31,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
private val settingsDiskSource: SettingsDiskSource, private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource, private val vaultDiskSource: VaultDiskSource,
private val featureFlagManager: FeatureFlagManager, private val featureFlagManager: FeatureFlagManager,
private val autofillEnabledManager: AutofillEnabledManager,
) : FirstTimeActionManager { ) : FirstTimeActionManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
@ -78,7 +80,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
.filterNotNull() .filterNotNull()
.flatMapLatest { .flatMapLatest {
// Can be expanded to support multiple autofill settings // Can be expanded to support multiple autofill settings
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = it) getShowAutofillSettingBadgeFlowInternal(userId = it)
.map { showAutofillBadge -> .map { showAutofillBadge ->
listOfNotNull(showAutofillBadge) listOfNotNull(showAutofillBadge)
} }
@ -128,7 +130,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
listOf( listOf(
getShowImportLoginsFlowInternal(userId = activeUserId), getShowImportLoginsFlowInternal(userId = activeUserId),
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId), settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = activeUserId), getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId), getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
), ),
) { ) {
@ -165,7 +167,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
FirstTimeState( FirstTimeState(
showImportLoginsCard = authDiskSource.getShowImportLogins(it), showImportLoginsCard = authDiskSource.getShowImportLogins(it),
showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it), showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it),
showSetupAutofillCard = settingsDiskSource.getShowAutoFillSettingBadge(it), showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
showImportLoginsCardInSettings = settingsDiskSource showImportLoginsCardInSettings = settingsDiskSource
.getShowImportLoginsSettingBadge(it), .getShowImportLoginsSettingBadge(it),
) )
@ -236,4 +238,23 @@ class FirstTimeActionManagerImpl @Inject constructor(
showImportLogins ?: false && ciphers.isEmpty() 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<Boolean> {
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
}
} }

View file

@ -6,6 +6,7 @@ import androidx.core.content.getSystemService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository 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.EventDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
@ -293,19 +294,23 @@ object PlatformManagerModule {
vaultDiskSource: VaultDiskSource, vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager, featureFlagManager: FeatureFlagManager,
autofillEnabledManager: AutofillEnabledManager,
): FirstTimeActionManager = FirstTimeActionManagerImpl( ): FirstTimeActionManager = FirstTimeActionManagerImpl(
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource, settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
autofillEnabledManager = autofillEnabledManager,
) )
@Provides @Provides
@Singleton @Singleton
fun provideDatabaseSchemeManager( fun provideDatabaseSchemeManager(
settingsDiskSource: SettingsDiskSource, settingsDiskSource: SettingsDiskSource,
dispatcherManager: DispatcherManager,
): DatabaseSchemeManager = DatabaseSchemeManagerImpl( ): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
settingsDiskSource = settingsDiskSource, settingsDiskSource = settingsDiskSource,
dispatcherManager = dispatcherManager,
) )
} }

View file

@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
/** /**
* Default implementation of [AuthenticatorBridgeProcessor]. * Default implementation of [AuthenticatorBridgeProcessor].
@ -93,7 +94,13 @@ class AuthenticatorBridgeProcessorImpl(
} }
override fun syncAccounts() { 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 { scope.launch {
// Encrypt the shared account data with the symmetric key: // Encrypt the shared account data with the symmetric key:
val encryptedSharedAccountData = authenticatorBridgeRepository val encryptedSharedAccountData = authenticatorBridgeRepository
@ -110,14 +117,31 @@ class AuthenticatorBridgeProcessorImpl(
} }
override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean { 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 intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext)
val totpData = data.decrypt(symmetricEncryptionKey) val totpData = data.decrypt(symmetricEncryptionKey)
.onFailure {
Timber.e(t = it, message = "Unable to decrypt TOTP data.")
return false
}
.getOrNull() .getOrNull()
?.totpUri ?.totpUri
?.toUri() ?.toUri()
?.getTotpDataOrNull() ?.getTotpDataOrNull()
?: return false ?: run {
Timber.e(
t = IllegalStateException(),
message = "Unable to parse TOTP URI.",
)
return false
}
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData
applicationContext.startActivity(intent) applicationContext.startActivity(intent)
return true return true

View file

@ -14,9 +14,16 @@ inline fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> =
/** /**
* Returns the given receiver of type [T] as a "success" [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> T.asSuccess(): Result<T> = fun <T> T.asSuccess(): Result<T> = if (this is Result<*>) {
@Suppress("UNCHECKED_CAST")
this as Result<T>
} else {
Result.success(this) Result.success(this)
}
/** /**
* Returns the given [Throwable] as a "failure" [Result]. * Returns the given [Throwable] as a "failure" [Result].

View file

@ -17,7 +17,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@ -125,7 +124,6 @@ class VaultDiskSourceImpl(
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains?> = override fun getDomains(userId: String): Flow<SyncResponseJson.Domains?> =
domainsDao domainsDao
.getDomains(userId) .getDomains(userId)
.filterNotNull()
.map { entity -> .map { entity ->
withContext(dispatcherManager.default) { withContext(dispatcherManager.default) {
entity?.domainsJson?.let { json.decodeFromString<SyncResponseJson.Domains>(it) } entity?.domainsJson?.let { json.decodeFromString<SyncResponseJson.Domains>(it) }

View file

@ -26,7 +26,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
FolderEntity::class, FolderEntity::class,
SendEntity::class, SendEntity::class,
], ],
version = 5, version = 6,
exportSchema = true, exportSchema = true,
) )
@TypeConverters(ZonedDateTimeTypeConverter::class) @TypeConverters(ZonedDateTimeTypeConverter::class)

View file

@ -32,5 +32,5 @@ data class CollectionEntity(
val isReadOnly: Boolean, val isReadOnly: Boolean,
@ColumnInfo(name = "manage") @ColumnInfo(name = "manage")
val canManage: Boolean, val canManage: Boolean?,
) )

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api package com.x8bit.bitwarden.data.vault.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Header import retrofit2.http.Header
@ -21,5 +22,5 @@ interface AzureApi {
@Header("x-ms-date") date: String, @Header("x-ms-date") date: String,
@Header("x-ms-version") version: String?, @Header("x-ms-version") version: String?,
@Body body: RequestBody, @Body body: RequestBody,
): Result<Unit> ): NetworkResult<Unit>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api 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.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
@ -26,7 +27,7 @@ interface CiphersApi {
* Create a cipher. * Create a cipher.
*/ */
@POST("ciphers") @POST("ciphers")
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher> suspend fun createCipher(@Body body: CipherJsonRequest): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Create a cipher that belongs to an organization. * Create a cipher that belongs to an organization.
@ -34,7 +35,7 @@ interface CiphersApi {
@POST("ciphers/create") @POST("ciphers/create")
suspend fun createCipherInOrganization( suspend fun createCipherInOrganization(
@Body body: CreateCipherInOrganizationJsonRequest, @Body body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher> ): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Associates an attachment with a cipher. * Associates an attachment with a cipher.
@ -43,7 +44,7 @@ interface CiphersApi {
suspend fun createAttachment( suspend fun createAttachment(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Body body: AttachmentJsonRequest, @Body body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse> ): NetworkResult<AttachmentJsonResponse>
/** /**
* Uploads the attachment associated with a cipher. * Uploads the attachment associated with a cipher.
@ -53,7 +54,7 @@ interface CiphersApi {
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String, @Path("attachmentId") attachmentId: String,
@Body body: MultipartBody, @Body body: MultipartBody,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Updates a cipher. * Updates a cipher.
@ -62,7 +63,7 @@ interface CiphersApi {
suspend fun updateCipher( suspend fun updateCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Body body: CipherJsonRequest, @Body body: CipherJsonRequest,
): Result<SyncResponseJson.Cipher> ): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Shares a cipher. * Shares a cipher.
@ -71,7 +72,7 @@ interface CiphersApi {
suspend fun shareCipher( suspend fun shareCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Body body: ShareCipherJsonRequest, @Body body: ShareCipherJsonRequest,
): Result<SyncResponseJson.Cipher> ): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Shares an attachment. * Shares an attachment.
@ -82,7 +83,7 @@ interface CiphersApi {
@Path("attachmentId") attachmentId: String, @Path("attachmentId") attachmentId: String,
@Query("organizationId") organizationId: String?, @Query("organizationId") organizationId: String?,
@Body body: MultipartBody, @Body body: MultipartBody,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Updates a cipher's collections. * Updates a cipher's collections.
@ -91,7 +92,7 @@ interface CiphersApi {
suspend fun updateCipherCollections( suspend fun updateCipherCollections(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Body body: UpdateCipherCollectionsJsonRequest, @Body body: UpdateCipherCollectionsJsonRequest,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Hard deletes a cipher. * Hard deletes a cipher.
@ -99,7 +100,7 @@ interface CiphersApi {
@DELETE("ciphers/{cipherId}") @DELETE("ciphers/{cipherId}")
suspend fun hardDeleteCipher( suspend fun hardDeleteCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Soft deletes a cipher. * Soft deletes a cipher.
@ -107,7 +108,7 @@ interface CiphersApi {
@PUT("ciphers/{cipherId}/delete") @PUT("ciphers/{cipherId}/delete")
suspend fun softDeleteCipher( suspend fun softDeleteCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Deletes an attachment from a cipher. * Deletes an attachment from a cipher.
@ -116,7 +117,7 @@ interface CiphersApi {
suspend fun deleteCipherAttachment( suspend fun deleteCipherAttachment(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String, @Path("attachmentId") attachmentId: String,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Restores a cipher. * Restores a cipher.
@ -124,7 +125,7 @@ interface CiphersApi {
@PUT("ciphers/{cipherId}/restore") @PUT("ciphers/{cipherId}/restore")
suspend fun restoreCipher( suspend fun restoreCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
): Result<SyncResponseJson.Cipher> ): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Gets a cipher. * Gets a cipher.
@ -132,7 +133,7 @@ interface CiphersApi {
@GET("ciphers/{cipherId}") @GET("ciphers/{cipherId}")
suspend fun getCipher( suspend fun getCipher(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
): Result<SyncResponseJson.Cipher> ): NetworkResult<SyncResponseJson.Cipher>
/** /**
* Gets a cipher attachment. * Gets a cipher attachment.
@ -141,11 +142,11 @@ interface CiphersApi {
suspend fun getCipherAttachment( suspend fun getCipherAttachment(
@Path("cipherId") cipherId: String, @Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String, @Path("attachmentId") attachmentId: String,
): Result<SyncResponseJson.Cipher.Attachment> ): NetworkResult<SyncResponseJson.Cipher.Attachment>
/** /**
* Indicates if the active user has unassigned ciphers. * Indicates if the active user has unassigned ciphers.
*/ */
@GET("ciphers/has-unassigned-ciphers") @GET("ciphers/has-unassigned-ciphers")
suspend fun hasUnassignedCiphers(): Result<Boolean> suspend fun hasUnassignedCiphers(): NetworkResult<Boolean>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api package com.x8bit.bitwarden.data.vault.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Streaming import retrofit2.http.Streaming
@ -16,5 +17,5 @@ interface DownloadApi {
@Streaming @Streaming
suspend fun getDataStream( suspend fun getDataStream(
@Url url: String, @Url url: String,
): Result<ResponseBody> ): NetworkResult<ResponseBody>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api 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.FolderJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import retrofit2.http.Body import retrofit2.http.Body
@ -18,7 +19,7 @@ interface FoldersApi {
* Create a folder. * Create a folder.
*/ */
@POST("folders") @POST("folders")
suspend fun createFolder(@Body body: FolderJsonRequest): Result<SyncResponseJson.Folder> suspend fun createFolder(@Body body: FolderJsonRequest): NetworkResult<SyncResponseJson.Folder>
/** /**
* Gets a folder. * Gets a folder.
@ -26,7 +27,7 @@ interface FoldersApi {
@GET("folders/{folderId}") @GET("folders/{folderId}")
suspend fun getFolder( suspend fun getFolder(
@Path("folderId") folderId: String, @Path("folderId") folderId: String,
): Result<SyncResponseJson.Folder> ): NetworkResult<SyncResponseJson.Folder>
/** /**
* Updates a folder. * Updates a folder.
@ -35,11 +36,11 @@ interface FoldersApi {
suspend fun updateFolder( suspend fun updateFolder(
@Path("folderId") folderId: String, @Path("folderId") folderId: String,
@Body body: FolderJsonRequest, @Body body: FolderJsonRequest,
): Result<SyncResponseJson.Folder> ): NetworkResult<SyncResponseJson.Folder>
/** /**
* Deletes a folder. * Deletes a folder.
*/ */
@DELETE("folders/{folderId}") @DELETE("folders/{folderId}")
suspend fun deleteFolder(@Path("folderId") folderId: String): Result<Unit> suspend fun deleteFolder(@Path("folderId") folderId: String): NetworkResult<Unit>
} }

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api package com.x8bit.bitwarden.data.vault.datasource.network.api
import androidx.annotation.Keep 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.CreateFileSendResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -22,13 +23,15 @@ interface SendsApi {
* Create a text send. * Create a text send.
*/ */
@POST("sends") @POST("sends")
suspend fun createTextSend(@Body body: SendJsonRequest): Result<SyncResponseJson.Send> suspend fun createTextSend(@Body body: SendJsonRequest): NetworkResult<SyncResponseJson.Send>
/** /**
* Create a file send. * Create a file send.
*/ */
@POST("sends/file/v2") @POST("sends/file/v2")
suspend fun createFileSend(@Body body: SendJsonRequest): Result<CreateFileSendResponseJson> suspend fun createFileSend(
@Body body: SendJsonRequest,
): NetworkResult<CreateFileSendResponseJson>
/** /**
* Updates a send. * Updates a send.
@ -37,7 +40,7 @@ interface SendsApi {
suspend fun updateSend( suspend fun updateSend(
@Path("sendId") sendId: String, @Path("sendId") sendId: String,
@Body body: SendJsonRequest, @Body body: SendJsonRequest,
): Result<SyncResponseJson.Send> ): NetworkResult<SyncResponseJson.Send>
/** /**
* Uploads the file associated with a send. * Uploads the file associated with a send.
@ -47,23 +50,25 @@ interface SendsApi {
@Path("sendId") sendId: String, @Path("sendId") sendId: String,
@Path("fileId") fileId: String, @Path("fileId") fileId: String,
@Body body: MultipartBody, @Body body: MultipartBody,
): Result<Unit> ): NetworkResult<Unit>
/** /**
* Deletes a send. * Deletes a send.
*/ */
@DELETE("sends/{sendId}") @DELETE("sends/{sendId}")
suspend fun deleteSend(@Path("sendId") sendId: String): Result<Unit> suspend fun deleteSend(@Path("sendId") sendId: String): NetworkResult<Unit>
/** /**
* Deletes a send. * Deletes a send.
*/ */
@PUT("sends/{sendId}/remove-password") @PUT("sends/{sendId}/remove-password")
suspend fun removeSendPassword(@Path("sendId") sendId: String): Result<SyncResponseJson.Send> suspend fun removeSendPassword(
@Path("sendId") sendId: String,
): NetworkResult<SyncResponseJson.Send>
/** /**
* Gets a send. * Gets a send.
*/ */
@GET("sends/{sendId}") @GET("sends/{sendId}")
suspend fun getSend(@Path("sendId") sendId: String): Result<SyncResponseJson.Send> suspend fun getSend(@Path("sendId") sendId: String): NetworkResult<SyncResponseJson.Send>
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api 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 com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import retrofit2.http.GET import retrofit2.http.GET
@ -13,8 +14,8 @@ interface SyncApi {
* @return A [SyncResponseJson] containing the vault response model. * @return A [SyncResponseJson] containing the vault response model.
*/ */
@GET("sync") @GET("sync")
suspend fun sync(): Result<SyncResponseJson> suspend fun sync(): NetworkResult<SyncResponseJson>
@GET("/accounts/revision-date") @GET("/accounts/revision-date")
suspend fun getAccountRevisionDateMillis(): Result<Long> suspend fun getAccountRevisionDateMillis(): NetworkResult<Long>
} }

View file

@ -1,8 +1,10 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import java.time.ZonedDateTime 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 domains A domains object associated with the vault data.
* @property sends A list of send objects associated with the vault data (nullable). * @property sends A list of send objects associated with the vault data (nullable).
*/ */
@OptIn(ExperimentalSerializationApi::class)
@Serializable @Serializable
data class SyncResponseJson( data class SyncResponseJson(
@SerialName("folders") @SerialName("folders")
@ -30,6 +33,7 @@ data class SyncResponseJson(
val collections: List<Collection>?, val collections: List<Collection>?,
@SerialName("profile") @SerialName("profile")
@JsonNames("Profile")
val profile: Profile, val profile: Profile,
@SerialName("ciphers") @SerialName("ciphers")
@ -39,6 +43,7 @@ data class SyncResponseJson(
val policies: List<Policy>?, val policies: List<Policy>?,
@SerialName("domains") @SerialName("domains")
@JsonNames("Domains")
val domains: Domains?, val domains: Domains?,
@SerialName("sends") @SerialName("sends")
@ -971,6 +976,6 @@ data class SyncResponseJson(
val id: String, val id: String,
@SerialName("manage") @SerialName("manage")
val canManage: Boolean, val canManage: Boolean?,
) )
} }

View file

@ -4,6 +4,7 @@ import androidx.core.net.toUri
import com.bitwarden.vault.Attachment import com.bitwarden.vault.Attachment
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError 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.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.asFailure 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.AzureApi
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
@ -34,20 +35,26 @@ class CiphersServiceImpl(
private val clock: Clock, private val clock: Clock,
) : CiphersService { ) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> = override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body) ciphersApi
.createCipher(body = body)
.toResult()
override suspend fun createCipherInOrganization( override suspend fun createCipherInOrganization(
body: CreateCipherInOrganizationJsonRequest, body: CreateCipherInOrganizationJsonRequest,
): Result<SyncResponseJson.Cipher> = ciphersApi.createCipherInOrganization(body = body) ): Result<SyncResponseJson.Cipher> = ciphersApi
.createCipherInOrganization(body = body)
.toResult()
override suspend fun createAttachment( override suspend fun createAttachment(
cipherId: String, cipherId: String,
body: AttachmentJsonRequest, body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse> = ): Result<AttachmentJsonResponse> =
ciphersApi.createAttachment( ciphersApi
cipherId = cipherId, .createAttachment(
body = body, cipherId = cipherId,
) body = body,
)
.toResult()
override suspend fun uploadAttachment( override suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse, attachmentJsonResponse: AttachmentJsonResponse,
@ -82,6 +89,7 @@ class CiphersServiceImpl(
) )
} }
} }
.toResult()
.map { cipher } .map { cipher }
} }
@ -94,6 +102,7 @@ class CiphersServiceImpl(
cipherId = cipherId, cipherId = cipherId,
body = body, body = body,
) )
.toResult()
.map { UpdateCipherResponseJson.Success(cipher = it) } .map { UpdateCipherResponseJson.Success(cipher = it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
@ -115,77 +124,97 @@ class CiphersServiceImpl(
?: return IllegalStateException("Attachment must have ID").asFailure() ?: return IllegalStateException("Attachment must have ID").asFailure()
val attachmentKey = attachment.key val attachmentKey = attachment.key
?: return IllegalStateException("Attachment must have Key").asFailure() ?: return IllegalStateException("Attachment must have Key").asFailure()
return ciphersApi.shareAttachment( return ciphersApi
cipherId = cipherId, .shareAttachment(
attachmentId = attachmentId, cipherId = cipherId,
organizationId = organizationId, attachmentId = attachmentId,
body = this organizationId = organizationId,
.createMultipartBodyBuilder( body = this
encryptedFile = encryptedFile, .createMultipartBodyBuilder(
filename = attachment.fileName, encryptedFile = encryptedFile,
) filename = attachment.fileName,
.addPart( )
part = MultipartBody.Part.createFormData( .addPart(
name = "key", part = MultipartBody.Part.createFormData(
value = attachmentKey, name = "key",
), value = attachmentKey,
) ),
.build(), )
) .build(),
)
.toResult()
} }
override suspend fun shareCipher( override suspend fun shareCipher(
cipherId: String, cipherId: String,
body: ShareCipherJsonRequest, body: ShareCipherJsonRequest,
): Result<SyncResponseJson.Cipher> = ): Result<SyncResponseJson.Cipher> =
ciphersApi.shareCipher( ciphersApi
cipherId = cipherId, .shareCipher(
body = body, cipherId = cipherId,
) body = body,
)
.toResult()
override suspend fun updateCipherCollections( override suspend fun updateCipherCollections(
cipherId: String, cipherId: String,
body: UpdateCipherCollectionsJsonRequest, body: UpdateCipherCollectionsJsonRequest,
): Result<Unit> = ): Result<Unit> =
ciphersApi.updateCipherCollections( ciphersApi
cipherId = cipherId, .updateCipherCollections(
body = body, cipherId = cipherId,
) body = body,
)
.toResult()
override suspend fun hardDeleteCipher(cipherId: String): Result<Unit> = override suspend fun hardDeleteCipher(cipherId: String): Result<Unit> =
ciphersApi.hardDeleteCipher(cipherId = cipherId) ciphersApi
.hardDeleteCipher(cipherId = cipherId)
.toResult()
override suspend fun softDeleteCipher(cipherId: String): Result<Unit> = override suspend fun softDeleteCipher(cipherId: String): Result<Unit> =
ciphersApi.softDeleteCipher(cipherId = cipherId) ciphersApi
.softDeleteCipher(cipherId = cipherId)
.toResult()
override suspend fun deleteCipherAttachment( override suspend fun deleteCipherAttachment(
cipherId: String, cipherId: String,
attachmentId: String, attachmentId: String,
): Result<Unit> = ): Result<Unit> =
ciphersApi.deleteCipherAttachment( ciphersApi
cipherId = cipherId, .deleteCipherAttachment(
attachmentId = attachmentId, cipherId = cipherId,
) attachmentId = attachmentId,
)
.toResult()
override suspend fun restoreCipher(cipherId: String): Result<SyncResponseJson.Cipher> = override suspend fun restoreCipher(cipherId: String): Result<SyncResponseJson.Cipher> =
ciphersApi.restoreCipher(cipherId = cipherId) ciphersApi
.restoreCipher(cipherId = cipherId)
.toResult()
override suspend fun getCipher( override suspend fun getCipher(
cipherId: String, cipherId: String,
): Result<SyncResponseJson.Cipher> = ): Result<SyncResponseJson.Cipher> =
ciphersApi.getCipher(cipherId = cipherId) ciphersApi
.getCipher(cipherId = cipherId)
.toResult()
override suspend fun getCipherAttachment( override suspend fun getCipherAttachment(
cipherId: String, cipherId: String,
attachmentId: String, attachmentId: String,
): Result<SyncResponseJson.Cipher.Attachment> = ): Result<SyncResponseJson.Cipher.Attachment> =
ciphersApi.getCipherAttachment( ciphersApi
cipherId = cipherId, .getCipherAttachment(
attachmentId = attachmentId, cipherId = cipherId,
) attachmentId = attachmentId,
)
.toResult()
override suspend fun hasUnassignedCiphers(): Result<Boolean> = override suspend fun hasUnassignedCiphers(): Result<Boolean> =
ciphersApi.hasUnassignedCiphers() ciphersApi
.hasUnassignedCiphers()
.toResult()
private fun createMultipartBodyBuilder( private fun createMultipartBodyBuilder(
encryptedFile: File, encryptedFile: File,

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service 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 com.x8bit.bitwarden.data.vault.datasource.network.api.DownloadApi
import okhttp3.ResponseBody import okhttp3.ResponseBody
@ -12,5 +13,7 @@ class DownloadServiceImpl(
override suspend fun getDataStream( override suspend fun getDataStream(
url: String, url: String,
): Result<ResponseBody> = ): Result<ResponseBody> =
downloadApi.getDataStream(url = url) downloadApi
.getDataStream(url = url)
.toResult()
} }

View file

@ -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.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull 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.api.FoldersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -13,7 +14,9 @@ class FolderServiceImpl(
private val json: Json, private val json: Json,
) : FolderService { ) : FolderService {
override suspend fun createFolder(body: FolderJsonRequest): Result<SyncResponseJson.Folder> = override suspend fun createFolder(body: FolderJsonRequest): Result<SyncResponseJson.Folder> =
foldersApi.createFolder(body = body) foldersApi
.createFolder(body = body)
.toResult()
override suspend fun updateFolder( override suspend fun updateFolder(
folderId: String, folderId: String,
@ -24,6 +27,7 @@ class FolderServiceImpl(
folderId = folderId, folderId = folderId,
body = body, body = body,
) )
.toResult()
.map { UpdateFolderResponseJson.Success(folder = it) } .map { UpdateFolderResponseJson.Success(folder = it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
@ -36,10 +40,13 @@ class FolderServiceImpl(
} }
override suspend fun deleteFolder(folderId: String): Result<Unit> = override suspend fun deleteFolder(folderId: String): Result<Unit> =
foldersApi.deleteFolder(folderId = folderId) foldersApi
.deleteFolder(folderId = folderId)
.toResult()
override suspend fun getFolder( override suspend fun getFolder(
folderId: String, folderId: String,
): Result<SyncResponseJson.Folder> = foldersApi ): Result<SyncResponseJson.Folder> = foldersApi
.getFolder(folderId = folderId) .getFolder(folderId = folderId)
.toResult()
} }

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError 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.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.AzureApi
import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
@ -34,7 +35,9 @@ class SendsServiceImpl(
override suspend fun createTextSend( override suspend fun createTextSend(
body: SendJsonRequest, body: SendJsonRequest,
): Result<CreateSendJsonResponse> = ): Result<CreateSendJsonResponse> =
sendsApi.createTextSend(body = body) sendsApi
.createTextSend(body = body)
.toResult()
.map { CreateSendJsonResponse.Success(send = it) } .map { CreateSendJsonResponse.Success(send = it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable.toBitwardenError() throwable.toBitwardenError()
@ -48,7 +51,9 @@ class SendsServiceImpl(
override suspend fun createFileSend( override suspend fun createFileSend(
body: SendJsonRequest, body: SendJsonRequest,
): Result<CreateFileSendResponse> = ): Result<CreateFileSendResponse> =
sendsApi.createFileSend(body = body) sendsApi
.createFileSend(body = body)
.toResult()
.map { CreateFileSendResponse.Success(it) } .map { CreateFileSendResponse.Success(it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable.toBitwardenError() throwable.toBitwardenError()
@ -68,6 +73,7 @@ class SendsServiceImpl(
sendId = sendId, sendId = sendId,
body = body, body = body,
) )
.toResult()
.map { UpdateSendResponseJson.Success(send = it) } .map { UpdateSendResponseJson.Success(send = it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
@ -118,16 +124,20 @@ class SendsServiceImpl(
) )
} }
} }
.toResult()
.onFailure { sendsApi.deleteSend(send.id) } .onFailure { sendsApi.deleteSend(send.id) }
.map { send } .map { send }
} }
override suspend fun deleteSend(sendId: String): Result<Unit> = override suspend fun deleteSend(sendId: String): Result<Unit> =
sendsApi.deleteSend(sendId = sendId) sendsApi
.deleteSend(sendId = sendId)
.toResult()
override suspend fun removeSendPassword(sendId: String): Result<UpdateSendResponseJson> = override suspend fun removeSendPassword(sendId: String): Result<UpdateSendResponseJson> =
sendsApi sendsApi
.removeSendPassword(sendId = sendId) .removeSendPassword(sendId = sendId)
.toResult()
.map { UpdateSendResponseJson.Success(send = it) } .map { UpdateSendResponseJson.Success(send = it) }
.recoverCatching { throwable -> .recoverCatching { throwable ->
throwable throwable
@ -142,5 +152,7 @@ class SendsServiceImpl(
override suspend fun getSend( override suspend fun getSend(
sendId: String, sendId: String,
): Result<SyncResponseJson.Send> = ): Result<SyncResponseJson.Send> =
sendsApi.getSend(sendId = sendId) sendsApi
.getSend(sendId = sendId)
.toResult()
} }

View file

@ -1,13 +1,18 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service 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.api.SyncApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
class SyncServiceImpl( class SyncServiceImpl(
private val syncApi: SyncApi, private val syncApi: SyncApi,
) : SyncService { ) : SyncService {
override suspend fun sync(): Result<SyncResponseJson> = syncApi.sync() override suspend fun sync(): Result<SyncResponseJson> = syncApi
.sync()
.toResult()
override suspend fun getAccountRevisionDateMillis(): Result<Long> = override suspend fun getAccountRevisionDateMillis(): Result<Long> =
syncApi.getAccountRevisionDateMillis() syncApi
.getAccountRevisionDateMillis()
.toResult()
} }

View file

@ -94,6 +94,15 @@ interface VaultSdkSource {
encryptedPin: String, encryptedPin: String,
): Result<String> ): Result<String>
/**
* Validate the user pin using the [pinProtectedUserKey].
*/
suspend fun validatePin(
userId: String,
pin: String,
pinProtectedUserKey: String,
): Result<Boolean>
/** /**
* Gets the key for an auth request that is required to approve or decline it. * Gets the key for an auth request that is required to approve or decline it.
*/ */

View file

@ -109,6 +109,17 @@ class VaultSdkSourceImpl(
.derivePinUserKey(encryptedPin = encryptedPin) .derivePinUserKey(encryptedPin = encryptedPin)
} }
override suspend fun validatePin(
userId: String,
pin: String,
pinProtectedUserKey: String,
): Result<Boolean> =
runCatchingWithLogs {
getClient(userId = userId)
.auth()
.validatePin(pin = pin, pinProtectedUserKey = pinProtectedUserKey)
}
override suspend fun getAuthRequestKey( override suspend fun getAuthRequestKey(
publicKey: String, publicKey: String,
userId: String, userId: String,

View file

@ -99,6 +99,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -323,6 +324,12 @@ class VaultRepositoryImpl(
.syncFolderUpsertFlow .syncFolderUpsertFlow
.onEach(::syncFolderIfNecessary) .onEach(::syncFolderIfNecessary)
.launchIn(ioScope) .launchIn(ioScope)
databaseSchemeManager
.lastDatabaseSchemeChangeInstantFlow
.filterNotNull()
.onEach { sync() }
.launchIn(ioScope)
} }
private fun clearUnlockedData() { private fun clearUnlockedData() {

View file

@ -17,7 +17,7 @@ fun SyncResponseJson.Collection.toEncryptedSdkCollection(): Collection =
externalId = this.externalId, externalId = this.externalId,
hidePasswords = this.shouldHidePasswords, hidePasswords = this.shouldHidePasswords,
readOnly = this.isReadOnly, readOnly = this.isReadOnly,
manage = this.canManage, manage = this.canManage ?: !this.isReadOnly,
) )
/** /**

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -134,14 +133,13 @@ fun SetupAutoFillScreen(
}, },
) )
}, },
) { innerPadding -> ) {
SetupAutoFillContent( SetupAutoFillContent(
state = state, state = state,
onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) }, onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) },
onContinueClick = handler.onContinueClick, onContinueClick = handler.onContinueClick,
onTurnOnLaterClick = handler.onTurnOnLaterClick, onTurnOnLaterClick = handler.onTurnOnLaterClick,
modifier = Modifier modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.fillMaxSize(), .fillMaxSize(),
) )

View file

@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -62,11 +61,9 @@ fun SetupCompleteScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding -> ) {
SetupCompleteContent( SetupCompleteContent(
modifier = Modifier modifier = Modifier.verticalScroll(rememberScrollState()),
.padding(innerPadding)
.verticalScroll(rememberScrollState()),
onContinue = setupCompleteAction, onContinue = setupCompleteAction,
) )
} }

View file

@ -128,15 +128,13 @@ fun SetupUnlockScreen(
}, },
) )
}, },
) { innerPadding -> ) {
SetupUnlockScreenContent( SetupUnlockScreenContent(
state = state, state = state,
showBiometricsPrompt = showBiometricsPrompt, showBiometricsPrompt = showBiometricsPrompt,
handler = handler, handler = handler,
biometricsManager = biometricsManager, biometricsManager = biometricsManager,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.padding(paddingValues = innerPadding)
.fillMaxSize(),
) )
} }
} }

View file

@ -76,6 +76,11 @@ class SetupUnlockViewModel @Inject constructor(
} }
private fun handleCloseClick() { 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) sendEvent(SetupUnlockEvent.NavigateBack)
} }

View file

@ -91,10 +91,9 @@ fun CheckEmailScreen(
onNavigationIconClick = handler.onBackClick, onNavigationIconClick = handler.onBackClick,
) )
}, },
) { innerPadding -> ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(innerPadding)
.imePadding() .imePadding()
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
@ -298,7 +297,7 @@ private fun CheckEmailLegacyContent(
), ),
highlights = listOf( highlights = listOf(
ClickableTextHighlight( ClickableTextHighlight(
textToHighlight = stringResource(id = R.string.log_in), textToHighlight = stringResource(id = R.string.log_in_verb),
onTextClick = onLoginClick, onTextClick = onLoginClick,
), ),
), ),

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -158,10 +157,9 @@ fun CompleteRegistrationScreen(
}, },
) )
}, },
) { innerPadding -> ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(innerPadding)
.imePadding() .imePadding()
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),

View file

@ -42,14 +42,20 @@ fun PasswordStrengthIndicator(
currentCharacterCount: Int, currentCharacterCount: Int,
minimumCharacterCount: Int? = null, minimumCharacterCount: Int? = null,
) { ) {
val minimumRequirementMet = (minimumCharacterCount == null) ||
(currentCharacterCount >= minimumCharacterCount)
val widthPercent by animateFloatAsState( val widthPercent by animateFloatAsState(
targetValue = when (state) { targetValue = if (minimumRequirementMet) {
PasswordStrengthState.NONE -> 0f when (state) {
PasswordStrengthState.WEAK_1 -> .25f PasswordStrengthState.NONE -> 0f
PasswordStrengthState.WEAK_2 -> .5f PasswordStrengthState.WEAK_1 -> .25f
PasswordStrengthState.WEAK_3 -> .66f PasswordStrengthState.WEAK_2 -> .5f
PasswordStrengthState.GOOD -> .82f PasswordStrengthState.WEAK_3 -> .66f
PasswordStrengthState.STRONG -> 1f PasswordStrengthState.GOOD -> .82f
PasswordStrengthState.STRONG -> 1f
}
} else {
0f
}, },
label = "Width Percent State", label = "Width Percent State",
) )
@ -107,11 +113,13 @@ fun PasswordStrengthIndicator(
minimumCharacterCount = minCount, minimumCharacterCount = minCount,
) )
} }
Text( if (minimumRequirementMet) {
text = label(), Text(
style = BitwardenTheme.typography.labelSmall, text = label(),
color = indicatorColor, style = BitwardenTheme.typography.labelSmall,
) color = indicatorColor,
)
}
} }
} }
} }
@ -122,14 +130,6 @@ private fun MinimumCharacterCount(
minimumRequirementMet: Boolean, minimumRequirementMet: Boolean,
minimumCharacterCount: Int, minimumCharacterCount: Int,
) { ) {
val characterCountColor by animateColorAsState(
targetValue = if (minimumRequirementMet) {
BitwardenTheme.colorScheme.status.strong
} else {
BitwardenTheme.colorScheme.text.secondary
},
label = "minmumCharacterCountColor",
)
Row( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -145,14 +145,14 @@ private fun MinimumCharacterCount(
Icon( Icon(
painter = rememberVectorPainter(id = it), painter = rememberVectorPainter(id = it),
contentDescription = null, contentDescription = null,
tint = characterCountColor, tint = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier.size(12.dp), modifier = Modifier.size(12.dp),
) )
} }
Spacer(modifier = Modifier.width(2.dp)) Spacer(modifier = Modifier.width(2.dp))
Text( Text(
text = stringResource(R.string.minimum_characters, minimumCharacterCount), text = stringResource(R.string.minimum_characters, minimumCharacterCount),
color = characterCountColor, color = BitwardenTheme.colorScheme.text.secondary,
style = BitwardenTheme.typography.labelSmall, style = BitwardenTheme.typography.labelSmall,
) )
} }

View file

@ -1,14 +1,11 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.widget.Toast 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource 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.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri 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.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText 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.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.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@ -187,10 +175,9 @@ fun CreateAccountScreen(
}, },
) )
}, },
) { innerPadding -> ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(innerPadding)
.imePadding() .imePadding()
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
@ -284,6 +271,9 @@ fun CreateAccountScreen(
onPrivacyPolicyClick = remember(viewModel) { onPrivacyPolicyClick = remember(viewModel) {
{ viewModel.trySendAction(PrivacyPolicyClick) } { viewModel.trySendAction(PrivacyPolicyClick) }
}, },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.navigationBarsPadding())
} }
@ -291,52 +281,24 @@ fun CreateAccountScreen(
} }
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod")
@Composable @Composable
private fun TermsAndPrivacySwitch( private fun TermsAndPrivacySwitch(
isChecked: Boolean, isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit,
onTermsClick: () -> Unit, onTermsClick: () -> Unit,
onPrivacyPolicyClick: () -> Unit, onPrivacyPolicyClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( BitwardenSwitch(
horizontalArrangement = Arrangement.Start, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, label = stringResource(id = R.string.accept_policies),
modifier = Modifier isChecked = isChecked,
.semantics(mergeDescendants = true) { contentDescription = "AcceptPoliciesToggle",
testTag = "AcceptPoliciesToggle" onCheckedChange = onCheckedChange,
toggleableState = ToggleableState(isChecked) subContent = {
}
.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,
)
FlowRow( FlowRow(
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.padding(end = 16.dp)
.fillMaxWidth()
.wrapContentHeight(),
) { ) {
BitwardenClickableText( BitwardenClickableText(
label = stringResource(id = R.string.terms_of_service), label = stringResource(id = R.string.terms_of_service),
@ -358,6 +320,6 @@ private fun TermsAndPrivacySwitch(
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
) )
} }
} },
} )
} }

View file

@ -117,7 +117,7 @@ fun EnterpriseSignOnScreen(
}, },
actions = { actions = {
BitwardenTextButton( BitwardenTextButton(
label = stringResource(id = R.string.log_in), label = stringResource(id = R.string.log_in_verb),
onClick = remember(viewModel) { onClick = remember(viewModel) {
{ viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) } { viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) }
}, },
@ -126,15 +126,13 @@ fun EnterpriseSignOnScreen(
}, },
) )
}, },
) { innerPadding -> ) {
EnterpriseSignOnScreenContent( EnterpriseSignOnScreenContent(
state = state, state = state,
onOrgIdentifierInputChange = remember(viewModel) { onOrgIdentifierInputChange = remember(viewModel) {
{ viewModel.trySendAction(EnterpriseSignOnAction.OrgIdentifierInputChange(it)) } { viewModel.trySendAction(EnterpriseSignOnAction.OrgIdentifierInputChange(it)) }
}, },
modifier = Modifier modifier = Modifier.fillMaxSize(),
.padding(innerPadding)
.fillMaxSize(),
) )
} }
} }

View file

@ -98,10 +98,9 @@ fun EnvironmentScreen(
}, },
) )
}, },
) { innerPadding -> ) {
Column( Column(
Modifier modifier = Modifier
.padding(innerPadding)
.fillMaxSize() .fillMaxSize()
.imePadding() .imePadding()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),

View file

@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -72,7 +71,7 @@ fun ExpiredRegistrationLinkScreen(
), ),
) )
}, },
) { innerPadding -> ) {
ExpiredRegistrationLinkContent( ExpiredRegistrationLinkContent(
onNavigateToLogin = remember(viewModel) { onNavigateToLogin = remember(viewModel) {
{ {
@ -87,7 +86,6 @@ fun ExpiredRegistrationLinkScreen(
} }
}, },
modifier = Modifier modifier = Modifier
.padding(innerPadding)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) )

View file

@ -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( LandingScreenContent(
state = state, state = state,
isAppBarVisible = isAppBarVisible, isAppBarVisible = isAppBarVisible,
@ -167,32 +189,7 @@ fun LandingScreen(
onCreateAccountClick = remember(viewModel) { onCreateAccountClick = remember(viewModel) {
{ viewModel.trySendAction(LandingAction.CreateAccountClick) } { viewModel.trySendAction(LandingAction.CreateAccountClick) }
}, },
modifier = Modifier modifier = Modifier.fillMaxSize(),
.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(),
) )
} }
} }

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