mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 19:20:11 -05:00
Compare commits
128 Commits
release/20
...
agalles/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e0a1ea6e | ||
|
|
30cd148d5f | ||
|
|
878ef0d74a | ||
|
|
8387aed394 | ||
|
|
3f875d83ff | ||
|
|
85951a502f | ||
|
|
04c38101b2 | ||
|
|
7e080775f6 | ||
|
|
a90a11d5b1 | ||
|
|
eea675f3b0 | ||
|
|
55c150a595 | ||
|
|
274e8293a3 | ||
|
|
c8710588b9 | ||
|
|
251d3001c2 | ||
|
|
acc9113f9a | ||
|
|
2eb829a25b | ||
|
|
04a1d4118f | ||
|
|
7e20acd6a2 | ||
|
|
dd2cf0fe0a | ||
|
|
9f63cede11 | ||
|
|
a93037d63e | ||
|
|
4e57f306d3 | ||
|
|
1638a20bf0 | ||
|
|
874edfad69 | ||
|
|
0469731fba | ||
|
|
0abfa5bb97 | ||
|
|
13e6728d46 | ||
|
|
4749080fa5 | ||
|
|
836b3ccf1f | ||
|
|
116bfd6351 | ||
|
|
6ca8a39355 | ||
|
|
24a54ce214 | ||
|
|
8d76ef50d3 | ||
|
|
22114d588a | ||
|
|
81245cf3e5 | ||
|
|
fec6479f6a | ||
|
|
a02a84ee08 | ||
|
|
df63bb4b6c | ||
|
|
2a134c619d | ||
|
|
5c5bd25d16 | ||
|
|
2363b0d619 | ||
|
|
f0946e05d5 | ||
|
|
24ccebd822 | ||
|
|
fd555e92d3 | ||
|
|
eab2c17614 | ||
|
|
617be1fd95 | ||
|
|
d5d4caea62 | ||
|
|
7bf4acbb28 | ||
|
|
2694138aa1 | ||
|
|
d2645863ea | ||
|
|
3edd5bd852 | ||
|
|
4cd5a1ed56 | ||
|
|
c122f83fa6 | ||
|
|
b558d70703 | ||
|
|
89ad7818f9 | ||
|
|
e91ba77105 | ||
|
|
cc685b2307 | ||
|
|
d14fba0c01 | ||
|
|
e965134697 | ||
|
|
df34db52e4 | ||
|
|
cf5d208516 | ||
|
|
d74040e7b9 | ||
|
|
8a2bcfade8 | ||
|
|
bc1dd730ec | ||
|
|
fa5053b5cc | ||
|
|
ad46d8d7c0 | ||
|
|
98530ed33d | ||
|
|
e57af949fc | ||
|
|
6f6aacabfb | ||
|
|
b0e0b44671 | ||
|
|
d53f3f313c | ||
|
|
4f244c52fa | ||
|
|
b4a31764c4 | ||
|
|
f4569cef2b | ||
|
|
b4926b72d9 | ||
|
|
0f899df83c | ||
|
|
12ea84c548 | ||
|
|
c13973c22a | ||
|
|
d5e9463dfa | ||
|
|
8006189dba | ||
|
|
e188a8eef8 | ||
|
|
70a266e6c7 | ||
|
|
898ea3c050 | ||
|
|
f5833eec71 | ||
|
|
ff03f49f43 | ||
|
|
2756bd9fde | ||
|
|
a39f83349f | ||
|
|
7d3ed2af88 | ||
|
|
8de465381e | ||
|
|
0864b2deeb | ||
|
|
f22f4399be | ||
|
|
766e6b1bb9 | ||
|
|
0fb364128e | ||
|
|
0cbce39499 | ||
|
|
f954b0b941 | ||
|
|
cfd0a5b8a5 | ||
|
|
d61e1cb6f1 | ||
|
|
b31983da8b | ||
|
|
e22d309423 | ||
|
|
9b53095b5e | ||
|
|
c6814c8870 | ||
|
|
7710ad8a73 | ||
|
|
80b3a7e675 | ||
|
|
8235045dad | ||
|
|
481a8c8fbc | ||
|
|
1dc6ea2227 | ||
|
|
6554234898 | ||
|
|
e990397b29 | ||
|
|
417835ef3f | ||
|
|
39a6dd1c4b | ||
|
|
4093e61b09 | ||
|
|
c4adf3ad42 | ||
|
|
417a1494e3 | ||
|
|
ef39ea6d5d | ||
|
|
f6c20e08d1 | ||
|
|
987e065dd7 | ||
|
|
ba7ee04281 | ||
|
|
808d57edc5 | ||
|
|
3356925c7a | ||
|
|
0487d95122 | ||
|
|
0834a7a883 | ||
|
|
2b0e8f9941 | ||
|
|
0702078b04 | ||
|
|
46c7e79039 | ||
|
|
1d6e733c08 | ||
|
|
a298b85374 | ||
|
|
fe79ea4822 | ||
|
|
4c50f873e2 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -48,3 +48,9 @@
|
||||
# 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: '17'
|
||||
default: '21'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -31,12 +31,12 @@ runs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -27,6 +27,9 @@
|
||||
],
|
||||
"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: 17
|
||||
JAVA_VERSION: 21
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -110,10 +110,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
JAVA_VERSION: 21
|
||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -118,10 +118,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -429,10 +429,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -503,7 +503,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1-5'
|
||||
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -11,24 +11,12 @@ permissions:
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
|
||||
publish-release-authenticator:
|
||||
name: Publish Authenticator Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
|
||||
with:
|
||||
release_name: "Authenticator"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
workflow_name: "publish-github-release-bwa.yml"
|
||||
credentials_filename: "authenticator_play_store-creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
24
.github/workflows/publish-github-release-bwpm.yml
vendored
Normal file
24
.github/workflows/publish-github-release-bwpm.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release-bwpm.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
30
.github/workflows/publish-store.yml
vendored
30
.github/workflows/publish-store.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: Publish to Google Play
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
run-name: >
|
||||
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -46,6 +47,10 @@ on:
|
||||
- production
|
||||
- Fastlane Automation Target
|
||||
required: true
|
||||
dry-run:
|
||||
description: "Dry-Run, Run the workflow without publishing to the store"
|
||||
type: boolean
|
||||
default: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
@@ -54,6 +59,7 @@ permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
@@ -71,10 +77,10 @@ jobs:
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -122,6 +128,8 @@ jobs:
|
||||
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Promote Play Store version to production
|
||||
if: ${{ inputs.dry-run == false }}
|
||||
id: publish
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
@@ -158,3 +166,19 @@ jobs:
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
track:"$TRACK_FROM" \
|
||||
trackPromoteTo:"$TRACK_TARGET"
|
||||
|
||||
- name: Enable Publish Github Release Workflow
|
||||
if: ${{ steps.publish.conclusion == 'success' || inputs.dry-run }}
|
||||
env:
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
DRY_RUN: ${{ inputs.dry-run }}
|
||||
run: |
|
||||
if $DRY_RUN ; then
|
||||
gh workflow view publish-github-release.yml
|
||||
exit 0
|
||||
fi
|
||||
if [ "$PRODUCT" = "Password Manager" ]; then
|
||||
gh workflow enable publish-github-release-bwpm.yml
|
||||
elif [ "$PRODUCT" = "Authenticator" ]; then
|
||||
gh workflow enable publish-github-release-bwa.yml
|
||||
fi
|
||||
|
||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/sdlc-sdk-update.yml
vendored
4
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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: 17
|
||||
_JAVA_VERSION: 21
|
||||
_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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.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@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
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@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
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.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-partitions (1.1166.0)
|
||||
aws-sdk-core (3.233.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.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.231.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-s3 (1.199.1)
|
||||
aws-sdk-core (~> 3, >= 3.231.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.2)
|
||||
bigdecimal (3.2.3)
|
||||
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.13.2)
|
||||
json (2.15.0)
|
||||
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.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.20.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.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` `17`:
|
||||
4. Setup JDK `Version` `21`:
|
||||
|
||||
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
|
||||
- Hit the selected Gradle JDK next to `Gradle JDK:`.
|
||||
- Select a `17.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `17`.
|
||||
- Select a `21.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `21`.
|
||||
- Select your preferred `Vendor`.
|
||||
- Hit `Download`.
|
||||
- Hit `Apply`.
|
||||
|
||||
@@ -224,6 +224,7 @@ dependencies {
|
||||
|
||||
implementation(project(":annotation"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":cxf"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":network"))
|
||||
implementation(project(":ui"))
|
||||
@@ -245,6 +246,8 @@ 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)
|
||||
@@ -258,7 +261,6 @@ 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)
|
||||
|
||||
@@ -20,6 +20,18 @@
|
||||
<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"
|
||||
android:label="@string/autofill_title"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -5,6 +5,8 @@ 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
|
||||
@@ -295,6 +297,7 @@ 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 {
|
||||
@@ -418,6 +421,16 @@ class MainViewModel @Inject constructor(
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
|
||||
importCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CredentialExchangeExport(
|
||||
data = ImportCredentialsRequestData(
|
||||
uri = importCredentialsRequest.uri,
|
||||
requestJson = importCredentialsRequest.request.requestJson,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ fun TrustDeviceResponse.toUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val deviceOptions = decryptionOptions
|
||||
.trustedDeviceUserDecryptionOptions
|
||||
|
||||
@@ -33,6 +33,8 @@ 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
|
||||
@@ -740,7 +742,17 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationCodeResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@@ -753,7 +765,17 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationOtpResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
|
||||
@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
|
||||
*/
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
val error: Throwable?,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
@@ -54,6 +55,23 @@ 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,
|
||||
@@ -61,6 +79,7 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
@@ -90,6 +109,7 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
|
||||
@@ -16,6 +16,8 @@ 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
|
||||
@@ -24,6 +26,8 @@ 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
|
||||
@@ -61,6 +65,22 @@ 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(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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,7 +6,9 @@ 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.
|
||||
@@ -15,4 +17,20 @@ 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,7 +24,6 @@ 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
|
||||
@@ -93,13 +92,11 @@ object CredentialProviderModule {
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
privilegedAppRepository: PrivilegedAppRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): OriginManager =
|
||||
OriginManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
privilegedAppRepository = privilegedAppRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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
|
||||
@@ -23,7 +21,6 @@ class OriginManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
private val privilegedAppRepository: PrivilegedAppRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : OriginManager {
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
@@ -70,10 +67,7 @@ class OriginManagerImpl(
|
||||
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
|
||||
.takeUnless {
|
||||
it is ValidateOriginResult.Error.PrivilegedAppNotAllowed &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.UserManagedPrivilegedApps)
|
||||
}
|
||||
.takeUnless { it is ValidateOriginResult.Error.PrivilegedAppNotAllowed }
|
||||
?: 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,
|
||||
val credentialProperties: CredentialProperties? = null,
|
||||
) {
|
||||
/**
|
||||
* Represents properties for newly created credential.
|
||||
|
||||
@@ -105,6 +105,11 @@ 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.
|
||||
*/
|
||||
@@ -281,6 +286,23 @@ 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,6 +37,7 @@ 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"
|
||||
@@ -48,6 +49,7 @@ 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].
|
||||
@@ -72,6 +74,9 @@ class SettingsDiskSourceImpl(
|
||||
private val mutablePullToRefreshEnabledFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowBrowserAutofillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -224,6 +229,12 @@ 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)
|
||||
@@ -431,6 +442,21 @@ 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),
|
||||
@@ -598,6 +624,13 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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,6 +63,12 @@ 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,6 +4,7 @@ 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
|
||||
@@ -25,12 +26,14 @@ 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)
|
||||
@@ -78,11 +81,12 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// Can be expanded to support multiple autofill settings
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOfNotNull(showAutofillBadge)
|
||||
}
|
||||
combine(
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = it),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = it),
|
||||
) { showAutofillBadge, showBrowserAutofillBadge ->
|
||||
listOf(showAutofillBadge, showBrowserAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
@@ -124,6 +128,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
|
||||
getShowAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = activeUserId),
|
||||
getShowBrowserAutofillSettingBadgeFlowInternal(userId = activeUserId),
|
||||
),
|
||||
) {
|
||||
FirstTimeState(
|
||||
@@ -131,19 +136,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupUnlockCard = it[1],
|
||||
showSetupAutofillCard = it[2],
|
||||
showImportLoginsCardInSettings = it[3],
|
||||
showSetupBrowserAutofillCard = it[4],
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart {
|
||||
emit(
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
.onStart { emit(currentOrDefaultUserFirstTimeState) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
@@ -176,14 +173,11 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
showSetupAutofillCard = getShowAutofillSettingBadgeInternal(it),
|
||||
showImportLoginsCardInSettings = settingsDiskSource
|
||||
.getShowImportLoginsSettingBadge(it),
|
||||
showSetupBrowserAutofillCard = settingsDiskSource
|
||||
.getShowBrowserAutofillSettingBadge(it),
|
||||
)
|
||||
}
|
||||
?: FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
showImportLoginsCardInSettings = null,
|
||||
)
|
||||
?: FirstTimeState()
|
||||
|
||||
override fun storeShowUnlockSettingBadge(showBadge: Boolean) {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
@@ -193,6 +187,14 @@ 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(
|
||||
@@ -257,6 +259,19 @@ 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.
|
||||
|
||||
@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
interface PushManager {
|
||||
/**
|
||||
* Flow that represents requests intended for full syncs.
|
||||
* Flow that represents requests intended for full syncs for the user ID provided.
|
||||
*/
|
||||
val fullSyncFlow: Flow<Unit>
|
||||
val fullSyncFlow: Flow<String>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to log a user out.
|
||||
|
||||
@@ -55,7 +55,7 @@ class PushManagerImpl @Inject constructor(
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow<String>()
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
@@ -73,7 +73,7 @@ class PushManagerImpl @Inject constructor(
|
||||
private val mutableSyncSendUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
|
||||
override val fullSyncFlow: SharedFlow<Unit>
|
||||
override val fullSyncFlow: SharedFlow<String>
|
||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||
|
||||
override val logoutFlow: SharedFlow<NotificationLogoutData>
|
||||
@@ -204,7 +204,10 @@ class PushManagerImpl @Inject constructor(
|
||||
NotificationType.SYNC_SETTINGS,
|
||||
NotificationType.SYNC_VAULT,
|
||||
-> {
|
||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncNotification>(notification.payload)
|
||||
.userId
|
||||
?.let { mutableFullSyncSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.SYNC_FOLDER_CREATE,
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
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].
|
||||
*/
|
||||
@@ -13,7 +24,7 @@ class SdkClientManagerImpl(
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
|
||||
Client(settings = null).apply {
|
||||
Client(tokenProvider = Token(), settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -18,6 +19,7 @@ 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
|
||||
@@ -41,8 +43,6 @@ 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,10 +242,6 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
@@ -355,12 +351,14 @@ 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,6 +8,7 @@ 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
|
||||
@@ -18,10 +19,12 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,4 +83,12 @@ sealed class NotificationPayload {
|
||||
@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,6 +2,7 @@ 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
|
||||
@@ -133,6 +134,14 @@ 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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -71,3 +72,12 @@ 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,6 +24,14 @@ 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,6 +97,24 @@ 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) {
|
||||
|
||||
@@ -37,6 +37,15 @@ 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].
|
||||
*/
|
||||
|
||||
@@ -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,6 +9,7 @@ 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
|
||||
@@ -428,6 +429,23 @@ 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,6 +2,7 @@ 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
|
||||
@@ -10,6 +11,7 @@ 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
|
||||
@@ -92,14 +94,17 @@ class VaultSdkSourceImpl(
|
||||
),
|
||||
)
|
||||
DeriveKeyConnectorResult.Success(key)
|
||||
} catch (exception: BitwardenException) {
|
||||
when {
|
||||
exception.message == "Wrong password" -> {
|
||||
} catch (ex: BitwardenException.DeriveKeyConnector) {
|
||||
when (ex.v1) {
|
||||
is DeriveKeyConnectorException.WrongPassword -> {
|
||||
DeriveKeyConnectorResult.WrongPasswordError
|
||||
}
|
||||
|
||||
else -> DeriveKeyConnectorResult.Error(exception)
|
||||
is DeriveKeyConnectorException.Crypto -> {
|
||||
DeriveKeyConnectorResult.Error(error = ex)
|
||||
}
|
||||
}
|
||||
} catch (exception: BitwardenException) {
|
||||
DeriveKeyConnectorResult.Error(error = exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,7 +478,7 @@ class VaultSdkSourceImpl(
|
||||
): Result<UpdatePasswordResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.crypto()
|
||||
.updatePassword(newPassword = newPassword)
|
||||
.makeUpdatePassword(newPassword = newPassword)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToString(
|
||||
@@ -491,6 +496,28 @@ 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,
|
||||
|
||||
@@ -31,7 +31,7 @@ fun PublicKeyCredentialAuthenticatorAttestationResponse.toAndroidAttestationResp
|
||||
.ClientExtensionResults
|
||||
.CredentialProperties(residentKey = residentKey),
|
||||
)
|
||||
},
|
||||
} ?: Fido2AttestationResponse.ClientExtensionResults(),
|
||||
authenticatorAttachment = authenticatorAttachment,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -17,7 +18,10 @@ 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
|
||||
@@ -34,13 +38,18 @@ 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")
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class CipherManagerImpl(
|
||||
private val fileManager: FileManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
@@ -49,9 +58,24 @@ 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(
|
||||
@@ -641,4 +665,79 @@ 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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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) },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
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,23 +5,37 @@ 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
|
||||
@@ -47,6 +61,8 @@ object VaultManagerModule {
|
||||
fileManager: FileManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
): CipherManager = CipherManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
@@ -55,6 +71,48 @@ 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
|
||||
@@ -110,4 +168,44 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
package com.x8bit.bitwarden.data.vault.manager.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, if any.
|
||||
* @property throwable The exception that caused the failure.
|
||||
*/
|
||||
data class Error(val throwable: Throwable?) : SyncVaultDataResult()
|
||||
data class Error(val throwable: Throwable) : SyncVaultDataResult()
|
||||
}
|
||||
@@ -1,36 +1,25 @@
|
||||
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.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.ImportCredentialsResult
|
||||
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
|
||||
@@ -41,7 +30,12 @@ import javax.crypto.Cipher
|
||||
* Responsible for managing vault data inside the network layer.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface VaultRepository : CipherManager, VaultLockManager {
|
||||
interface VaultRepository :
|
||||
CipherManager,
|
||||
FolderManager,
|
||||
SendManager,
|
||||
VaultLockManager,
|
||||
VaultSyncManager {
|
||||
|
||||
/**
|
||||
* The [VaultFilterType] for the current user.
|
||||
@@ -51,52 +45,6 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -107,26 +55,6 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
*/
|
||||
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.
|
||||
@@ -203,50 +131,11 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
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.
|
||||
*
|
||||
@@ -258,6 +147,20 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
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,29 +1,22 @@
|
||||
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.FileManager
|
||||
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.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
|
||||
|
||||
/**
|
||||
@@ -36,42 +29,28 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
syncService: SyncService,
|
||||
sendsService: SendsService,
|
||||
ciphersService: CiphersService,
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
cipherManager: CipherManager,
|
||||
fileManager: FileManager,
|
||||
folderManager: FolderManager,
|
||||
sendManager: SendManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
totpCodeManager: TotpCodeManager,
|
||||
pushManager: PushManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
cipherManager = cipherManager,
|
||||
fileManager = fileManager,
|
||||
folderManager = folderManager,
|
||||
sendManager = sendManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
pushManager = pushManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
credentialExchangeImportManager = credentialExchangeImportManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
)
|
||||
@@ -106,6 +106,7 @@ fun Cipher.toEncryptedNetworkCipherResponse(
|
||||
shouldViewPassword = viewPassword,
|
||||
key = key,
|
||||
encryptedFor = encryptedFor,
|
||||
archivedDate = archivedDate?.let { ZonedDateTime.ofInstant(it, ZoneOffset.UTC) },
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -389,6 +390,7 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
|
||||
creationDate = creationDate.toInstant(),
|
||||
deletedDate = deletedDate?.toInstant(),
|
||||
revisionDate = revisionDate.toInstant(),
|
||||
archivedDate = archivedDate?.toInstant(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -704,4 +706,5 @@ fun Cipher.toFailureCipherListView(): CipherListView =
|
||||
deletedDate = deletedDate,
|
||||
revisionDate = revisionDate,
|
||||
copyableFields = emptyList(),
|
||||
archivedDate = archivedDate,
|
||||
)
|
||||
|
||||
@@ -87,9 +87,18 @@ fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? =
|
||||
/**
|
||||
* Add the setup autofill screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
|
||||
fun NavGraphBuilder.setupAutoFillDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToBrowserAutofill: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<SetupAutofillRoute.Standard> {
|
||||
SetupAutoFillScreen(onNavigateBack = onNavigateBack)
|
||||
SetupAutoFillScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToBrowserAutofill = {
|
||||
onNavigateBack()
|
||||
onNavigateToBrowserAutofill()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +111,9 @@ fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
|
||||
onNavigateBack = {
|
||||
// No-Op
|
||||
},
|
||||
onNavigateToBrowserAutofill = {
|
||||
// No-Op
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -27,6 +28,7 @@ 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.
|
||||
@@ -100,15 +102,22 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
|
||||
private fun handleTurnOnLaterConfirmClick() {
|
||||
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = true)
|
||||
updateOnboardingStatusToFinalStep()
|
||||
updateOnboardingStatusToNextStep()
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
firstTimeActionManager.storeShowAutoFillSettingBadge(showBadge = false)
|
||||
if (state.isInitialSetup) {
|
||||
updateOnboardingStatusToFinalStep()
|
||||
updateOnboardingStatusToNextStep()
|
||||
} else {
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
val isBrowserAutofillUnconfigured = browserThirdPartyAutofillEnabledManager
|
||||
.browserThirdPartyAutofillStatus
|
||||
.isAnyIsAvailableAndDisabled
|
||||
if (isBrowserAutofillUnconfigured) {
|
||||
sendEvent(SetupAutoFillEvent.NavigateToBrowserAutofill)
|
||||
} else {
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +129,18 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOnboardingStatusToFinalStep() =
|
||||
authRepository.setOnboardingStatus(
|
||||
status = OnboardingStatus.FINAL_STEP,
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +181,11 @@ 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.navigation.compose.hiltViewModel
|
||||
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.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
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,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.manager.utils.startSystemAutofillSettings
|
||||
@Composable
|
||||
fun SetupAutoFillScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToBrowserAutofill: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: SetupAutoFillViewModel = hiltViewModel(),
|
||||
) {
|
||||
@@ -75,6 +76,7 @@ fun SetupAutoFillScreen(
|
||||
}
|
||||
|
||||
SetupAutoFillEvent.NavigateBack -> onNavigateBack()
|
||||
SetupAutoFillEvent.NavigateToBrowserAutofill -> onNavigateToBrowserAutofill()
|
||||
}
|
||||
}
|
||||
when (state.dialogState) {
|
||||
@@ -114,7 +116,7 @@ fun SetupAutoFillScreen(
|
||||
id = if (state.isInitialSetup) {
|
||||
BitwardenString.account_setup
|
||||
} else {
|
||||
BitwardenString.turn_on_autofill
|
||||
BitwardenString.autofill_setup
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -182,13 +184,14 @@ 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) {
|
||||
BitwardenTextButton(
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(BitwardenString.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
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.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.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.navigation.compose.hiltViewModel
|
||||
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.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
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(
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenTextButton(
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = BitwardenString.set_up_later),
|
||||
onClick = { displayConfirmation = true },
|
||||
modifier = modifier.testTag(tag = "SetUpLaterButton"),
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -34,6 +35,7 @@ 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 {
|
||||
@@ -203,10 +205,14 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun updateOnboardingStatusToNextStep() {
|
||||
val nextStep = if (settingsRepository.isAutofillEnabledStateFlow.value) {
|
||||
OnboardingStatus.FINAL_STEP
|
||||
} else {
|
||||
OnboardingStatus.AUTOFILL_SETUP
|
||||
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
|
||||
}
|
||||
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.navigation.compose.hiltViewModel
|
||||
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.annotatedStringResource
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -26,14 +25,13 @@ 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.navigation.compose.hiltViewModel
|
||||
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
|
||||
@@ -48,6 +46,9 @@ 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
|
||||
@@ -74,16 +75,15 @@ fun CompleteRegistrationScreen(
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = rememberCompleteRegistrationHandler(viewModel = viewModel)
|
||||
val context = LocalContext.current
|
||||
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
// 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.ShowToast -> {
|
||||
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
|
||||
is CompleteRegistrationEvent.ShowSnackbar -> {
|
||||
snackbarHostState.showSnackbar(BitwardenSnackbarData(message = event.message))
|
||||
}
|
||||
|
||||
CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance()
|
||||
@@ -143,6 +143,7 @@ fun CompleteRegistrationScreen(
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
},
|
||||
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -54,6 +55,7 @@ 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()
|
||||
@@ -146,9 +148,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
|
||||
viewModelScope.launch {
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = BitwardenString.email_verified.asText(),
|
||||
),
|
||||
CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -243,11 +243,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
|
||||
private fun handleLoginResult(action: Internal.ReceiveLoginResult) {
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = BitwardenString.account_created_success.asText(),
|
||||
),
|
||||
)
|
||||
toastManager.show(messageId = BitwardenString.account_created_success)
|
||||
|
||||
authRepository.setOnboardingStatus(
|
||||
status = OnboardingStatus.NOT_STARTED,
|
||||
@@ -504,9 +500,9 @@ sealed class CompleteRegistrationEvent {
|
||||
data object NavigateBack : CompleteRegistrationEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given message.
|
||||
* Show a snackbar with the given message.
|
||||
*/
|
||||
data class ShowToast(
|
||||
data class ShowSnackbar(
|
||||
val message: Text,
|
||||
) : CompleteRegistrationEvent()
|
||||
|
||||
|
||||
@@ -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.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -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.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -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.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.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
|
||||
|
||||
@@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
|
||||
@@ -28,7 +28,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.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -27,7 +27,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.navigation.compose.hiltViewModel
|
||||
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.components.appbar.BitwardenTopAppBar
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -21,7 +21,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.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
|
||||
@@ -18,7 +18,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.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -21,7 +21,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.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.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
|
||||
|
||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
|
||||
@@ -36,7 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
|
||||
@@ -24,7 +24,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.navigation.compose.hiltViewModel
|
||||
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
|
||||
|
||||
@@ -32,7 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
|
||||
@@ -407,7 +407,8 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.verification_email_not_sent.asText(),
|
||||
message = result.message?.asText()
|
||||
?: BitwardenString.verification_email_not_sent.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user