mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 02:15:43 -05:00
Compare commits
2 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b497156302 | ||
|
|
ab90f5ff95 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -48,9 +48,3 @@
|
||||
# app/src/main/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
|
||||
# app/src/test/java/com/x8bit/bitwarden/data/vault @bitwarden/team-vault-dev
|
||||
# app/src/test/java/com/x8bit/bitwarden/ui/vault @bitwarden/team-vault-dev
|
||||
|
||||
# Docker-related files
|
||||
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
@@ -4,7 +4,7 @@ inputs:
|
||||
java-version:
|
||||
description: 'Java version to use'
|
||||
required: false
|
||||
default: '21'
|
||||
default: '17'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -31,12 +31,12 @@ runs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -27,9 +27,6 @@
|
||||
],
|
||||
"matchManagers": [
|
||||
"gradle"
|
||||
],
|
||||
"excludePackageNames": [
|
||||
"com.github.bumptech.glide:compose"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
14
.github/workflows/build-authenticator.yml
vendored
14
.github/workflows/build-authenticator.yml
vendored
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 21
|
||||
JAVA_VERSION: 17
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
@@ -76,13 +76,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -110,10 +110,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -18,17 +18,17 @@ on:
|
||||
distribute-to-firebase:
|
||||
description: "Optional. Distribute artifacts to Firebase."
|
||||
required: false
|
||||
default: true
|
||||
default: false
|
||||
type: boolean
|
||||
publish-to-play-store:
|
||||
description: "Optional. Deploy bundle artifact to Google Play Store"
|
||||
required: false
|
||||
default: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 21
|
||||
JAVA_VERSION: 17
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
@@ -77,13 +77,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -118,10 +118,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -417,7 +417,7 @@ jobs:
|
||||
bundle exec fastlane run validate_play_store_json_key
|
||||
|
||||
- name: Publish Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
||||
run: |
|
||||
bundle exec fastlane publishProdToPlayStore
|
||||
bundle exec fastlane publishBetaToPlayStore
|
||||
@@ -429,10 +429,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -503,7 +503,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Download Google Privileged Browsers List
|
||||
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
|
||||
|
||||
4
.github/workflows/crowdin-pull.yml
vendored
4
.github/workflows/crowdin-pull.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
4
.github/workflows/crowdin-push.yml
vendored
4
.github/workflows/crowdin-push.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/publish-store.yml
vendored
4
.github/workflows/publish-store.yml
vendored
@@ -71,10 +71,10 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
2
.github/workflows/release-branch.yml
vendored
2
.github/workflows/release-branch.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
98
.github/workflows/sdlc-sdk-update.yml
vendored
98
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -22,10 +22,6 @@ on:
|
||||
pr-id:
|
||||
description: "Pull Request ID"
|
||||
|
||||
env:
|
||||
_BOT_NAME: "bw-ghapp[bot]"
|
||||
_BOT_EMAIL: "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update and PR
|
||||
@@ -58,15 +54,11 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-pull-requests: write
|
||||
permission-actions: read
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
@@ -78,57 +70,14 @@ jobs:
|
||||
run: |
|
||||
BRANCH_NAME="sdlc/sdk-update"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git switch -c $BRANCH_NAME
|
||||
|
||||
if git switch $BRANCH_NAME; then
|
||||
echo "✅ Switched to existing branch: $BRANCH_NAME"
|
||||
echo "updating_existing_branch=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📝 Creating new branch: $BRANCH_NAME"
|
||||
git switch -c $BRANCH_NAME
|
||||
echo "updating_existing_branch=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prevent updating the branch when the last committer isn't the bot
|
||||
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' $_BRANCH_NAME)
|
||||
|
||||
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
|
||||
echo "Expected bot email: $_BOT_EMAIL"
|
||||
|
||||
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
|
||||
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
|
||||
"This indicates manual changes have been made to the branch," \
|
||||
"PR has to be merged or closed before running this workflow again."
|
||||
echo "👀 Fetching existing PR..."
|
||||
gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty'
|
||||
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ❌ Merge or close: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
|
||||
|
||||
# Using main to retrieve the changelog on consecutive updates of the same PR.
|
||||
- name: Get current SDK version from main branch
|
||||
- name: Get current SDK version
|
||||
id: get-current-sdk
|
||||
run: |
|
||||
git show origin/main:gradle/libs.versions.toml
|
||||
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
|
||||
if [ -z "$SDK_VERSION" ]; then
|
||||
echo "::error::Failed to get current SDK version from main branch."
|
||||
exit 1
|
||||
fi
|
||||
SDK_VERSION=$(grep "bitwardenSdk =" gradle/libs.versions.toml | cut -d'"' -f2)
|
||||
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
|
||||
echo "Current SDK version (from main): $SDK_VERSION"
|
||||
echo "Current SDK version: $SDK_VERSION"
|
||||
echo "Current SDK git ref: $GIT_REF"
|
||||
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
|
||||
@@ -148,14 +97,14 @@ jobs:
|
||||
run: |
|
||||
echo "👀 Committing SDK version update..."
|
||||
|
||||
git config user.name "$_BOT_NAME"
|
||||
git config user.email "$_BOT_EMAIL"
|
||||
git config user.name "bw-ghapp[bot]"
|
||||
git config user.email "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
|
||||
git add gradle/libs.versions.toml
|
||||
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
|
||||
git push origin $_BRANCH_NAME
|
||||
|
||||
- name: Create or Update Pull Request
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
@@ -172,26 +121,17 @@ jobs:
|
||||
|
||||
$CHANGELOG"
|
||||
|
||||
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
|
||||
# Use echo -e to interpret escape sequences and pipe to gh pr create
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head $_BRANCH_NAME \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "🔄 Updating existing PR #$EXISTING_PR..."
|
||||
echo -e "$PR_BODY" | gh pr edit $EXISTING_PR \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file -
|
||||
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
|
||||
echo "## ✅ Updated PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "📝 Creating new PR..."
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head $_BRANCH_NAME \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "🚀 Created PR: $PR_URL"
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: Test Update
|
||||
@@ -203,7 +143,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
_JAVA_VERSION: 21
|
||||
_JAVA_VERSION: 17
|
||||
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
@@ -52,12 +52,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env._JAVA_VERSION }}
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -11,8 +11,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1166.0)
|
||||
aws-sdk-core (3.233.0)
|
||||
aws-partitions (1.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -20,18 +20,18 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.231.0)
|
||||
aws-sdk-kms (1.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.199.1)
|
||||
aws-sdk-core (~> 3, >= 3.231.0)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.3)
|
||||
bigdecimal (3.2.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -169,7 +169,7 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.15.0)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -192,15 +192,15 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.4)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.21.0)
|
||||
signet (0.20.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
|
||||
@@ -52,12 +52,12 @@
|
||||
|
||||
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
|
||||
|
||||
4. Setup JDK `Version` `21`:
|
||||
4. Setup JDK `Version` `17`:
|
||||
|
||||
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
|
||||
- Hit the selected Gradle JDK next to `Gradle JDK:`.
|
||||
- Select a `21.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `21`.
|
||||
- Select a `17.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `17`.
|
||||
- Select your preferred `Vendor`.
|
||||
- Hit `Download`.
|
||||
- Hit `Apply`.
|
||||
|
||||
@@ -224,7 +224,6 @@ dependencies {
|
||||
|
||||
implementation(project(":annotation"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":cxf"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":ui"))
|
||||
@@ -246,8 +245,6 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.providerevents)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
@@ -261,6 +258,7 @@ dependencies {
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.bitwarden.sdk)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.google.hilt.android)
|
||||
ksp(libs.google.hilt.compiler)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "11387825dab701f9d2dd2e940ffbd794",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `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": "hasTotp",
|
||||
"columnName": "has_totp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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, `default_user_collection_email` TEXT, `type` TEXT NOT NULL DEFAULT '0', 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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUserCollectionEmail",
|
||||
"columnName": "default_user_collection_email",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'0'"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '11387825dab701f9d2dd2e940ffbd794')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,6 @@
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Handle Credential Exchange transfer requests -->
|
||||
<intent-filter
|
||||
android:autoVerify="true"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="application/octet-stream"
|
||||
android:scheme="content"
|
||||
tools:ignore="AppLinkUriRelativeFilterGroupError" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill_title"
|
||||
android:label="@string/autofill"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
@@ -297,7 +295,6 @@ class MainViewModel @Inject constructor(
|
||||
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
|
||||
val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
@@ -421,16 +418,6 @@ class MainViewModel @Inject constructor(
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
|
||||
importCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CredentialExchangeExport(
|
||||
data = ImportCredentialsRequestData(
|
||||
uri = importCredentialsRequest.uri,
|
||||
requestJson = importCredentialsRequest.request.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,6 @@ enum class OnboardingStatus {
|
||||
@SerialName("autofillSetup")
|
||||
AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the browser autofill service setup.
|
||||
*/
|
||||
@SerialName("browserAutofillSetup")
|
||||
BROWSER_AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the final step of the onboarding process.
|
||||
*/
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the global state of all users.
|
||||
*/
|
||||
interface UserStateManager {
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
|
||||
*/
|
||||
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Tracks whether there is an account that is pending deletion in order to allow the account to
|
||||
* remain active until the deletion is finalized.
|
||||
*/
|
||||
var hasPendingAccountDeletion: Boolean
|
||||
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
suspend fun <T> userStateTransaction(block: suspend () -> T): T
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* The default implementation of the [UserStateManager].
|
||||
*/
|
||||
class UserStateManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserStateManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
//region Pending Account Addition
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
|
||||
get() = mutableHasPendingAccountAdditionStateFlow
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
//endregion Pending Account Addition
|
||||
|
||||
//region Pending Account Deletion
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override var hasPendingAccountDeletion: Boolean
|
||||
by mutableHasPendingAccountDeletionStateFlow::value
|
||||
//endregion Pending Account Deletion
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultLockManager.vaultUnlockDataStateFlow,
|
||||
hasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultLockManager.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultLockManager.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType = authDiskSource
|
||||
.getPinProtectedUserKey(userId = userId)
|
||||
?.let { VaultUnlockType.PIN }
|
||||
?: VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
@@ -26,7 +26,6 @@ fun TrustDeviceResponse.toUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val deviceOptions = decryptionOptions
|
||||
.trustedDeviceUserDecryptionOptions
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
@@ -28,6 +27,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
@@ -44,12 +44,17 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
/**
|
||||
* Models the current auth state.
|
||||
*/
|
||||
val authStateFlow: StateFlow<AuthState>
|
||||
|
||||
/**
|
||||
* Emits updates for changes to the [UserState].
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
@@ -105,6 +110,15 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
*/
|
||||
var shouldTrustDevice: Boolean
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
*
|
||||
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
|
||||
* Note that this call has no effect when there is no [UserState] information available.
|
||||
*/
|
||||
var hasPendingAccountAddition: Boolean
|
||||
|
||||
/**
|
||||
* Return the cached password policies for the current user.
|
||||
*/
|
||||
@@ -126,6 +140,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
*/
|
||||
val showWelcomeCarousel: Boolean
|
||||
|
||||
/**
|
||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||
*/
|
||||
fun clearPendingAccountDeletion()
|
||||
|
||||
/**
|
||||
* Attempt to delete the current account using the [masterPassword] and log them out
|
||||
* upon success.
|
||||
|
||||
@@ -33,8 +33,6 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
@@ -48,6 +46,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
@@ -56,7 +55,6 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
@@ -79,8 +77,13 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
@@ -88,26 +91,36 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -115,11 +128,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -129,6 +144,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -147,7 +163,6 @@ class AuthRepositoryImpl(
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@@ -157,13 +172,12 @@ class AuthRepositoryImpl(
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val userStateManager: UserStateManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager,
|
||||
UserStateManager by userStateManager {
|
||||
AuthRequestManager by authRequestManager {
|
||||
/**
|
||||
* A scope intended for use when simply collecting multiple flows in order to combine them. The
|
||||
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
|
||||
@@ -176,6 +190,24 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* If there is a pending account deletion, continue showing the original UserState until it
|
||||
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
|
||||
* whenever set to `true`.
|
||||
*/
|
||||
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* Whenever a function needs to update multiple underlying data-points that contribute to the
|
||||
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
|
||||
* until the transaction is complete. This is accomplished by blocking the emissions of the
|
||||
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
|
||||
* process is updating data simultaneously).
|
||||
*/
|
||||
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
|
||||
|
||||
/**
|
||||
* The auth information to make the identity token request will need to be
|
||||
* cached to make the request again in the case of two-factor authentication.
|
||||
@@ -236,6 +268,68 @@ class AuthRepositoryImpl(
|
||||
initialValue = AuthState.Uninitialized,
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber")
|
||||
override val userStateFlow: StateFlow<UserState?> = combine(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
merge(
|
||||
mutableHasPendingAccountDeletionStateFlow,
|
||||
mutableUserStateTransactionCountStateFlow,
|
||||
vaultRepository.isActiveUserUnlockingFlow,
|
||||
),
|
||||
) { array ->
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot {
|
||||
mutableHasPendingAccountDeletionStateFlow.value ||
|
||||
mutableUserStateTransactionCountStateFlow.value > 0 ||
|
||||
vaultRepository.isActiveUserUnlockingFlow.value
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = authDiskSource
|
||||
.userState
|
||||
?.toUserState(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
||||
|
||||
@@ -264,6 +358,9 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||
get() = policyManager.getActivePolicies()
|
||||
|
||||
@@ -282,7 +379,7 @@ class AuthRepositoryImpl(
|
||||
|
||||
init {
|
||||
combine(
|
||||
userStateManager.hasPendingAccountAdditionStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
@@ -301,16 +398,11 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
.onEach { userId ->
|
||||
if (userId == activeUserId) {
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
// We just sync now to get the latest data
|
||||
vaultRepository.sync(forced = true)
|
||||
} else {
|
||||
// We clear the last sync time to ensure we sync when we become the active user
|
||||
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||
}
|
||||
.onEach {
|
||||
val userId = activeUserId ?: return@onEach
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
|
||||
// happens on a background thread
|
||||
@@ -368,12 +460,16 @@ class AuthRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
override fun clearPendingAccountDeletion() {
|
||||
mutableHasPendingAccountDeletionStateFlow.value = false
|
||||
}
|
||||
|
||||
override suspend fun deleteAccountWithMasterPassword(
|
||||
masterPassword: String,
|
||||
): DeleteAccountResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
return authSdkSource
|
||||
.hashPassword(
|
||||
email = profile.email,
|
||||
@@ -393,7 +489,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun deleteAccountWithOneTimePassword(
|
||||
oneTimePassword: String,
|
||||
): DeleteAccountResult {
|
||||
userStateManager.hasPendingAccountDeletion = true
|
||||
mutableHasPendingAccountDeletionStateFlow.value = true
|
||||
return accountsService
|
||||
.deleteAccount(
|
||||
masterPasswordHash = null,
|
||||
@@ -405,13 +501,13 @@ class AuthRepositoryImpl(
|
||||
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
|
||||
fold(
|
||||
onFailure = {
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(error = it, message = null)
|
||||
},
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is DeleteAccountResponseJson.Invalid -> {
|
||||
userStateManager.hasPendingAccountDeletion = false
|
||||
clearPendingAccountDeletion()
|
||||
DeleteAccountResult.Error(message = response.message, error = null)
|
||||
}
|
||||
|
||||
@@ -742,17 +838,7 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationCodeResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -765,17 +851,7 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationOtpResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -798,7 +874,7 @@ class AuthRepositoryImpl(
|
||||
// We need to make sure that the environment is set back to the correct spot.
|
||||
updateEnvironment()
|
||||
// No switching to do but clear any pending account additions
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
hasPendingAccountAddition = false
|
||||
return SwitchAccountResult.NoChange
|
||||
}
|
||||
|
||||
@@ -813,7 +889,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
|
||||
|
||||
// Clear any pending account additions
|
||||
userStateManager.hasPendingAccountAddition = false
|
||||
hasPendingAccountAddition = false
|
||||
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
@@ -1476,6 +1552,27 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun isBiometricsEnabled(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
|
||||
|
||||
private fun isDeviceTrusted(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType =
|
||||
when {
|
||||
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
|
||||
VaultUnlockType.PIN
|
||||
}
|
||||
|
||||
else -> {
|
||||
VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the saved state with the force password reset reason.
|
||||
*/
|
||||
@@ -1586,7 +1683,7 @@ class AuthRepositoryImpl(
|
||||
deviceData: DeviceDataModel?,
|
||||
orgIdentifier: String?,
|
||||
userConfirmedKeyConnector: Boolean,
|
||||
): LoginResult = userStateManager.userStateTransaction {
|
||||
): LoginResult = userStateTransaction {
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
@@ -1625,7 +1722,7 @@ class AuthRepositoryImpl(
|
||||
// we should ask him to confirm the domain
|
||||
if (isNewKeyConnectorUser && isNotConfirmed) {
|
||||
keyConnectorResponse = loginResponse
|
||||
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
|
||||
return LoginResult.ConfirmKeyConnectorDomain(
|
||||
domain = keyConnectorUrl,
|
||||
)
|
||||
}
|
||||
@@ -2059,6 +2156,22 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
//endregion LoginCommon
|
||||
|
||||
/**
|
||||
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
|
||||
* where many individual changes might occur that would normally affect the [UserState] but we
|
||||
* only want a single final emission. In the rare case that multiple threads are running
|
||||
* transactions simultaneously, there will be no [UserState] updates until the last
|
||||
* transaction completes.
|
||||
*/
|
||||
private inline fun <T> userStateTransaction(block: () -> T): T {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.inc() }
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
mutableUserStateTransactionCountStateFlow.update { it.dec() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,11 +13,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@@ -25,7 +22,6 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -53,7 +49,6 @@ object AuthRepositoryModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
configDiskSource: ConfigDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
@@ -65,8 +60,8 @@ object AuthRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
userStateManager: UserStateManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
@@ -76,7 +71,6 @@ object AuthRepositoryModule {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -89,21 +83,7 @@ object AuthRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesUserStateManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserStateManager = UserStateManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
logsManager = logsManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
|
||||
*/
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
val error: Throwable,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
@@ -55,23 +54,6 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
val userId = syncProfile.id
|
||||
val account = this.accounts[userId] ?: return this
|
||||
val profile = account.profile
|
||||
val userDecryptionOptions = syncResponse
|
||||
.userDecryption
|
||||
?.let { syncUserDecryption ->
|
||||
profile
|
||||
.userDecryptionOptions
|
||||
?.copy(masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock)
|
||||
?: UserDecryptionOptionsJson(
|
||||
hasMasterPassword = syncUserDecryption.masterPasswordUnlock != null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock,
|
||||
)
|
||||
}
|
||||
?: profile
|
||||
.userDecryptionOptions
|
||||
?.copy(masterPasswordUnlock = null)
|
||||
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
avatarColorHex = syncProfile.avatarColor,
|
||||
@@ -79,7 +61,6 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
@@ -109,7 +90,6 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
|
||||
@@ -16,8 +16,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
@@ -26,8 +24,6 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -65,22 +61,6 @@ object AutofillModule {
|
||||
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesBrowserAutofillDialogManager(
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
clock: Clock,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): BrowserAutofillDialogManager = BrowserAutofillDialogManagerImpl(
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
|
||||
clock = clock,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
/**
|
||||
* Manager to handle whether the Browser Autofill Dialog should be displayed.
|
||||
*/
|
||||
interface BrowserAutofillDialogManager {
|
||||
/**
|
||||
* Number of browsers installed that may need autofill enabled.
|
||||
*/
|
||||
val browserCount: Int
|
||||
|
||||
/**
|
||||
* Indicates whether the dialog should be displayed to the user.
|
||||
*/
|
||||
val shouldShowDialog: Boolean
|
||||
|
||||
/**
|
||||
* The dialog has been dismissed and we should delay displaying it again.
|
||||
*/
|
||||
fun delayDialog()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* We only show the dialog once per 24 hour period.
|
||||
*/
|
||||
private const val SHOW_DIALOG_DELAY_MS: Long = 24L * 60L * 60L * 1000L
|
||||
|
||||
/**
|
||||
* The default implementation of the [BrowserAutofillDialogManager].
|
||||
*/
|
||||
internal class BrowserAutofillDialogManagerImpl(
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
private val clock: Clock,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
) : BrowserAutofillDialogManager {
|
||||
override val browserCount: Int
|
||||
get() = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.availableCount
|
||||
|
||||
override val shouldShowDialog: Boolean
|
||||
get() = autofillEnabledManager.isAutofillEnabled &&
|
||||
browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled &&
|
||||
!firstTimeActionManager
|
||||
.currentOrDefaultUserFirstTimeState
|
||||
.showSetupBrowserAutofillCard &&
|
||||
settingsDiskSource.browserAutofillDialogReshowTime?.isBefore(clock.instant()) != false
|
||||
|
||||
override fun delayDialog() {
|
||||
settingsDiskSource.browserAutofillDialogReshowTime =
|
||||
clock.instant().plusMillis(SHOW_DIALOG_DELAY_MS)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ package com.x8bit.bitwarden.data.autofill.model.browser
|
||||
data class BrowserThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
) {
|
||||
val isAvailableButDisabled: Boolean = isAvailable && !isThirdPartyEnabled
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant browsers.
|
||||
@@ -17,20 +15,4 @@ data class BrowserThirdPartyAutofillStatus(
|
||||
val braveStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
|
||||
) {
|
||||
/**
|
||||
* The total number of available browsers.
|
||||
*/
|
||||
val availableCount: Int
|
||||
get() = (if (braveStableStatusData.isAvailable) 1 else 0) +
|
||||
(if (chromeStableStatusData.isAvailable) 1 else 0) +
|
||||
(if (chromeBetaChannelStatusData.isAvailable) 1 else 0)
|
||||
|
||||
/**
|
||||
* Whether any of the available browsers have third party autofill disabled.
|
||||
*/
|
||||
val isAnyIsAvailableAndDisabled: Boolean
|
||||
get() = braveStableStatusData.isAvailableButDisabled ||
|
||||
chromeStableStatusData.isAvailableButDisabled ||
|
||||
chromeBetaChannelStatusData.isAvailableButDisabled
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -92,11 +93,13 @@ object CredentialProviderModule {
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
privilegedAppRepository: PrivilegedAppRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): OriginManager =
|
||||
OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
privilegedAppRepository = privilegedAppRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
@@ -16,7 +15,6 @@ import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
@@ -26,6 +24,7 @@ import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithCopyablePassword
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
@@ -42,6 +41,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.fold
|
||||
@@ -207,6 +207,8 @@ class BitwardenCredentialManagerImpl(
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.toPublicKeyCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherListViews = cipherListViews
|
||||
.filter { it.isActiveWithFido2Credentials },
|
||||
)
|
||||
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
|
||||
|
||||
@@ -223,72 +225,65 @@ class BitwardenCredentialManagerImpl(
|
||||
|
||||
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): Result<List<CredentialEntry>> {
|
||||
if (this.isEmpty()) return emptyList<CredentialEntry>().asSuccess()
|
||||
val assertionOptions = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson) }
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException(
|
||||
"Passkey assertion options required.",
|
||||
)
|
||||
.asFailure()
|
||||
}
|
||||
|
||||
val relyingPartyIds = assertionOptions
|
||||
.mapNotNull { it.relyingPartyId }
|
||||
.toSet()
|
||||
val relyingPartyIds = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
|
||||
.distinct()
|
||||
.ifEmpty {
|
||||
return GetCredentialUnknownException("Relying party id required.").asFailure()
|
||||
}
|
||||
|
||||
val allowedCredentials = assertionOptions
|
||||
.flatMap { option ->
|
||||
option
|
||||
.allowCredentials
|
||||
?.map { it.id }
|
||||
val cipherViews = cipherListViews
|
||||
.filter { cipherListView ->
|
||||
cipherListView.login
|
||||
?.fido2Credentials
|
||||
.orEmpty()
|
||||
.any { credential -> credential.rpId in relyingPartyIds }
|
||||
}
|
||||
.mapNotNull { cipherListView ->
|
||||
when (val result = vaultRepository.getCipher(cipherListView.id.orEmpty())) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found while building public key credential entries.")
|
||||
null
|
||||
}
|
||||
|
||||
val discoveredCredentials = relyingPartyIds
|
||||
.flatMap { relyingPartyId ->
|
||||
vaultSdkSource
|
||||
.silentlyDiscoverCredentials(
|
||||
userId = userId,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to discover credentials.")
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(
|
||||
result.error,
|
||||
"Failed to decrypt cipher while building credential entries.",
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
}
|
||||
.filterAllowedCredentialsIfNecessary(allowedCredentials)
|
||||
.toTypedArray()
|
||||
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
|
||||
|
||||
return credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = discoveredCredentials,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
cipherViews = cipherViews,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { fido2AutofillViews ->
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.asSuccess()
|
||||
},
|
||||
onFailure = {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
},
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.filterAllowedCredentialsIfNecessary(
|
||||
allowedCredentialIds: List<String>,
|
||||
): List<Fido2CredentialAutofillView> = if (allowedCredentialIds.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
this.filter {
|
||||
Base64
|
||||
.encodeToString(
|
||||
it.credentialId,
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING,
|
||||
) in allowedCredentialIds
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.credentials.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
|
||||
import com.x8bit.bitwarden.data.credentials.model.ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import timber.log.Timber
|
||||
@@ -21,6 +23,7 @@ class OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val privilegedAppRepository: PrivilegedAppRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
@@ -67,7 +70,10 @@ class OriginManagerImpl(
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
.takeUnless {
|
||||
it is ValidateOriginResult.Error.PrivilegedAppNotAllowed &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.UserManagedPrivilegedApps)
|
||||
}
|
||||
?: validatePrivilegedAppSignatureWithUserTrustList(callingAppInfo)
|
||||
|
||||
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
|
||||
|
||||
@@ -20,7 +20,7 @@ data class Fido2AttestationResponse(
|
||||
@SerialName("response")
|
||||
val response: RegistrationResponse,
|
||||
@SerialName("clientExtensionResults")
|
||||
val clientExtensionResults: ClientExtensionResults,
|
||||
val clientExtensionResults: ClientExtensionResults?,
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: String?,
|
||||
) {
|
||||
@@ -50,7 +50,7 @@ data class Fido2AttestationResponse(
|
||||
@Serializable
|
||||
data class ClientExtensionResults(
|
||||
@SerialName("credProps")
|
||||
val credentialProperties: CredentialProperties? = null,
|
||||
val credentialProperties: CredentialProperties,
|
||||
) {
|
||||
/**
|
||||
* Represents properties for newly created credential.
|
||||
|
||||
@@ -105,11 +105,6 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
|
||||
|
||||
/**
|
||||
* The time at which the browser autofill dialog is allowed to be shown to the user again.
|
||||
*/
|
||||
var browserAutofillDialogReshowTime: Instant?
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
@@ -286,23 +281,6 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
fun getUserHasSignedInPreviously(userId: String): Boolean
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
*/
|
||||
fun getShowBrowserAutofillSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* enable the browser autofill integration in onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowAutoFillSettingBadge] for the given [userId].
|
||||
*/
|
||||
fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
|
||||
@@ -37,7 +37,6 @@ private const val CLEAR_CLIPBOARD_INTERVAL_KEY = "clearClipboard"
|
||||
private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
|
||||
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
|
||||
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
|
||||
private const val SHOW_BROWSER_AUTOFILL_SETTING_BADGE = "showBrowserAutofillSettingBadge"
|
||||
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
|
||||
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
|
||||
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
|
||||
@@ -49,7 +48,6 @@ private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMa
|
||||
private const val RESUME_SCREEN = "resumeScreen"
|
||||
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
|
||||
private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
|
||||
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -74,9 +72,6 @@ class SettingsDiskSourceImpl(
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowBrowserAutofillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -229,12 +224,6 @@ class SettingsDiskSourceImpl(
|
||||
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
|
||||
get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) }
|
||||
|
||||
override var browserAutofillDialogReshowTime: Instant?
|
||||
get() = getLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME)?.let { Instant.ofEpochMilli(it) }
|
||||
set(value) {
|
||||
putLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME, value = value?.toEpochMilli())
|
||||
}
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
@@ -442,21 +431,6 @@ class SettingsDiskSourceImpl(
|
||||
key = HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY.appendIdentifier(userId),
|
||||
) == true
|
||||
|
||||
override fun getShowBrowserAutofillSettingBadge(userId: String): Boolean? =
|
||||
getBoolean(key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId))
|
||||
|
||||
override fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOW_BROWSER_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
|
||||
value = showBadge,
|
||||
)
|
||||
getMutableShowBrowserAutofillSettingBadgeFlow(userId).tryEmit(showBadge)
|
||||
}
|
||||
|
||||
override fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShowBrowserAutofillSettingBadgeFlow(userId = userId)
|
||||
.onSubscription { emit(getShowBrowserAutofillSettingBadge(userId)) }
|
||||
|
||||
override fun getShowAutoFillSettingBadge(userId: String): Boolean? =
|
||||
getBoolean(
|
||||
key = SHOW_AUTOFILL_SETTING_BADGE.appendIdentifier(userId),
|
||||
@@ -624,13 +598,6 @@ class SettingsDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowBrowserAutofillSettingBadgeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutableShowBrowserAutofillSettingBadgeFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowAutoFillSettingBadgeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that the security stamps for the current user do not match.
|
||||
*/
|
||||
class SecurityStampMismatchException : IllegalStateException("Security stamps do not match!")
|
||||
@@ -63,12 +63,6 @@ interface FirstTimeActionManager {
|
||||
*/
|
||||
fun storeShowUnlockSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable the browser autofill integration later, during onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable autofill later, during onboarding.
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
@@ -26,14 +25,12 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Implementation of [FirstTimeActionManager]
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FirstTimeActionManagerImpl @Inject constructor(
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) : FirstTimeActionManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
@@ -81,12 +78,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
combine(
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = it),
|
||||
) { showAutofillBadge, showBrowserAutofillBadge ->
|
||||
listOf(showAutofillBadge, showBrowserAutofillBadge)
|
||||
}
|
||||
// Can be expanded to support multiple autofill settings
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOfNotNull(showAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
@@ -128,7 +124,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
),
|
||||
) {
|
||||
FirstTimeState(
|
||||
@@ -136,11 +131,19 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupUnlockCard = it[1],
|
||||
showSetupAutofillCard = it[2],
|
||||
showImportLoginsCardInSettings = it[3],
|
||||
showSetupBrowserAutofillCard = it[4],
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart { emit(currentOrDefaultUserFirstTimeState) }
|
||||
.onStart {
|
||||
emit(
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
@@ -173,11 +176,14 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
|
||||
showImportLoginsCardInSettings = settingsDiskSource
|
||||
.getShowImportLoginsSettingBadge(it),
|
||||
showSetupBrowserAutofillCard = settingsDiskSource
|
||||
.getShowBrowserAutofillSettingBadge(it),
|
||||
)
|
||||
}
|
||||
?: FirstTimeState()
|
||||
?: FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
)
|
||||
|
||||
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
@@ -187,14 +193,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storeShowBrowserAutofillSettingBadge(
|
||||
userId = activeUserId,
|
||||
showBadge = showBadge,
|
||||
)
|
||||
}
|
||||
|
||||
override fun storeShowAutoFillSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storeShowAutoFillSettingBadge(
|
||||
@@ -259,19 +257,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showBrowserAutofill value which takes
|
||||
* into account if autofill and if browser autofill is already enabled.
|
||||
*/
|
||||
private fun getShowBrowserAutofillSettingBadgeFlowInternal(userId: String): Flow<Boolean> =
|
||||
combine(
|
||||
settingsDiskSource.getShowBrowserAutofillSettingBadgeFlow(userId = userId),
|
||||
autofillEnabledManager.isAutofillEnabledStateFlow,
|
||||
thirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatusFlow,
|
||||
) { showBadge, autofillEnabled, status ->
|
||||
showBadge ?: false && autofillEnabled && status.isAnyIsAvailableAndDisabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showAutofill value which takes
|
||||
* into account if autofill is already enabled globally.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bitwarden.data.manager
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Manager for loading native libraries.
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.bitwarden.data.manager
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import timber.log.Timber
|
||||
@@ -7,7 +7,7 @@ import timber.log.Timber
|
||||
* Primary implementation of [NativeLibraryManager].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
internal class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> {
|
||||
return try {
|
||||
System.loadLibrary(libraryName)
|
||||
@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended for full syncs for the user ID provided.
|
||||
* Flow that represents requests intended for full syncs.
|
||||
*/
|
||||
val fullSyncFlow: Flow<String>
|
||||
val fullSyncFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to log a user out.
|
||||
@@ -52,7 +52,7 @@ interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended to trigger syncing organization keys.
|
||||
*/
|
||||
val syncOrgKeysFlow: Flow<String>
|
||||
val syncOrgKeysFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync send delete.
|
||||
|
||||
@@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
@@ -55,7 +54,7 @@ class PushManagerImpl @Inject constructor(
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
@@ -67,13 +66,13 @@ class PushManagerImpl @Inject constructor(
|
||||
bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncSendDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendDeleteData>()
|
||||
private val mutableSyncSendUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
|
||||
override val fullSyncFlow: SharedFlow<String>
|
||||
override val fullSyncFlow: SharedFlow<Unit>
|
||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||
|
||||
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
||||
@@ -94,7 +93,7 @@ class PushManagerImpl @Inject constructor(
|
||||
override val syncFolderUpsertFlow: SharedFlow<SyncFolderUpsertData>
|
||||
get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncOrgKeysFlow: SharedFlow<String>
|
||||
override val syncOrgKeysFlow: SharedFlow<Unit>
|
||||
get() = mutableSyncOrgKeysSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncSendDeleteFlow: SharedFlow<SyncSendDeleteData>
|
||||
@@ -130,8 +129,8 @@ class PushManagerImpl @Inject constructor(
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun onMessageReceived(notification: BitwardenNotification) {
|
||||
if (authDiskSource.uniqueAppId == notification.contextId) return
|
||||
|
||||
val userId = activeUserId ?: return
|
||||
Timber.d("Push Notification Received: ${notification.notificationType}")
|
||||
|
||||
when (val type = notification.notificationType) {
|
||||
NotificationType.AUTH_REQUEST,
|
||||
@@ -190,24 +189,16 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.cipherId != null }
|
||||
?.let {
|
||||
SyncCipherDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.cipherId
|
||||
?.let { mutableSyncCipherDeleteSharedFlow.tryEmit(SyncCipherDeleteData(it)) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHERS,
|
||||
NotificationType.SYNC_SETTINGS,
|
||||
NotificationType.SYNC_VAULT,
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
|
||||
.userId
|
||||
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
|
||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
NotificationType.SYNC_FOLDER_CREATE,
|
||||
@@ -235,24 +226,15 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.folderId != null }
|
||||
?.let {
|
||||
SyncFolderDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
folderId = requireNotNull(it.folderId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.folderId
|
||||
?.let { mutableSyncFolderDeleteSharedFlow.tryEmit(SyncFolderDeleteData(it)) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_ORG_KEYS -> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SynchronizeOrganizationKeysNotifications>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.userId
|
||||
.takeIf { authDiskSource.userState?.accounts.orEmpty().containsKey(it) }
|
||||
?.let { mutableSyncOrgKeysSharedFlow.tryEmit(it) }
|
||||
if (isLoggedIn(userId)) {
|
||||
mutableSyncOrgKeysSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType.SYNC_SEND_CREATE,
|
||||
@@ -280,14 +262,9 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.sendId != null }
|
||||
?.let {
|
||||
SyncSendDeleteData(
|
||||
userId = requireNotNull(it.userId),
|
||||
sendId = requireNotNull(it.sendId),
|
||||
)
|
||||
}
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.sendId
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(SyncSendDeleteData(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* The token provider to pass to the SDK.
|
||||
*/
|
||||
class Token : ClientManagedTokens {
|
||||
override suspend fun getAccessToken(): String? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
@@ -24,7 +13,7 @@ class SdkClientManagerImpl(
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
|
||||
Client(tokenProvider = Token(), settings = null).apply {
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManagerImpl
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.EventService
|
||||
@@ -19,7 +18,6 @@ import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@@ -43,6 +41,8 @@ import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -242,6 +242,10 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
@@ -351,14 +355,12 @@ object PlatformManagerModule {
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
): FirstTimeActionManager = FirstTimeActionManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
thirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -8,7 +8,6 @@ data class FirstTimeState(
|
||||
val showImportLoginsCardInSettings: Boolean,
|
||||
val showSetupUnlockCard: Boolean,
|
||||
val showSetupAutofillCard: Boolean,
|
||||
val showSetupBrowserAutofillCard: Boolean,
|
||||
) {
|
||||
/**
|
||||
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
|
||||
@@ -19,12 +18,10 @@ data class FirstTimeState(
|
||||
showSetupUnlockCard: Boolean? = null,
|
||||
showSetupAutofillCard: Boolean? = null,
|
||||
showImportLoginsCardInSettings: Boolean? = null,
|
||||
showSetupBrowserAutofillCard: Boolean? = null,
|
||||
) : this(
|
||||
showImportLoginsCard = showImportLoginsCard ?: true,
|
||||
showSetupUnlockCard = showSetupUnlockCard ?: false,
|
||||
showSetupAutofillCard = showSetupAutofillCard ?: false,
|
||||
showImportLoginsCardInSettings = showImportLoginsCardInSettings ?: false,
|
||||
showSetupBrowserAutofillCard = showSetupBrowserAutofillCard ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,21 +74,4 @@ sealed class NotificationPayload {
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
* A notification payload for resynchronizing organization keys.
|
||||
*/
|
||||
@Serializable
|
||||
data class SynchronizeOrganizationKeysNotifications(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
* A notification payload for syncing a users vault.
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncNotification(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
) : NotificationPayload()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CredentialManager
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
@@ -134,14 +133,6 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object VerificationCodeShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched to select an account to export credentials from.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CredentialExchangeExport(
|
||||
val data: ImportCredentialsRequestData,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync cipher delete operations.
|
||||
*
|
||||
* @property cipherId The cipher ID.
|
||||
*/
|
||||
data class SyncCipherDeleteData(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync folder delete operations.
|
||||
*
|
||||
* @property folderId The folder ID.
|
||||
*/
|
||||
data class SyncFolderDeleteData(
|
||||
val userId: String,
|
||||
val folderId: String,
|
||||
)
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Required data for sync send delete operations.
|
||||
*
|
||||
* @property sendId The send ID.
|
||||
*/
|
||||
data class SyncSendDeleteData(
|
||||
val userId: String,
|
||||
val sendId: String,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
@@ -72,12 +71,3 @@ fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
|
||||
is SpecialCircumstance.AddTotpLoginItem -> this.data
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ImportCredentialsRequestData] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toImportCredentialsRequestDataOrNull(): ImportCredentialsRequestData? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.CredentialExchangeExport -> this.data
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -24,14 +24,6 @@ interface VaultDiskSource {
|
||||
*/
|
||||
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers with the given [cipherIds] from the data source for a given [userId].
|
||||
*/
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
||||
*/
|
||||
|
||||
@@ -97,24 +97,6 @@ class VaultDiskSourceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getSelectedCiphers(userId = userId, cipherIds = cipherIds)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
@@ -164,8 +146,6 @@ class VaultDiskSourceImpl(
|
||||
externalId = collection.externalId,
|
||||
isReadOnly = collection.isReadOnly,
|
||||
canManage = collection.canManage,
|
||||
defaultUserCollectionEmail = collection.defaultUserCollectionEmail,
|
||||
type = json.encodeToString(collection.type),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -187,8 +167,6 @@ class VaultDiskSourceImpl(
|
||||
externalId = entity.externalId,
|
||||
isReadOnly = entity.isReadOnly,
|
||||
canManage = entity.canManage,
|
||||
defaultUserCollectionEmail = entity.defaultUserCollectionEmail,
|
||||
type = json.decodeFromString(entity.type),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -312,8 +290,6 @@ class VaultDiskSourceImpl(
|
||||
externalId = collection.externalId,
|
||||
isReadOnly = collection.isReadOnly,
|
||||
canManage = collection.canManage,
|
||||
defaultUserCollectionEmail = collection.defaultUserCollectionEmail,
|
||||
type = json.encodeToString(collection.type),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -37,15 +37,6 @@ interface CiphersDao {
|
||||
userId: String,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database with the given [cipherIds] for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id IN (:cipherIds)")
|
||||
suspend fun getSelectedCiphers(
|
||||
userId: String,
|
||||
cipherIds: List<String>,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId].
|
||||
*/
|
||||
|
||||
@@ -27,11 +27,10 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
||||
FolderEntity::class,
|
||||
SendEntity::class,
|
||||
],
|
||||
version = 8,
|
||||
version = 7,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
],
|
||||
)
|
||||
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
||||
|
||||
@@ -33,10 +33,4 @@ data class CollectionEntity(
|
||||
|
||||
@ColumnInfo(name = "manage")
|
||||
val canManage: Boolean?,
|
||||
|
||||
@ColumnInfo(name = "default_user_collection_email")
|
||||
val defaultUserCollectionEmail: String?,
|
||||
|
||||
@ColumnInfo(name = "type", defaultValue = "0")
|
||||
val type: String,
|
||||
)
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
@@ -429,23 +428,6 @@ interface VaultSdkSource {
|
||||
format: ExportFormat,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Exports the users vault data to a CXF formatted string.
|
||||
*/
|
||||
suspend fun exportVaultDataToCxf(
|
||||
userId: String,
|
||||
account: Account,
|
||||
ciphers: List<Cipher>,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Imports the given CXF formatted [payload] into the users vault.
|
||||
*
|
||||
* @return Result of the import. If successful, a list of [Cipher]s deciphered from the CXF
|
||||
* payload.
|
||||
*/
|
||||
suspend fun importCxf(userId: String, payload: String): Result<List<Cipher>>
|
||||
|
||||
/**
|
||||
* Register a new FIDO 2 credential to a cipher.
|
||||
*
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.collections.Collection
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DeriveKeyConnectorException
|
||||
import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
@@ -11,7 +10,6 @@ import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
|
||||
@@ -94,17 +92,14 @@ class VaultSdkSourceImpl(
|
||||
),
|
||||
)
|
||||
DeriveKeyConnectorResult.Success(key)
|
||||
} catch (ex: BitwardenException.DeriveKeyConnector) {
|
||||
when (ex.v1) {
|
||||
is DeriveKeyConnectorException.WrongPassword -> {
|
||||
} catch (exception: BitwardenException) {
|
||||
when {
|
||||
exception.message == "Wrong password" -> {
|
||||
DeriveKeyConnectorResult.WrongPasswordError
|
||||
}
|
||||
is DeriveKeyConnectorException.Crypto -> {
|
||||
DeriveKeyConnectorResult.Error(error = ex)
|
||||
}
|
||||
|
||||
else -> DeriveKeyConnectorResult.Error(exception)
|
||||
}
|
||||
} catch (exception: BitwardenException) {
|
||||
DeriveKeyConnectorResult.Error(error = exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +473,7 @@ class VaultSdkSourceImpl(
|
||||
): Result<UpdatePasswordResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.crypto()
|
||||
.makeUpdatePassword(newPassword = newPassword)
|
||||
.updatePassword(newPassword = newPassword)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToString(
|
||||
@@ -496,28 +491,6 @@ class VaultSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToCxf(
|
||||
userId: String,
|
||||
account: Account,
|
||||
ciphers: List<Cipher>,
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.exporters()
|
||||
.exportCxf(
|
||||
account = account,
|
||||
ciphers = ciphers,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun importCxf(
|
||||
userId: String,
|
||||
payload: String,
|
||||
): Result<List<Cipher>> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.exporters()
|
||||
.importCxf(payload = payload)
|
||||
}
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
request: RegisterFido2CredentialRequest,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@@ -27,13 +28,21 @@ class Fido2CredentialStoreImpl(
|
||||
/**
|
||||
* Return all active ciphers that contain FIDO 2 credentials.
|
||||
*/
|
||||
override suspend fun allCredentials(): List<CipherListView> = vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
?.successes
|
||||
.orEmpty()
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
override suspend fun allCredentials(): List<CipherListView> {
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
if (syncResult is SyncVaultDataResult.Error) {
|
||||
syncResult.throwable
|
||||
?.let { throw it }
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
return vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
?.successes
|
||||
.orEmpty()
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ciphers that contain FIDO 2 credentials for the given [ripId] with the provided
|
||||
@@ -42,8 +51,15 @@ class Fido2CredentialStoreImpl(
|
||||
* @param ids Optional list of FIDO 2 credential ID's to find.
|
||||
* @param ripId Relying Party ID to find.
|
||||
*/
|
||||
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> =
|
||||
vaultRepository
|
||||
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> {
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
if (syncResult is SyncVaultDataResult.Error) {
|
||||
syncResult.throwable
|
||||
?.let { throw it }
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
|
||||
return vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
@@ -62,6 +78,7 @@ class Fido2CredentialStoreImpl(
|
||||
.toCipherViewOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the provided [cred] to the users vault.
|
||||
@@ -78,7 +95,7 @@ class Fido2CredentialStoreImpl(
|
||||
?: vaultRepository.createCipher(decryptedCipherView)
|
||||
}
|
||||
.onFailure { throw it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filtered list containing elements that match the given [relyingPartyId] and a
|
||||
|
||||
@@ -31,7 +31,7 @@ fun PublicKeyCredentialAuthenticatorAttestationResponse.toAndroidAttestationResp
|
||||
.ClientExtensionResults
|
||||
.CredentialProperties(residentKey = residentKey),
|
||||
)
|
||||
} ?: Fido2AttestationResponse.ClientExtensionResults(),
|
||||
},
|
||||
authenticatorAttachment = authenticatorAttachment,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
@@ -18,10 +17,7 @@ import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.EncryptionContext
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
|
||||
@@ -38,18 +34,13 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toNetworkAttachmentRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* The default implementation of the [CipherManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
class CipherManagerImpl(
|
||||
private val fileManager: FileManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
@@ -58,24 +49,9 @@ class CipherManagerImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val clock: Clock,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
) : CipherManager {
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncCipherDeleteFlow
|
||||
.onEach(::deleteCipher)
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncCipherUpsertFlow
|
||||
.onEach(::syncCipherIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(
|
||||
@@ -665,79 +641,4 @@ class CipherManagerImpl(
|
||||
}
|
||||
return migratedCipherView.asSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteCipher(syncCipherDeleteData: SyncCipherDeleteData) {
|
||||
vaultDiskSource.deleteCipher(
|
||||
userId = syncCipherDeleteData.userId,
|
||||
cipherId = syncCipherDeleteData.cipherId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual cipher contained in [syncCipherUpsertData] to disk if certain criteria
|
||||
* are met. If the resource cannot be found cloud-side, and it was updated, delete it from disk
|
||||
* for now.
|
||||
*/
|
||||
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val cipherId = syncCipherUpsertData.cipherId
|
||||
val organizationId = syncCipherUpsertData.organizationId
|
||||
val collectionIds = syncCipherUpsertData.collectionIds
|
||||
val revisionDate = syncCipherUpsertData.revisionDate
|
||||
val isUpdate = syncCipherUpsertData.isUpdate
|
||||
|
||||
// Return if local cipher is more recent
|
||||
val localCipher = vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
if (localCipher != null &&
|
||||
localCipher.revisionDate.toEpochSecond() > revisionDate.toEpochSecond()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldUpdate: Boolean
|
||||
val shouldCheckCollections: Boolean
|
||||
when {
|
||||
isUpdate -> {
|
||||
shouldUpdate = localCipher != null
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
|
||||
collectionIds == null || organizationId == null -> {
|
||||
shouldUpdate = localCipher == null
|
||||
shouldCheckCollections = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
shouldUpdate = false
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate && shouldCheckCollections && organizationId != null) {
|
||||
// Check if there are any collections in common
|
||||
shouldUpdate = vaultDiskSource
|
||||
.getCollections(userId = userId)
|
||||
.first()
|
||||
.any { collectionIds?.contains(it.id) == true }
|
||||
}
|
||||
|
||||
if (!shouldUpdate) return
|
||||
|
||||
ciphersService
|
||||
.getCipher(cipherId = cipherId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveCipher(userId = userId, cipher = it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult
|
||||
|
||||
/**
|
||||
* Manages the import process for Credential Exchange Format (CXF) payloads.
|
||||
*
|
||||
* This interface provides a contract for importing credential data from a standardized
|
||||
* CXF string, associating it with a specific user. It handles the parsing, decryption,
|
||||
* and storage of the credentials contained within the payload.
|
||||
*/
|
||||
interface CredentialExchangeImportManager {
|
||||
|
||||
/**
|
||||
* Attempt to import a CXF payload.
|
||||
*
|
||||
* @param payload The CXF payload to import.
|
||||
*/
|
||||
suspend fun importCxfPayload(userId: String, payload: String): ImportCxfPayloadResult
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.network.model.ImportCiphersJsonRequest
|
||||
import com.bitwarden.network.model.ImportCiphersResponseJson
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
|
||||
|
||||
/**
|
||||
* Default implementation of [CredentialExchangeImportManager].
|
||||
*/
|
||||
class CredentialExchangeImportManagerImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val ciphersService: CiphersService,
|
||||
private val vaultSyncManager: VaultSyncManager,
|
||||
) : CredentialExchangeImportManager {
|
||||
|
||||
override suspend fun importCxfPayload(
|
||||
userId: String,
|
||||
payload: String,
|
||||
): ImportCxfPayloadResult = vaultSdkSource
|
||||
.importCxf(
|
||||
userId = userId,
|
||||
payload = payload,
|
||||
)
|
||||
.flatMap { cipherList ->
|
||||
if (cipherList.isEmpty()) {
|
||||
// If no ciphers were returned, we can skip the remaining steps and return the
|
||||
// appropriate result.
|
||||
return ImportCxfPayloadResult.NoItems
|
||||
}
|
||||
ciphersService
|
||||
.importCiphers(
|
||||
request = ImportCiphersJsonRequest(
|
||||
ciphers = cipherList.map {
|
||||
it.toEncryptedNetworkCipher(
|
||||
encryptedFor = userId,
|
||||
)
|
||||
},
|
||||
folders = emptyList(),
|
||||
folderRelationships = emptyList(),
|
||||
),
|
||||
)
|
||||
.flatMap { importCiphersResponseJson ->
|
||||
when (importCiphersResponseJson) {
|
||||
is ImportCiphersResponseJson.Invalid -> {
|
||||
ImportCredentialsUnknownErrorException().asFailure()
|
||||
}
|
||||
|
||||
ImportCiphersResponseJson.Success -> {
|
||||
ImportCxfPayloadResult
|
||||
.Success(itemCount = cipherList.size)
|
||||
.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map {
|
||||
when (val syncResult = vaultSyncManager.syncForResult(forced = true)) {
|
||||
is SyncVaultDataResult.Success -> it
|
||||
is SyncVaultDataResult.Error -> {
|
||||
ImportCxfPayloadResult.SyncFailed(error = syncResult.throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { ImportCxfPayloadResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
|
||||
/**
|
||||
* Manages the creating, updating, and deleting folders.
|
||||
*/
|
||||
interface FolderManager {
|
||||
/**
|
||||
* Attempt to create a folder.
|
||||
*/
|
||||
suspend fun createFolder(folderView: FolderView): CreateFolderResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a folder.
|
||||
*/
|
||||
suspend fun deleteFolder(folderId: String): DeleteFolderResult
|
||||
|
||||
/**
|
||||
* Attempt to update a folder.
|
||||
*/
|
||||
suspend fun updateFolder(folderId: String, folderView: FolderView): UpdateFolderResult
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.UpdateFolderResponseJson
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* The default implementation of the [FolderManager].
|
||||
*/
|
||||
class FolderManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val folderService: FolderService,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
) : FolderManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncFolderDeleteFlow
|
||||
.onEach(::deleteFolder)
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncFolderUpsertFlow
|
||||
.onEach(::syncFolderIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
|
||||
val userId = activeUserId ?: return CreateFolderResult.Error(NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptFolder(userId = userId, folder = folderView)
|
||||
.flatMap { folderService.createFolder(body = it.toEncryptedNetworkFolder()) }
|
||||
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
|
||||
.flatMap {
|
||||
vaultSdkSource.decryptFolder(userId = userId, folder = it.toEncryptedSdkFolder())
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { CreateFolderResult.Success(folderView = it) },
|
||||
onFailure = { CreateFolderResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
|
||||
val userId = activeUserId ?: return DeleteFolderResult.Error(NoActiveUserException())
|
||||
return folderService
|
||||
.deleteFolder(folderId = folderId)
|
||||
.onSuccess {
|
||||
clearFolderIdFromCiphers(userId = userId, folderId = folderId)
|
||||
vaultDiskSource.deleteFolder(userId = userId, folderId = folderId)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { DeleteFolderResult.Success },
|
||||
onFailure = { DeleteFolderResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateFolder(
|
||||
folderId: String,
|
||||
folderView: FolderView,
|
||||
): UpdateFolderResult {
|
||||
val userId = activeUserId ?: return UpdateFolderResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptFolder(userId = userId, folder = folderView)
|
||||
.flatMap { folder ->
|
||||
folderService.updateFolder(
|
||||
folderId = folder.id.toString(),
|
||||
body = folder.toEncryptedNetworkFolder(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateFolderResponseJson.Success -> {
|
||||
vaultDiskSource.saveFolder(userId = userId, folder = response.folder)
|
||||
vaultSdkSource
|
||||
.decryptFolder(
|
||||
userId = userId,
|
||||
folder = response.folder.toEncryptedSdkFolder(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { UpdateFolderResult.Success(it) },
|
||||
onFailure = {
|
||||
UpdateFolderResult.Error(errorMessage = null, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is UpdateFolderResponseJson.Invalid -> {
|
||||
UpdateFolderResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun clearFolderIdFromCiphers(userId: String, folderId: String) {
|
||||
vaultDiskSource.getCiphers(userId = userId).forEach {
|
||||
if (it.folderId == folderId) {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = it.copy(folderId = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the folder specified by [syncFolderDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteFolder(syncFolderDeleteData: SyncFolderDeleteData) {
|
||||
clearFolderIdFromCiphers(
|
||||
folderId = syncFolderDeleteData.folderId,
|
||||
userId = syncFolderDeleteData.userId,
|
||||
)
|
||||
vaultDiskSource.deleteFolder(
|
||||
folderId = syncFolderDeleteData.folderId,
|
||||
userId = syncFolderDeleteData.userId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual folder contained in [syncFolderUpsertData] to disk if certain criteria
|
||||
* are met.
|
||||
*/
|
||||
private suspend fun syncFolderIfNecessary(syncFolderUpsertData: SyncFolderUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val folderId = syncFolderUpsertData.folderId
|
||||
val isUpdate = syncFolderUpsertData.isUpdate
|
||||
val revisionDate = syncFolderUpsertData.revisionDate
|
||||
val localFolder = vaultDiskSource
|
||||
.getFolders(userId = userId)
|
||||
.first()
|
||||
.find { it.id == folderId }
|
||||
val isValidCreate = !isUpdate && localFolder == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localFolder != null &&
|
||||
localFolder.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
folderService
|
||||
.getFolder(folderId = folderId)
|
||||
.onSuccess { vaultDiskSource.saveFolder(userId = userId, folder = it) }
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
|
||||
/**
|
||||
* Manages the creating, updating, and deleting sends.
|
||||
*/
|
||||
interface SendManager {
|
||||
/**
|
||||
* Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a
|
||||
* [SendView.type] of [SendType.FILE].
|
||||
*/
|
||||
suspend fun createSend(sendView: SendView, fileUri: Uri?): CreateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a send.
|
||||
*/
|
||||
suspend fun deleteSend(sendId: String): DeleteSendResult
|
||||
|
||||
/**
|
||||
* Attempt to remove the password from a send.
|
||||
*/
|
||||
suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult
|
||||
|
||||
/**
|
||||
* Attempt to update a send.
|
||||
*/
|
||||
suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.CreateFileSendResponse
|
||||
import com.bitwarden.network.model.CreateSendJsonResponse
|
||||
import com.bitwarden.network.model.UpdateSendResponseJson
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.send.Send
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import retrofit2.HttpException
|
||||
|
||||
/**
|
||||
* The default implementation of the [SendManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class SendManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val sendsService: SendsService,
|
||||
private val fileManager: FileManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : SendManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncSendDeleteFlow
|
||||
.onEach(::deleteSend)
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncSendUpsertFlow
|
||||
.onEach(::syncSendIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun createSend(
|
||||
sendView: SendView,
|
||||
fileUri: Uri?,
|
||||
): CreateSendResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateSendResult.Error(message = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptSend(userId = userId, sendView = sendView)
|
||||
.flatMap { send ->
|
||||
when (send.type) {
|
||||
SendType.TEXT -> sendsService.createTextSend(send.toEncryptedNetworkSend())
|
||||
SendType.FILE -> createFileSend(uri = fileUri, userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
.map { createSendResponse ->
|
||||
when (createSendResponse) {
|
||||
is CreateSendJsonResponse.Invalid -> {
|
||||
return CreateSendResult.Error(
|
||||
message = createSendResponse.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is CreateSendJsonResponse.Success -> {
|
||||
// Save the send immediately, regardless of whether the decrypt succeeds
|
||||
vaultDiskSource.saveSend(userId = userId, send = createSendResponse.send)
|
||||
createSendResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMap { createSendSuccessResponse ->
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId,
|
||||
send = createSendSuccessResponse.send.toEncryptedSdkSend(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateSendResult.Error(message = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerCreateSendAction()
|
||||
CreateSendResult.Success(sendView = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteSend(sendId: String): DeleteSendResult {
|
||||
val userId = activeUserId ?: return DeleteSendResult.Error(error = NoActiveUserException())
|
||||
return sendsService
|
||||
.deleteSend(sendId)
|
||||
.onSuccess { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) }
|
||||
.fold(
|
||||
onSuccess = { DeleteSendResult.Success },
|
||||
onFailure = { DeleteSendResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult {
|
||||
val userId = activeUserId ?: return RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return sendsService
|
||||
.removeSendPassword(sendId = sendId)
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = response.message,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { RemovePasswordSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RemovePasswordSendResult.Error(errorMessage = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult {
|
||||
val userId = activeUserId ?: return UpdateSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptSend(userId = userId, sendView = sendView)
|
||||
.flatMap { send ->
|
||||
sendsService.updateSend(sendId = sendId, body = send.toEncryptedNetworkSend())
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UpdateSendResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
UpdateSendResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { UpdateSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
UpdateSendResult.Error(errorMessage = null, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createFileSend(
|
||||
uri: Uri?,
|
||||
userId: String,
|
||||
send: Send,
|
||||
): Result<CreateSendJsonResponse> {
|
||||
uri ?: return IllegalArgumentException("File URI must be present to create a File Send.")
|
||||
.asFailure()
|
||||
|
||||
return fileManager
|
||||
.writeUriToCache(uri)
|
||||
.flatMap { file ->
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = send,
|
||||
path = file.absolutePath,
|
||||
destinationFilePath = file.absolutePath,
|
||||
)
|
||||
}
|
||||
.flatMap { encryptedFile ->
|
||||
sendsService
|
||||
.createFileSend(
|
||||
body = send.toEncryptedNetworkSend(fileLength = encryptedFile.length()),
|
||||
)
|
||||
.flatMap { sendFileResponse ->
|
||||
when (sendFileResponse) {
|
||||
is CreateFileSendResponse.Invalid -> {
|
||||
CreateSendJsonResponse
|
||||
.Invalid(
|
||||
message = sendFileResponse.message,
|
||||
validationErrors = sendFileResponse.validationErrors,
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
is CreateFileSendResponse.Success -> {
|
||||
sendsService
|
||||
.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
.also {
|
||||
// Delete encrypted file once it has been uploaded.
|
||||
fileManager.delete(encryptedFile)
|
||||
}
|
||||
.map { CreateSendJsonResponse.Success(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the send specified by [syncSendDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteSend(syncSendDeleteData: SyncSendDeleteData) {
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = syncSendDeleteData.userId,
|
||||
sendId = syncSendDeleteData.sendId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual send contained in [syncSendUpsertData] to disk if certain criteria are
|
||||
* met. If the resource cannot be found cloud-side, and it was updated, delete it from disk for
|
||||
* now.
|
||||
*/
|
||||
private suspend fun syncSendIfNecessary(syncSendUpsertData: SyncSendUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val sendId = syncSendUpsertData.sendId
|
||||
val isUpdate = syncSendUpsertData.isUpdate
|
||||
val revisionDate = syncSendUpsertData.revisionDate
|
||||
val localSend = vaultDiskSource
|
||||
.getSends(userId = userId)
|
||||
.first()
|
||||
.find { it.id == sendId }
|
||||
val isValidCreate = !isUpdate && localSend == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localSend != null &&
|
||||
localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
sendsService
|
||||
.getSend(sendId = sendId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveSend(userId = userId, send = it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the synchronization of the user's vault data with the remote server.
|
||||
* This interface provides a way to trigger a sync process, which updates the local
|
||||
* database with the latest changes from the server.
|
||||
*/
|
||||
interface VaultSyncManager {
|
||||
/**
|
||||
* Flow that represents the current vault data.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val vaultDataStateFlow: StateFlow<DataState<VaultData>>
|
||||
|
||||
/**
|
||||
* Flow that represents all ciphers for the active user, including references to ciphers that
|
||||
* cannot be decrypted.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val decryptCipherListResultStateFlow: StateFlow<DataState<DecryptCipherListResult>>
|
||||
|
||||
/**
|
||||
* Flow that represents all collections for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val collectionsStateFlow: StateFlow<DataState<List<CollectionView>>>
|
||||
|
||||
/**
|
||||
* Flow that represents all domains for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val domainsStateFlow: StateFlow<DataState<DomainsData>>
|
||||
|
||||
/**
|
||||
* Flow that represents all folders for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
|
||||
|
||||
/**
|
||||
* Flow that represents the current send data.
|
||||
*/
|
||||
val sendDataStateFlow: StateFlow<DataState<SendData>>
|
||||
|
||||
/**
|
||||
* Sync the vault data for the current user.
|
||||
*
|
||||
* Unlike [syncIfNecessary], this will always perform the requested sync and should only be
|
||||
* utilized in cases where the user specifically requested the action.
|
||||
*/
|
||||
fun sync(forced: Boolean = false)
|
||||
|
||||
/**
|
||||
* Checks if conditions have been met to perform a sync request and, if so, syncs the vault
|
||||
* data for the current user.
|
||||
*/
|
||||
fun syncIfNecessary()
|
||||
|
||||
/**
|
||||
* Syncs the vault data for the current user. This is an explicit request to sync and will
|
||||
* return the result of the sync as a [SyncVaultDataResult].
|
||||
*/
|
||||
suspend fun syncForResult(forced: Boolean = false): SyncVaultDataResult
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.combineDataStates
|
||||
import com.bitwarden.core.data.repository.util.map
|
||||
import com.bitwarden.core.data.repository.util.updateToPendingOrLoading
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.network.util.isNoConnectionError
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.error.SecurityStampMismatchException
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
/**
|
||||
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
|
||||
* specified period of time after it no longer has subscribers.
|
||||
*/
|
||||
private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
private const val SYNC_IF_NECESSARY_DELAY_MIN: Long = 30L
|
||||
|
||||
/**
|
||||
* Default implementation of [VaultSyncManager].
|
||||
*/
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class VaultSyncManagerImpl(
|
||||
private val syncService: SyncService,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val clock: Clock,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : VaultSyncManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private var syncJob: Job = Job().apply { complete() }
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendData>>(DataState.Loading)
|
||||
|
||||
private val mutableDecryptCipherListResultFlow =
|
||||
MutableStateFlow<DataState<DecryptCipherListResult>>(DataState.Loading)
|
||||
|
||||
private val mutableFoldersStateFlow =
|
||||
MutableStateFlow<DataState<List<FolderView>>>(DataState.Loading)
|
||||
|
||||
private val mutableCollectionsStateFlow =
|
||||
MutableStateFlow<DataState<List<CollectionView>>>(DataState.Loading)
|
||||
|
||||
private val mutableDomainsStateFlow =
|
||||
MutableStateFlow<DataState<DomainsData>>(DataState.Loading)
|
||||
|
||||
override val decryptCipherListResultStateFlow: StateFlow<DataState<DecryptCipherListResult>>
|
||||
get() = mutableDecryptCipherListResultFlow.asStateFlow()
|
||||
|
||||
override val domainsStateFlow: StateFlow<DataState<DomainsData>>
|
||||
get() = mutableDomainsStateFlow.asStateFlow()
|
||||
|
||||
override val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
|
||||
get() = mutableFoldersStateFlow.asStateFlow()
|
||||
|
||||
override val collectionsStateFlow: StateFlow<DataState<List<CollectionView>>>
|
||||
get() = mutableCollectionsStateFlow.asStateFlow()
|
||||
|
||||
override val sendDataStateFlow: StateFlow<DataState<SendData>>
|
||||
get() = mutableSendDataStateFlow.asStateFlow()
|
||||
|
||||
override val vaultDataStateFlow: StateFlow<DataState<VaultData>> =
|
||||
combine(
|
||||
decryptCipherListResultStateFlow,
|
||||
foldersStateFlow,
|
||||
collectionsStateFlow,
|
||||
sendDataStateFlow,
|
||||
) { ciphersDataState, foldersDataState, collectionsDataState, sendsDataState ->
|
||||
combineDataStates(
|
||||
ciphersDataState,
|
||||
foldersDataState,
|
||||
collectionsDataState,
|
||||
sendsDataState,
|
||||
) { ciphersData, foldersData, collectionsData, sendsData ->
|
||||
VaultData(
|
||||
decryptCipherListResult = ciphersData,
|
||||
collectionViewList = collectionsData,
|
||||
folderViewList = foldersData,
|
||||
sendViewList = sendsData.sendViewList,
|
||||
)
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
init {
|
||||
// Cancel any ongoing sync request and clear the vault data in memory every time
|
||||
// the user switches or the vault is locked for the active user.
|
||||
merge(
|
||||
authDiskSource
|
||||
.userSwitchingChangesFlow
|
||||
.onEach {
|
||||
// DomainState is not part of the locked data but should still be cleared
|
||||
// when the user changes
|
||||
mutableDomainsStateFlow.update { DataState.Loading }
|
||||
},
|
||||
vaultLockManager
|
||||
.vaultUnlockDataStateFlow
|
||||
.filter { vaultUnlockDataList ->
|
||||
// Clear if the active user is not currently unlocking or unlocked
|
||||
vaultUnlockDataList.none { it.userId == activeUserId }
|
||||
},
|
||||
)
|
||||
.onEach {
|
||||
syncJob.cancel()
|
||||
clearUnlockedData()
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
// Setup ciphers MutableStateFlow
|
||||
mutableDecryptCipherListResultFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultLockManager.vaultUnlockDataStateFlow,
|
||||
) { activeUserId -> observeVaultDiskCiphersToCipherListView(userId = activeUserId) }
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
// Setup domains MutableStateFlow
|
||||
mutableDomainsStateFlow
|
||||
.observeWhenSubscribedAndLoggedIn(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
) { activeUserId -> observeVaultDiskDomains(userId = activeUserId) }
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup folders MutableStateFlow
|
||||
mutableFoldersStateFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultLockManager.vaultUnlockDataStateFlow,
|
||||
) { activeUserId -> observeVaultDiskFolders(userId = activeUserId) }
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup collections MutableStateFlow
|
||||
mutableCollectionsStateFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultLockManager.vaultUnlockDataStateFlow,
|
||||
) { activeUserId -> observeVaultDiskCollections(userId = activeUserId) }
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup sends MutableStateFlow
|
||||
mutableSendDataStateFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultLockManager.vaultUnlockDataStateFlow,
|
||||
) { activeUserId -> observeVaultDiskSends(userId = activeUserId) }
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.fullSyncFlow
|
||||
.onEach { userId ->
|
||||
if (userId == activeUserId) {
|
||||
sync(forced = false)
|
||||
} else {
|
||||
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||
}
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
databaseSchemeManager
|
||||
.databaseSchemeChangeFlow
|
||||
.onEach { sync(forced = true) }
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override fun sync(forced: Boolean) {
|
||||
val userId = activeUserId ?: return
|
||||
if (!syncJob.isCompleted) return
|
||||
mutableDecryptCipherListResultFlow.updateToPendingOrLoading()
|
||||
mutableDomainsStateFlow.updateToPendingOrLoading()
|
||||
mutableFoldersStateFlow.updateToPendingOrLoading()
|
||||
mutableCollectionsStateFlow.updateToPendingOrLoading()
|
||||
mutableSendDataStateFlow.updateToPendingOrLoading()
|
||||
syncJob = ioScope.launch { syncInternal(userId = userId, forced = forced) }
|
||||
}
|
||||
|
||||
override fun syncIfNecessary() {
|
||||
val userId = activeUserId ?: return
|
||||
// Sync if we have never done so or the last time was at last 30 minutes ago.
|
||||
val shouldSync = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.let {
|
||||
clock.instant().isAfter(it.plus(SYNC_IF_NECESSARY_DELAY_MIN, ChronoUnit.MINUTES))
|
||||
}
|
||||
?: true
|
||||
if (shouldSync) {
|
||||
sync(forced = false)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncForResult(forced: Boolean): SyncVaultDataResult {
|
||||
val userId = activeUserId ?: return SyncVaultDataResult.Error(NoActiveUserException())
|
||||
syncJob = ioScope
|
||||
.async { syncInternal(userId = userId, forced = forced) }
|
||||
.also {
|
||||
return try {
|
||||
it.await()
|
||||
} catch (e: CancellationException) {
|
||||
SyncVaultDataResult.Error(throwable = e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private suspend fun syncInternal(
|
||||
userId: String,
|
||||
forced: Boolean,
|
||||
): SyncVaultDataResult {
|
||||
if (!forced) {
|
||||
// Skip this check if we are forcing the request.
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
lastSyncInstant?.let { lastSyncTimeMs ->
|
||||
// If the lasSyncState is null we just sync, no checks required.
|
||||
syncService.getAccountRevisionDateMillis().fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncTimeMs) {
|
||||
// We can skip the actual sync call if there is no new data or
|
||||
// database scheme changes since the last sync.
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
val itemsAvailable = vaultDiskSource
|
||||
.getCiphersFlow(userId)
|
||||
.firstOrNull()
|
||||
?.isNotEmpty() == true
|
||||
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(throwable = it)
|
||||
return SyncVaultDataResult.Error(throwable = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return syncService.sync().fold(
|
||||
onSuccess = { syncResponse ->
|
||||
val localSecurityStamp = authDiskSource.userState?.activeAccount?.profile?.stamp
|
||||
val serverSecurityStamp = syncResponse.profile.securityStamp
|
||||
// Log the user out if the stamps do not match
|
||||
localSecurityStamp?.let {
|
||||
if (serverSecurityStamp != localSecurityStamp) {
|
||||
// Ensure UserLogoutManager is available
|
||||
userLogoutManager.softLogout(
|
||||
userId = userId,
|
||||
reason = LogoutReason.SecurityStamp,
|
||||
)
|
||||
return SyncVaultDataResult.Error(SecurityStampMismatchException())
|
||||
}
|
||||
}
|
||||
|
||||
// Update user information with additional information from sync response
|
||||
authDiskSource.userState = authDiskSource.userState?.toUpdatedUserStateJson(
|
||||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
|
||||
storeProfileData(syncResponse = syncResponse)
|
||||
|
||||
// Treat absent network policies as known empty data to
|
||||
// distinguish between unknown null data.
|
||||
authDiskSource.storePolicies(
|
||||
userId = userId,
|
||||
policies = syncResponse.policies.orEmpty(),
|
||||
)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
|
||||
val itemsAvailable = syncResponse.ciphers?.isNotEmpty() == true
|
||||
SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(throwable = it)
|
||||
SyncVaultDataResult.Error(throwable = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun unlockVaultForOrganizationsIfNecessary(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: return
|
||||
|
||||
// There shouldn't be issues when unlocking directly from the syncResponse so we can ignore
|
||||
// the return type here.
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = syncResponse.profile.id,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
}
|
||||
|
||||
private fun storeProfileData(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
val profile = syncResponse.profile
|
||||
val userId = profile.id
|
||||
authDiskSource.apply {
|
||||
storeUserKey(userId = userId, userKey = profile.key)
|
||||
storePrivateKey(userId = userId, privateKey = profile.privateKey)
|
||||
storeAccountKeys(userId = userId, accountKeys = profile.accountKeys)
|
||||
storeOrganizationKeys(
|
||||
userId = userId,
|
||||
organizationKeys = profile.organizations
|
||||
.orEmpty()
|
||||
.filter { it.key != null }
|
||||
.associate { it.id to requireNotNull(it.key) },
|
||||
)
|
||||
storeShouldUseKeyConnector(
|
||||
userId = userId,
|
||||
shouldUseKeyConnector = profile.shouldUseKeyConnector,
|
||||
)
|
||||
storeOrganizations(userId = userId, organizations = profile.organizations)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeVaultDiskCiphersToCipherListView(
|
||||
userId: String,
|
||||
): Flow<DataState<DecryptCipherListResult>> =
|
||||
vaultDiskSource
|
||||
.getCiphersFlow(userId = userId)
|
||||
.onStart { mutableDecryptCipherListResultFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
vaultLockManager.waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptCipherListWithFailures(
|
||||
userId = userId,
|
||||
cipherList = it.toEncryptedSdkCipherList(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { result ->
|
||||
DataState.Loaded(
|
||||
result.copy(successes = result.successes.sortAlphabetically()),
|
||||
)
|
||||
},
|
||||
onFailure = { throwable -> DataState.Error(error = throwable) },
|
||||
)
|
||||
}
|
||||
.map {
|
||||
it
|
||||
.takeUnless { settingsDiskSource.getLastSyncTime(userId = userId) == null }
|
||||
?: DataState.Loading
|
||||
}
|
||||
.onEach { mutableDecryptCipherListResultFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskDomains(
|
||||
userId: String,
|
||||
): Flow<DataState<DomainsData>> =
|
||||
vaultDiskSource
|
||||
.getDomains(userId = userId)
|
||||
.onStart { mutableDomainsStateFlow.updateToPendingOrLoading() }
|
||||
.map { DataState.Loaded(data = it.toDomainsData()) }
|
||||
.onEach { mutableDomainsStateFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskFolders(
|
||||
userId: String,
|
||||
): Flow<DataState<List<FolderView>>> =
|
||||
vaultDiskSource
|
||||
.getFolders(userId = userId)
|
||||
.onStart { mutableFoldersStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
vaultLockManager.waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptFolderList(userId = userId, folderList = it.toEncryptedSdkFolderList())
|
||||
.fold(
|
||||
onSuccess = { folders -> DataState.Loaded(folders.sortAlphabetically()) },
|
||||
onFailure = { throwable -> DataState.Error(throwable) },
|
||||
)
|
||||
}
|
||||
.map { it.orLoadingIfNotSynced(userId = userId) }
|
||||
.onEach { mutableFoldersStateFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskCollections(
|
||||
userId: String,
|
||||
): Flow<DataState<List<CollectionView>>> =
|
||||
vaultDiskSource
|
||||
.getCollections(userId = userId)
|
||||
.onStart { mutableCollectionsStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
vaultLockManager.waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptCollectionList(
|
||||
userId = userId,
|
||||
collectionList = it.toEncryptedSdkCollectionList(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { collections ->
|
||||
DataState.Loaded(
|
||||
data = collections.sortAlphabeticallyByTypeAndOrganization(
|
||||
userOrganizations = authDiskSource
|
||||
.getOrganizations(userId = userId)
|
||||
.orEmpty(),
|
||||
),
|
||||
)
|
||||
},
|
||||
onFailure = { throwable -> DataState.Error(error = throwable) },
|
||||
)
|
||||
}
|
||||
.map { it.orLoadingIfNotSynced(userId = userId) }
|
||||
.onEach { mutableCollectionsStateFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskSends(
|
||||
userId: String,
|
||||
): Flow<DataState<SendData>> =
|
||||
vaultDiskSource
|
||||
.getSends(userId = userId)
|
||||
.onStart { mutableSendDataStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
vaultLockManager.waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptSendList(userId = userId, sendList = it.toEncryptedSdkSendList())
|
||||
.fold(
|
||||
onSuccess = { sends -> DataState.Loaded(sends.sortAlphabetically()) },
|
||||
onFailure = { throwable -> DataState.Error(throwable) },
|
||||
)
|
||||
}
|
||||
.map { it.orLoadingIfNotSynced(userId = userId) }
|
||||
.map { dataState -> dataState.map { SendData(sendViewList = it) } }
|
||||
.onEach { mutableSendDataStateFlow.value = it }
|
||||
|
||||
private fun clearUnlockedData() {
|
||||
mutableDecryptCipherListResultFlow.update { DataState.Loading }
|
||||
mutableFoldersStateFlow.update { DataState.Loading }
|
||||
mutableCollectionsStateFlow.update { DataState.Loading }
|
||||
mutableSendDataStateFlow.update { DataState.Loading }
|
||||
}
|
||||
|
||||
private fun updateVaultStateFlowsToError(throwable: Throwable) {
|
||||
mutableDecryptCipherListResultFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(data = currentState.data)
|
||||
}
|
||||
mutableDomainsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(data = currentState.data)
|
||||
}
|
||||
mutableFoldersStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(data = currentState.data)
|
||||
}
|
||||
mutableCollectionsStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(data = currentState.data)
|
||||
}
|
||||
mutableSendDataStateFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(data = currentState.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given [DataState] as-is, or [DataState.Loading] if vault data for the given
|
||||
* [userId] has not synced. This can be used to distinguish between empty data in the database
|
||||
* because we are in the process of syncing from legitimately having no vault data.
|
||||
*/
|
||||
private fun <T> DataState<List<T>>.orLoadingIfNotSynced(
|
||||
userId: String,
|
||||
): DataState<List<T>> =
|
||||
this
|
||||
.takeUnless { settingsDiskSource.getLastSyncTime(userId = userId) == null }
|
||||
?: DataState.Loading
|
||||
}
|
||||
|
||||
private fun <T> Throwable.toNetworkOrErrorState(
|
||||
data: T?,
|
||||
): DataState<T> =
|
||||
if (isNoConnectionError()) {
|
||||
DataState.NoNetwork(data = data)
|
||||
} else {
|
||||
DataState.Error(error = this, data = data)
|
||||
}
|
||||
@@ -5,37 +5,23 @@ import com.bitwarden.core.data.manager.realtime.RealtimeManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.FolderManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FolderManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManagerImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -61,8 +47,6 @@ object VaultManagerModule {
|
||||
fileManager: FileManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
): CipherManager = CipherManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
@@ -71,48 +55,6 @@ object VaultManagerModule {
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
pushManager = pushManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFolderManager(
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
): FolderManager = FolderManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
pushManager = pushManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendManager(
|
||||
sendsService: SendsService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
fileManager: FileManager,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): SendManager = SendManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
sendsService = sendsService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
pushManager = pushManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -168,44 +110,4 @@ object VaultManagerModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
clock = clock,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVaultSyncManager(
|
||||
syncService: SyncService,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
clock: Clock,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): VaultSyncManager = VaultSyncManagerImpl(
|
||||
syncService = syncService,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
userLogoutManager = userLogoutManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
clock = clock,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
pushManager = pushManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCredentialExchangeImportManager(
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
ciphersService: CiphersService,
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
): CredentialExchangeImportManager = CredentialExchangeImportManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
ciphersService = ciphersService,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager.model
|
||||
|
||||
/**
|
||||
* Models result of the vault data being imported from a CXF payload.
|
||||
*/
|
||||
sealed class ImportCxfPayloadResult {
|
||||
|
||||
/**
|
||||
* The vault data has been successfully imported.
|
||||
*/
|
||||
data class Success(val itemCount: Int) : ImportCxfPayloadResult()
|
||||
|
||||
/**
|
||||
* There are no items to import.
|
||||
*/
|
||||
data object NoItems : ImportCxfPayloadResult()
|
||||
|
||||
/**
|
||||
* The sync process has failed after importing the CXF payload.
|
||||
*/
|
||||
data class SyncFailed(val error: Throwable) : ImportCxfPayloadResult()
|
||||
|
||||
/**
|
||||
* There was an error importing the vault data.
|
||||
*
|
||||
* @param error The error that occurred during import.
|
||||
*/
|
||||
data class Error(val error: Throwable) : ImportCxfPayloadResult()
|
||||
}
|
||||
@@ -1,25 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FolderManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -30,12 +41,7 @@ import javax.crypto.Cipher
|
||||
* Responsible for managing vault data inside the network layer.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface VaultRepository :
|
||||
CipherManager,
|
||||
FolderManager,
|
||||
SendManager,
|
||||
VaultLockManager,
|
||||
VaultSyncManager {
|
||||
interface VaultRepository : CipherManager, VaultLockManager {
|
||||
|
||||
/**
|
||||
* The [VaultFilterType] for the current user.
|
||||
@@ -45,6 +51,52 @@ interface VaultRepository :
|
||||
*/
|
||||
var vaultFilterType: VaultFilterType
|
||||
|
||||
/**
|
||||
* Flow that represents the current vault data.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val vaultDataStateFlow: StateFlow<DataState<VaultData>>
|
||||
|
||||
/**
|
||||
* Flow that represents all ciphers for the active user, including references to ciphers that
|
||||
* cannot be decrypted.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val decryptCipherListResultStateFlow: StateFlow<DataState<DecryptCipherListResult>>
|
||||
|
||||
/**
|
||||
* Flow that represents all collections for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val collectionsStateFlow: StateFlow<DataState<List<CollectionView>>>
|
||||
|
||||
/**
|
||||
* Flow that represents all domains for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val domainsStateFlow: StateFlow<DataState<DomainsData>>
|
||||
|
||||
/**
|
||||
* Flow that represents all folders for the active user.
|
||||
*
|
||||
* Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself
|
||||
* must be collected in order to trigger state changes.
|
||||
*/
|
||||
val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
|
||||
|
||||
/**
|
||||
* Flow that represents the current send data.
|
||||
*/
|
||||
val sendDataStateFlow: StateFlow<DataState<SendData>>
|
||||
|
||||
/**
|
||||
* Flow that represents the totp code.
|
||||
*/
|
||||
@@ -55,6 +107,26 @@ interface VaultRepository :
|
||||
*/
|
||||
fun deleteVaultData(userId: String)
|
||||
|
||||
/**
|
||||
* Sync the vault data for the current user.
|
||||
*
|
||||
* Unlike [syncIfNecessary], this will always perform the requested sync and should only be
|
||||
* utilized in cases where the user specifically requested the action.
|
||||
*/
|
||||
fun sync(forced: Boolean = false)
|
||||
|
||||
/**
|
||||
* Checks if conditions have been met to perform a sync request and, if so, syncs the vault
|
||||
* data for the current user.
|
||||
*/
|
||||
fun syncIfNecessary()
|
||||
|
||||
/**
|
||||
* Syncs the vault data for the current user. This is an explicit request to sync and will
|
||||
* return the result of the sync as a [SyncVaultDataResult].
|
||||
*/
|
||||
suspend fun syncForResult(): SyncVaultDataResult
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a specific vault item as found by ID. This may emit `null`
|
||||
* if the item cannot be found.
|
||||
@@ -131,11 +203,50 @@ interface VaultRepository :
|
||||
pin: String,
|
||||
): VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a
|
||||
* [SendView.type] of [SendType.FILE].
|
||||
*/
|
||||
suspend fun createSend(sendView: SendView, fileUri: Uri?): CreateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to update a send.
|
||||
*/
|
||||
suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to remove the password from a send.
|
||||
*/
|
||||
suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult
|
||||
|
||||
/**
|
||||
* Attempt to get the verification code and the period.
|
||||
*/
|
||||
suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a send.
|
||||
*/
|
||||
suspend fun deleteSend(sendId: String): DeleteSendResult
|
||||
|
||||
/**
|
||||
* Attempt to create a folder.
|
||||
*/
|
||||
suspend fun createFolder(folderView: FolderView): CreateFolderResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a folder.
|
||||
*/
|
||||
suspend fun deleteFolder(folderId: String): DeleteFolderResult
|
||||
|
||||
/**
|
||||
* Attempt to update a folder.
|
||||
*/
|
||||
suspend fun updateFolder(folderId: String, folderView: FolderView): UpdateFolderResult
|
||||
|
||||
/**
|
||||
* Attempt to get the user's vault data for export.
|
||||
*
|
||||
@@ -147,20 +258,6 @@ interface VaultRepository :
|
||||
restrictedTypes: List<CipherType>,
|
||||
): ExportVaultDataResult
|
||||
|
||||
/**
|
||||
* Attempt to import a CXF payload.
|
||||
*
|
||||
* @param payload The CXF payload to import.
|
||||
*/
|
||||
suspend fun importCxfPayload(payload: String): ImportCredentialsResult
|
||||
|
||||
/**
|
||||
* Attempt to export the vault data to a CXF file.
|
||||
*
|
||||
* @param ciphers Ciphers selected for export.
|
||||
*/
|
||||
suspend fun exportVaultDataToCxf(ciphers: List<CipherListView>): Result<String>
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a specific vault list item as found by ID. This may emit
|
||||
* `null` if the item cannot be found.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,29 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.di
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FolderManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -29,28 +36,42 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
syncService: SyncService,
|
||||
sendsService: SendsService,
|
||||
ciphersService: CiphersService,
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
cipherManager: CipherManager,
|
||||
folderManager: FolderManager,
|
||||
sendManager: SendManager,
|
||||
fileManager: FileManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
totpCodeManager: TotpCodeManager,
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
pushManager: PushManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
cipherManager = cipherManager,
|
||||
folderManager = folderManager,
|
||||
sendManager = sendManager,
|
||||
fileManager = fileManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
credentialExchangeImportManager = credentialExchangeImportManager,
|
||||
pushManager = pushManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
/**
|
||||
* Represents the result of importing credentials from a credential manager.
|
||||
*/
|
||||
sealed class ImportCredentialsResult {
|
||||
|
||||
/**
|
||||
* Indicates the vault data has been successfully imported.
|
||||
*/
|
||||
data class Success(val itemCount: Int) : ImportCredentialsResult()
|
||||
|
||||
/**
|
||||
* Indicates there are no items to import.
|
||||
*/
|
||||
data object NoItems : ImportCredentialsResult()
|
||||
|
||||
/**
|
||||
* Indicates the vault data has been successfully uploaded, but there was an error syncing the
|
||||
* vault data.
|
||||
*/
|
||||
data class SyncFailed(val error: Throwable) : ImportCredentialsResult()
|
||||
|
||||
/**
|
||||
* Indicates there was an error importing the vault data.
|
||||
*
|
||||
* @param error The error that occurred during import.
|
||||
*/
|
||||
data class Error(val error: Throwable) : ImportCredentialsResult()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager.model
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
/**
|
||||
* Represents the result of a sync operation.
|
||||
@@ -14,7 +14,7 @@ sealed class SyncVaultDataResult {
|
||||
/**
|
||||
* Indicates a failed sync operation.
|
||||
*
|
||||
* @property throwable The exception that caused the failure.
|
||||
* @property throwable The exception that caused the failure, if any.
|
||||
*/
|
||||
data class Error(val throwable: Throwable) : SyncVaultDataResult()
|
||||
data class Error(val throwable: Throwable?) : SyncVaultDataResult()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.exporters.Account
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
|
||||
/**
|
||||
* Converts a [AccountJson] to a [Account] for use in the SDK.
|
||||
*/
|
||||
fun AccountJson.toSdkAccount(): Account = Account(
|
||||
id = profile.userId,
|
||||
email = profile.email,
|
||||
name = profile.name,
|
||||
)
|
||||
@@ -14,10 +14,8 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.UriMatchTypeJson
|
||||
import com.bitwarden.vault.Attachment
|
||||
import com.bitwarden.vault.Card
|
||||
import com.bitwarden.vault.CardListView
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherPermissions
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
@@ -28,7 +26,6 @@ import com.bitwarden.vault.Field
|
||||
import com.bitwarden.vault.FieldType
|
||||
import com.bitwarden.vault.Identity
|
||||
import com.bitwarden.vault.Login
|
||||
import com.bitwarden.vault.LoginListView
|
||||
import com.bitwarden.vault.LoginUri
|
||||
import com.bitwarden.vault.PasswordHistory
|
||||
import com.bitwarden.vault.SecureNote
|
||||
@@ -106,7 +103,6 @@ fun Cipher.toEncryptedNetworkCipherResponse(
|
||||
shouldViewPassword = viewPassword,
|
||||
key = key,
|
||||
encryptedFor = encryptedFor,
|
||||
archivedDate = archivedDate?.let { ZonedDateTime.ofInstant(it, ZoneOffset.UTC) },
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -390,7 +386,6 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
|
||||
creationDate = creationDate.toInstant(),
|
||||
deletedDate = deletedDate?.toInstant(),
|
||||
revisionDate = revisionDate.toInstant(),
|
||||
archivedDate = archivedDate?.toInstant(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -656,55 +651,3 @@ fun EncryptionContext.toEncryptedNetworkCipher(): CipherJsonRequest =
|
||||
*/
|
||||
fun EncryptionContext.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher =
|
||||
cipher.toEncryptedNetworkCipherResponse(encryptedFor = encryptedFor)
|
||||
|
||||
/**
|
||||
* Converts a Bitwarden SDK [Cipher] object to a corresponding
|
||||
* [CipherListView] object with modified field to represent a decryption error instance.
|
||||
* This allows reuse of existing logic for filtering and grouping ciphers to construct
|
||||
* the sections in the vault list.
|
||||
*/
|
||||
fun Cipher.toFailureCipherListView(): CipherListView =
|
||||
CipherListView(
|
||||
id = id,
|
||||
organizationId = organizationId,
|
||||
folderId = folderId,
|
||||
collectionIds = collectionIds,
|
||||
key = key,
|
||||
name = name,
|
||||
subtitle = "",
|
||||
type = when (type) {
|
||||
CipherType.LOGIN -> CipherListViewType.Login(
|
||||
v1 = LoginListView(
|
||||
fido2Credentials = null,
|
||||
hasFido2 = false,
|
||||
username = null,
|
||||
totp = null,
|
||||
uris = null,
|
||||
),
|
||||
)
|
||||
|
||||
CipherType.SECURE_NOTE -> CipherListViewType.SecureNote
|
||||
CipherType.CARD -> CipherListViewType.Card(
|
||||
CardListView(
|
||||
brand = null,
|
||||
),
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> CipherListViewType.Identity
|
||||
CipherType.SSH_KEY -> CipherListViewType.SshKey
|
||||
},
|
||||
favorite = favorite,
|
||||
reprompt = reprompt,
|
||||
organizationUseTotp = organizationUseTotp,
|
||||
edit = edit,
|
||||
permissions = permissions,
|
||||
viewPassword = viewPassword,
|
||||
attachments = 0.toUInt(),
|
||||
hasOldAttachments = false,
|
||||
localData = null,
|
||||
creationDate = creationDate,
|
||||
deletedDate = deletedDate,
|
||||
revisionDate = revisionDate,
|
||||
copyableFields = emptyList(),
|
||||
archivedDate = archivedDate,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.util
|
||||
|
||||
import com.bitwarden.collections.Collection
|
||||
import com.bitwarden.collections.CollectionType
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.data.repository.util.SpecialCharWithPrecedenceComparator
|
||||
import com.bitwarden.network.model.CollectionTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
|
||||
/**
|
||||
@@ -20,8 +18,6 @@ fun SyncResponseJson.Collection.toEncryptedSdkCollection(): Collection =
|
||||
hidePasswords = this.shouldHidePasswords,
|
||||
readOnly = this.isReadOnly,
|
||||
manage = this.canManage ?: !this.isReadOnly,
|
||||
defaultUserCollectionEmail = this.defaultUserCollectionEmail,
|
||||
type = this.type.toSdkCollectionType(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -32,54 +28,13 @@ fun List<SyncResponseJson.Collection>.toEncryptedSdkCollectionList(): List<Colle
|
||||
map { it.toEncryptedSdkCollection() }
|
||||
|
||||
/**
|
||||
* Sorts a list of [CollectionView] objects based on a multi-level sorting logic.
|
||||
*
|
||||
* The sorting criteria are as follows, in order of precedence:
|
||||
* 1. Collections of type `DEFAULT_USER_COLLECTION` are placed first.
|
||||
* 2. All other collections are grouped by their `CollectionType`.
|
||||
* 3. Within each group, collections are sorted alphabetically by name. For collections of
|
||||
* type `DEFAULT_USER_COLLECTION`, the corresponding organization's name is used for sorting
|
||||
* instead of the collection's own name.
|
||||
*
|
||||
* This function uses a [SpecialCharWithPrecedenceComparator] for the alphabetical sort.
|
||||
*
|
||||
* @param userOrganizations A list of the user's organizations, used to find the name for
|
||||
* `DEFAULT_USER_COLLECTION` types.
|
||||
* @return A new list containing the sorted [CollectionView] objects.
|
||||
* Sorts the data in alphabetical order by name.
|
||||
*/
|
||||
fun List<CollectionView>.sortAlphabeticallyByTypeAndOrganization(
|
||||
userOrganizations: List<SyncResponseJson.Profile.Organization>,
|
||||
): List<CollectionView> {
|
||||
@JvmName("toAlphabeticallySortedCollectionList")
|
||||
fun List<CollectionView>.sortAlphabetically(): List<CollectionView> {
|
||||
return this.sortedWith(
|
||||
// DEFAULT_USER_COLLECTION come first
|
||||
comparator = compareBy<CollectionView> { it.type != CollectionType.DEFAULT_USER_COLLECTION }
|
||||
// Then sort by other CollectionType ordinals
|
||||
.thenBy { it.type }
|
||||
// Finally, sort within each group. For default collections, use the
|
||||
// organization's name; for others, use the collection's name.
|
||||
.thenBy(SpecialCharWithPrecedenceComparator) {
|
||||
if (it.type == CollectionType.DEFAULT_USER_COLLECTION) {
|
||||
// For default collections, sort by the organization's name
|
||||
userOrganizations
|
||||
.find { org -> org.id == it.organizationId }
|
||||
?.name
|
||||
?: it.name
|
||||
} else {
|
||||
// For other collections, sort by the collection's name
|
||||
it.name
|
||||
}
|
||||
}
|
||||
// As a final fallback if names are identical, sort by ID to ensure a stable order
|
||||
.thenBy { it.id },
|
||||
comparator = { collection1, collection2 ->
|
||||
SpecialCharWithPrecedenceComparator.compare(collection1.name, collection2.name)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a [CollectionType] object to a corresponding
|
||||
* Bitwarden SDK [CollectionTypeJson] object.
|
||||
*/
|
||||
fun CollectionTypeJson.toSdkCollectionType(): CollectionType =
|
||||
when (this) {
|
||||
CollectionTypeJson.SHARED_COLLECTION -> CollectionType.SHARED_COLLECTION
|
||||
CollectionTypeJson.DEFAULT_USER_COLLECTION -> CollectionType.DEFAULT_USER_COLLECTION
|
||||
}
|
||||
|
||||
@@ -87,18 +87,9 @@ fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? =
|
||||
/**
|
||||
* Add the setup autofill screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupAutoFillDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToBrowserAutofill: () -> Unit,
|
||||
) {
|
||||
fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
|
||||
composableWithSlideTransitions<SetupAutofillRoute.Standard> {
|
||||
SetupAutoFillScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToBrowserAutofill = {
|
||||
onNavigateBack()
|
||||
onNavigateToBrowserAutofill()
|
||||
},
|
||||
)
|
||||
SetupAutoFillScreen(onNavigateBack = onNavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +102,6 @@ fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
|
||||
onNavigateBack = {
|
||||
// No-Op
|
||||
},
|
||||
onNavigateToBrowserAutofill = {
|
||||
// No-Op
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -28,7 +27,6 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) :
|
||||
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
@@ -102,22 +100,15 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
|
||||
private fun handleTurnOnLaterConfirmClick() {
|
||||
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true)
|
||||
updateOnboardingStatusToNextStep()
|
||||
updateOnboardingStatusToFinalStep()
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
|
||||
if (state.isInitialSetup) {
|
||||
updateOnboardingStatusToNextStep()
|
||||
updateOnboardingStatusToFinalStep()
|
||||
} else {
|
||||
val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled
|
||||
if (isBrowserAutofillUnconfigured) {
|
||||
sendEvent(SetupAutoFillEvent.NavigateToBrowserAutofill)
|
||||
} else {
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
}
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,18 +120,10 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOnboardingStatusToNextStep() {
|
||||
val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value
|
||||
val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled
|
||||
val nextStep = when {
|
||||
!isAutofillEnabled -> OnboardingStatus.FINAL_STEP
|
||||
isBrowserAutofillUnconfigured -> OnboardingStatus.BROWSER_AUTOFILL_SETUP
|
||||
else -> OnboardingStatus.FINAL_STEP
|
||||
}
|
||||
authRepository.setOnboardingStatus(status = nextStep)
|
||||
}
|
||||
private fun updateOnboardingStatusToFinalStep() =
|
||||
authRepository.setOnboardingStatus(
|
||||
status = OnboardingStatus.FINAL_STEP,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,11 +164,6 @@ sealed class SetupAutoFillEvent {
|
||||
*/
|
||||
data object NavigateToAutofillSettings : SetupAutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the setup browser autofill screen.
|
||||
*/
|
||||
data object NavigateToBrowserAutofill : SetupAutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
|
||||
@@ -27,14 +27,14 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.image.BitwardenGifImage
|
||||
@@ -60,7 +60,6 @@ import com.x8bit.bitwarden.ui.platform.manager.utils.startSystemAutofillSettings
|
||||
@Composable
|
||||
fun SetupAutoFillScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToBrowserAutofill: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: SetupAutoFillViewModel = hiltViewModel(),
|
||||
) {
|
||||
@@ -76,7 +75,6 @@ fun SetupAutoFillScreen(
|
||||
}
|
||||
|
||||
SetupAutoFillEvent.NavigateBack -> onNavigateBack()
|
||||
SetupAutoFillEvent.NavigateToBrowserAutofill -> onNavigateToBrowserAutofill()
|
||||
}
|
||||
}
|
||||
when (state.dialogState) {
|
||||
@@ -116,7 +114,7 @@ fun SetupAutoFillScreen(
|
||||
id = if (state.isInitialSetup) {
|
||||
BitwardenString.account_setup
|
||||
} else {
|
||||
BitwardenString.autofill_setup
|
||||
BitwardenString.turn_on_autofill
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -184,14 +182,13 @@ private fun SetupAutoFillContent(
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = onContinueClick,
|
||||
isEnabled = state.autofillEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (state.isInitialSetup) {
|
||||
BitwardenOutlinedButton(
|
||||
BitwardenTextButton(
|
||||
label = stringResource(BitwardenString.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.toRoute
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.bitwarden.ui.platform.util.ParcelableRouteSerializer
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the setup browser autofill screen.
|
||||
*/
|
||||
@Parcelize
|
||||
@Serializable(with = SetupBrowserAutofillRoute.Serializer::class)
|
||||
sealed class SetupBrowserAutofillRoute : Parcelable {
|
||||
/**
|
||||
* The [isInitialSetup] value used in the setup browser autofill screen.
|
||||
*/
|
||||
abstract val isInitialSetup: Boolean
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<SetupBrowserAutofillRoute>(
|
||||
kClass = SetupBrowserAutofillRoute::class,
|
||||
)
|
||||
|
||||
/**
|
||||
* The type-safe route for the standard setup browser autofill screen.
|
||||
*/
|
||||
@Parcelize
|
||||
@Serializable(with = Standard.Serializer::class)
|
||||
data object Standard : SetupBrowserAutofillRoute() {
|
||||
override val isInitialSetup: Boolean get() = false
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<Standard>(Standard::class)
|
||||
}
|
||||
|
||||
/**
|
||||
* The type-safe route for the root setup browser autofill screen.
|
||||
*/
|
||||
@Parcelize
|
||||
@Serializable(with = AsRoot.Serializer::class)
|
||||
data object AsRoot : SetupBrowserAutofillRoute() {
|
||||
override val isInitialSetup: Boolean get() = true
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<AsRoot>(AsRoot::class)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for the [SetupBrowserAutofillScreen] using [SavedStateHandle].
|
||||
*/
|
||||
data class SetupBrowserAutofillScreenArgs(val isInitialSetup: Boolean)
|
||||
|
||||
/**
|
||||
* Constructs a [SetupAutoFillScreenArgs] from the [SavedStateHandle] and internal route data.
|
||||
*/
|
||||
fun SavedStateHandle.toSetupBrowserAutofillArgs(): SetupBrowserAutofillScreenArgs {
|
||||
val route = this.toRoute<SetupBrowserAutofillRoute>()
|
||||
return SetupBrowserAutofillScreenArgs(isInitialSetup = route.isInitialSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the setup browser autofill screen.
|
||||
*/
|
||||
fun NavController.navigateToSetupBrowserAutofillScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(route = SetupBrowserAutofillRoute.Standard, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the setup browser autofill screen as the root.
|
||||
*/
|
||||
fun NavController.navigateToSetupBrowserAutoFillAsRootScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(route = SetupBrowserAutofillRoute.AsRoot, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup browser autofill screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupBrowserAutofillDestination(onNavigateBack: () -> Unit) {
|
||||
composableWithSlideTransitions<SetupBrowserAutofillRoute.Standard> {
|
||||
SetupBrowserAutofillScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup browser autofill screen to the nav graph as a root.
|
||||
*/
|
||||
fun NavGraphBuilder.setupBrowserAutofillDestinationAsRoot() {
|
||||
composableWithPushTransitions<SetupBrowserAutofillRoute.AsRoot> {
|
||||
SetupBrowserAutofillScreen(
|
||||
onNavigateBack = {
|
||||
// No-Op
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.BrowserAutofillSettingsCard
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption
|
||||
import com.x8bit.bitwarden.ui.platform.manager.utils.startBrowserAutofillSettingsActivity
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Top level composable for the Setup Browser Autofill screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SetupBrowserAutofillScreen(
|
||||
viewModel: SetupBrowserAutofillViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
SetupBrowserAutofillEvent.NavigateBack -> onNavigateBack()
|
||||
is SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings -> {
|
||||
intentManager.startBrowserAutofillSettingsActivity(
|
||||
browserPackage = event.browserPackage,
|
||||
)
|
||||
}
|
||||
|
||||
SetupBrowserAutofillEvent.NavigateToBrowserIntegrationsInfo -> {
|
||||
intentManager.launchUri(
|
||||
"https://bitwarden.com/help/auto-fill-android/#browser-integrations/".toUri(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SetupBrowserAutofillDialogs(
|
||||
dialogState = state.dialogState,
|
||||
onDismissDialog = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.DismissDialog) }
|
||||
},
|
||||
onTurnOnLaterConfirm = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterConfirmClick) }
|
||||
},
|
||||
)
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(
|
||||
id = if (state.isInitialSetup) {
|
||||
BitwardenString.account_setup
|
||||
} else {
|
||||
BitwardenString.autofill_setup
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = if (state.isInitialSetup) {
|
||||
null
|
||||
} else {
|
||||
NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.CloseClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
SetupBrowserAutofillContent(
|
||||
state = state,
|
||||
onWhyIsThisStepRequiredClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.WhyIsThisStepRequiredClick) }
|
||||
},
|
||||
onBrowserClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.BrowserIntegrationClick(it)) }
|
||||
},
|
||||
onContinueClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.ContinueClick) }
|
||||
},
|
||||
onTurnOnLaterClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SetupBrowserAutofillAction.TurnOnLaterClick) }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SetupBrowserAutofillContent(
|
||||
state: SetupBrowserAutofillState,
|
||||
onWhyIsThisStepRequiredClick: () -> Unit,
|
||||
onBrowserClick: (BrowserPackage) -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
onTurnOnLaterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.turn_on_browser_autofill_integration),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = BitwardenPlurals.youre_using_a_browser_that_requires_special_permissions,
|
||||
count = state.browserCount,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = BitwardenString.why_is_this_step_required),
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
onClick = onWhyIsThisStepRequiredClick,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BrowserAutofillSettingsCard(
|
||||
options = state.browserAutofillSettingsOptions,
|
||||
onOptionClicked = onBrowserClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = onContinueClick,
|
||||
isEnabled = state.isContinueEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
if (state.isInitialSetup) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(BitwardenString.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetupBrowserAutofillDialogs(
|
||||
dialogState: SetupBrowserAutofillState.DialogState?,
|
||||
onTurnOnLaterConfirm: () -> Unit,
|
||||
onDismissDialog: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
SetupBrowserAutofillState.DialogState.TurnOnLaterDialog -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(BitwardenString.turn_on_browser_autofill_integration_later),
|
||||
message = stringResource(
|
||||
id = BitwardenString.return_to_complete_this_step_anytime_in_settings,
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = onTurnOnLaterConfirm,
|
||||
onDismissClick = onDismissDialog,
|
||||
onDismissRequest = onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SetupBrowserAutofillContent_preview() {
|
||||
BitwardenTheme {
|
||||
SetupBrowserAutofillContent(
|
||||
state = SetupBrowserAutofillState(
|
||||
dialogState = null,
|
||||
isInitialSetup = true,
|
||||
browserAutofillSettingsOptions = persistentListOf(
|
||||
BrowserAutofillSettingsOption.BraveStable(enabled = true),
|
||||
BrowserAutofillSettingsOption.ChromeStable(enabled = false),
|
||||
BrowserAutofillSettingsOption.ChromeBeta(enabled = true),
|
||||
),
|
||||
),
|
||||
onWhyIsThisStepRequiredClick = { },
|
||||
onBrowserClick = { },
|
||||
onContinueClick = { },
|
||||
onTurnOnLaterClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.util.toBrowserAutoFillSettingsOptions
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* View model for the Setup Browser Autofill screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupBrowserAutofillViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<SetupBrowserAutofillState, SetupBrowserAutofillEvent, SetupBrowserAutofillAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: SetupBrowserAutofillState(
|
||||
dialogState = null,
|
||||
isInitialSetup = savedStateHandle.toSetupBrowserAutofillArgs().isInitialSetup,
|
||||
browserAutofillSettingsOptions = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.toBrowserAutoFillSettingsOptions(),
|
||||
),
|
||||
) {
|
||||
init {
|
||||
browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatusFlow
|
||||
.map(SetupBrowserAutofillAction.Internal::BrowserAutofillStatusReceive)
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: SetupBrowserAutofillAction) {
|
||||
when (action) {
|
||||
is SetupBrowserAutofillAction.BrowserIntegrationClick -> {
|
||||
handleBrowserIntegrationClick(action)
|
||||
}
|
||||
|
||||
SetupBrowserAutofillAction.WhyIsThisStepRequiredClick -> {
|
||||
handleWhyIsThisStepRequiredClick()
|
||||
}
|
||||
|
||||
SetupBrowserAutofillAction.CloseClick -> handleCloseClick()
|
||||
SetupBrowserAutofillAction.DismissDialog -> handleDismissDialog()
|
||||
SetupBrowserAutofillAction.ContinueClick -> handleContinueClick()
|
||||
SetupBrowserAutofillAction.TurnOnLaterClick -> handleTurnOnLaterClick()
|
||||
SetupBrowserAutofillAction.TurnOnLaterConfirmClick -> handleTurnOnLaterConfirmClick()
|
||||
is SetupBrowserAutofillAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: SetupBrowserAutofillAction.Internal) {
|
||||
when (action) {
|
||||
is SetupBrowserAutofillAction.Internal.BrowserAutofillStatusReceive -> {
|
||||
handleBrowserAutofillStatusReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBrowserIntegrationClick(
|
||||
action: SetupBrowserAutofillAction.BrowserIntegrationClick,
|
||||
) {
|
||||
sendEvent(
|
||||
SetupBrowserAutofillEvent.NavigateToBrowserAutofillSettings(action.browserPackage),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleWhyIsThisStepRequiredClick() {
|
||||
sendEvent(SetupBrowserAutofillEvent.NavigateToBrowserIntegrationsInfo)
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(SetupBrowserAutofillEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
firstTimeActionManager.storeShowBrowserAutofillSettingBadge(showBadge = false)
|
||||
if (state.isInitialSetup) {
|
||||
authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
|
||||
} else {
|
||||
sendEvent(SetupBrowserAutofillEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTurnOnLaterClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SetupBrowserAutofillState.DialogState.TurnOnLaterDialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTurnOnLaterConfirmClick() {
|
||||
firstTimeActionManager.storeShowBrowserAutofillSettingBadge(showBadge = true)
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
authRepository.setOnboardingStatus(status = OnboardingStatus.FINAL_STEP)
|
||||
}
|
||||
|
||||
private fun handleBrowserAutofillStatusReceive(
|
||||
action: SetupBrowserAutofillAction.Internal.BrowserAutofillStatusReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
browserAutofillSettingsOptions = action.status.toBrowserAutoFillSettingsOptions(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI State for the Setup Browser Autofill screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SetupBrowserAutofillState(
|
||||
val dialogState: DialogState?,
|
||||
val isInitialSetup: Boolean,
|
||||
val browserAutofillSettingsOptions: ImmutableList<BrowserAutofillSettingsOption>,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* The number of browsers that can be configured.
|
||||
*/
|
||||
val browserCount: Int get() = browserAutofillSettingsOptions.size
|
||||
|
||||
/**
|
||||
* Indicates if the Continue button should be enabled or not.
|
||||
*/
|
||||
val isContinueEnabled: Boolean get() = browserAutofillSettingsOptions.all { it.isEnabled }
|
||||
|
||||
/**
|
||||
* Models dialogs that can be shown on the Setup Browser Autofill screen.
|
||||
*/
|
||||
@Parcelize
|
||||
sealed class DialogState : Parcelable {
|
||||
/**
|
||||
* Represents the turn on later dialog.
|
||||
*/
|
||||
data object TurnOnLaterDialog : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Events for the Setup Browser Autofill screen.
|
||||
*/
|
||||
sealed class SetupBrowserAutofillEvent {
|
||||
/**
|
||||
* Navigates back.
|
||||
*/
|
||||
data object NavigateBack : SetupBrowserAutofillEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Autofill settings of the specified [browserPackage].
|
||||
*/
|
||||
data class NavigateToBrowserAutofillSettings(
|
||||
val browserPackage: BrowserPackage,
|
||||
) : SetupBrowserAutofillEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the browser integrations info page.
|
||||
*/
|
||||
data object NavigateToBrowserIntegrationsInfo : SetupBrowserAutofillEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Actions for the Setup Browser Autofill screen.
|
||||
*/
|
||||
sealed class SetupBrowserAutofillAction {
|
||||
/**
|
||||
* Indicates that a browser integration toggle was clicked.
|
||||
*/
|
||||
data class BrowserIntegrationClick(
|
||||
val browserPackage: BrowserPackage,
|
||||
) : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the close button has been clicked.
|
||||
*/
|
||||
data object CloseClick : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the dialog has been dismissed.
|
||||
*/
|
||||
data object DismissDialog : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the "Continue" button was clicked.
|
||||
*/
|
||||
data object ContinueClick : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the "Turn on later" button was clicked.
|
||||
*/
|
||||
data object TurnOnLaterClick : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the confirmation button was clicked to turn on later.
|
||||
*/
|
||||
data object TurnOnLaterConfirmClick : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Indicates that the "Why is this step required?" button was clicked.
|
||||
*/
|
||||
data object WhyIsThisStepRequiredClick : SetupBrowserAutofillAction()
|
||||
|
||||
/**
|
||||
* Models actions the [SetupBrowserAutofillViewModel] itself may send.
|
||||
*/
|
||||
sealed class Internal : SetupBrowserAutofillAction() {
|
||||
/**
|
||||
* Received updated [BrowserThirdPartyAutofillStatus] data.
|
||||
*/
|
||||
data class BrowserAutofillStatusReceive(
|
||||
val status: BrowserThirdPartyAutofillStatus,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
|
||||
@@ -31,14 +31,14 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
@@ -218,11 +218,11 @@ private fun SetUpLaterButton(
|
||||
) {
|
||||
var displayConfirmation by rememberSaveable { mutableStateOf(value = false) }
|
||||
if (displayConfirmation) {
|
||||
@Suppress("MaxLineLength")
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = BitwardenString.set_up_unlock_later),
|
||||
message = stringResource(
|
||||
id = BitwardenString
|
||||
.you_can_return_to_complete_this_step_anytime_from_account_security_in_settings,
|
||||
id = BitwardenString.you_can_return_to_complete_this_step_anytime_from_account_security_in_settings,
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
@@ -235,7 +235,7 @@ private fun SetUpLaterButton(
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.set_up_later),
|
||||
onClick = { displayConfirmation = true },
|
||||
modifier = modifier.testTag(tag = "SetUpLaterButton"),
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -35,7 +34,6 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
@@ -205,14 +203,10 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun updateOnboardingStatusToNextStep() {
|
||||
val isAutofillEnabled = settingsRepository.isAutofillEnabledStateFlow.value
|
||||
val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled
|
||||
val nextStep = when {
|
||||
!isAutofillEnabled -> OnboardingStatus.AUTOFILL_SETUP
|
||||
isBrowserAutofillUnconfigured -> OnboardingStatus.BROWSER_AUTOFILL_SETUP
|
||||
else -> OnboardingStatus.FINAL_STEP
|
||||
val nextStep = if (settingsRepository.isAutofillEnabledStateFlow.value) {
|
||||
OnboardingStatus.FINAL_STEP
|
||||
} else {
|
||||
OnboardingStatus.AUTOFILL_SETUP
|
||||
}
|
||||
authRepository.setOnboardingStatus(nextStep)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.completeregistration
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -25,13 +26,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
@@ -46,9 +48,6 @@ import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
|
||||
import com.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
@@ -75,15 +74,16 @@ fun CompleteRegistrationScreen(
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = rememberCompleteRegistrationHandler(viewModel = viewModel)
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
val context = LocalContext.current
|
||||
|
||||
// route OS back actions through the VM to clear the special circumstance
|
||||
BackHandler(onBack = handler.onBackClick)
|
||||
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
is CompleteRegistrationEvent.ShowSnackbar -> {
|
||||
snackbarHostState.showSnackbar(BitwardenSnackbarData(message = event.message))
|
||||
is CompleteRegistrationEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance()
|
||||
@@ -143,7 +143,6 @@ fun CompleteRegistrationScreen(
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -3,13 +3,10 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
@@ -55,7 +52,6 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val toastManager: ToastManager,
|
||||
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val args = savedStateHandle.toCompleteRegistrationArgs()
|
||||
@@ -148,7 +144,9 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
|
||||
viewModelScope.launch {
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()),
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = BitwardenString.email_verified.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -243,7 +241,11 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
|
||||
private fun handleLoginResult(action: Internal.ReceiveLoginResult) {
|
||||
clearDialogState()
|
||||
toastManager.show(messageId = BitwardenString.account_created_success)
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = BitwardenString.account_created_success.asText(),
|
||||
),
|
||||
)
|
||||
|
||||
authRepository.setOnboardingStatus(
|
||||
status = OnboardingStatus.NOT_STARTED,
|
||||
@@ -319,11 +321,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenPlurals.master_password_length_val_message_x
|
||||
.asPluralsText(
|
||||
quantity = MIN_PASSWORD_LENGTH,
|
||||
args = arrayOf(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -500,9 +499,9 @@ sealed class CompleteRegistrationEvent {
|
||||
data object NavigateBack : CompleteRegistrationEvent()
|
||||
|
||||
/**
|
||||
* Show a snackbar with the given message.
|
||||
* Show a toast with the given message.
|
||||
*/
|
||||
data class ShowSnackbar(
|
||||
data class ShowToast(
|
||||
val message: Text,
|
||||
) : CompleteRegistrationEvent()
|
||||
|
||||
|
||||
@@ -24,12 +24,11 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
@@ -155,11 +154,7 @@ private fun MinimumCharacterCount(
|
||||
}
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
id = BitwardenPlurals.minimum_characters,
|
||||
count = minimumCharacterCount,
|
||||
formatArgs = arrayOf(minimumCharacterCount),
|
||||
),
|
||||
text = stringResource(BitwardenString.minimum_characters, minimumCharacterCount),
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.labelSmall,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
|
||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user