mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
Compare commits
59 Commits
v2025.7.1-
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277fcbf14c | ||
|
|
729ec60ba8 | ||
|
|
3d220cf765 | ||
|
|
df2acadea0 | ||
|
|
7043b4be26 | ||
|
|
e4678cc7df | ||
|
|
e665c386ff | ||
|
|
2f2ec71fc4 | ||
|
|
7b115df83a | ||
|
|
edd1763198 | ||
|
|
37d3ff30e4 | ||
|
|
258a58aa25 | ||
|
|
da5dcef41e | ||
|
|
7a578ff2c5 | ||
|
|
355facc36b | ||
|
|
c60f3131b6 | ||
|
|
bb950c8c59 | ||
|
|
c7df80ff00 | ||
|
|
d308b84943 | ||
|
|
79ad18877d | ||
|
|
4f51507e4b | ||
|
|
88fcd35d1a | ||
|
|
987639b2a3 | ||
|
|
d32b4c7c7e | ||
|
|
9ed59e61a3 | ||
|
|
3342ebf139 | ||
|
|
4050215145 | ||
|
|
3e0ee5fcd8 | ||
|
|
fcd7326f2c | ||
|
|
c94fe56b47 | ||
|
|
17287680d9 | ||
|
|
e4935318de | ||
|
|
f22643fec1 | ||
|
|
6454dc1a58 | ||
|
|
411e359600 | ||
|
|
e75d7844de | ||
|
|
25680f9255 | ||
|
|
628cb12081 | ||
|
|
710e35680b | ||
|
|
b5cd0c9d9d | ||
|
|
9995fa92f1 | ||
|
|
44aae70fe4 | ||
|
|
fca4ebe023 | ||
|
|
2d2a5e74da | ||
|
|
b53ca30974 | ||
|
|
8178a61dba | ||
|
|
f0bdc8ede3 | ||
|
|
145c19da22 | ||
|
|
39b1409cbd | ||
|
|
f26d54a2e2 | ||
|
|
33cfaa5e95 | ||
|
|
9274e0f349 | ||
|
|
46656d659e | ||
|
|
811f0f2757 | ||
|
|
8f783a43e4 | ||
|
|
b8f74cdefa | ||
|
|
5e6dcb5b58 | ||
|
|
c5a40a89d9 | ||
|
|
929233081c |
25
.github/workflows/build-authenticator.yml
vendored
25
.github/workflows/build-authenticator.yml
vendored
@@ -32,6 +32,7 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -122,9 +123,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "BWA-AAB-KEYSTORE-STORE-PASSWORD,BWA-AAB-KEYSTORE-KEY-PASSWORD,BWA-APK-KEYSTORE-STORE-PASSWORD,BWA-APK-KEYSTORE-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -168,6 +178,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: AZ Logout
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
run: |
|
||||
@@ -222,18 +235,18 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane bundleAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
bundle exec fastlane buildAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
|
||||
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -33,6 +33,7 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -130,9 +131,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -169,6 +179,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
@@ -216,48 +229,48 @@ jobs:
|
||||
- name: Generate release Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
|
||||
env:
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreRelease \
|
||||
storeFile:app_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:upload \
|
||||
keyPassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store bundle
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
env:
|
||||
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_BETA_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_BETA_KEY_PASSWORD: ${{ secrets.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
|
||||
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreBeta \
|
||||
storeFile:app_beta_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta-upload \
|
||||
keyPassword:${{ env.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreReleaseApk \
|
||||
storeFile:app_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:${{ env.PLAY_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_BETA_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
|
||||
PLAY_BETA_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
|
||||
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreBetaApk \
|
||||
storeFile:app_beta_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
@@ -429,9 +442,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "FDROID-KEYSTORE-PASSWORD,FDROID-BETA-KEYSTORE-PASSWORD,FDROID-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -454,6 +476,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
@@ -508,7 +533,7 @@ jobs:
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidReleaseApk \
|
||||
storeFile:app_fdroid-keystore.jks \
|
||||
@@ -518,14 +543,14 @@ jobs:
|
||||
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
FDROID_BETA_KEY_PASSWORD: ${{ secrets.FDROID_BETA_KEY_PASSWORD }}
|
||||
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
|
||||
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidBetaApk \
|
||||
storeFile:app_beta_fdroid-keystore.jks \
|
||||
storePassword:"${{ env.FDROID_BETA_KEYSTORE_PASSWORD }}" \
|
||||
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
23
.github/workflows/crowdin-pull.yml
vendored
23
.github/workflows/crowdin-pull.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -28,10 +29,19 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -40,12 +50,15 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Download translations for ${{ matrix.name }}
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
|
||||
10
.github/workflows/crowdin-push.yml
vendored
10
.github/workflows/crowdin-push.yml
vendored
@@ -13,14 +13,17 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -40,6 +43,9 @@ jobs:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Upload sources for Authenticator
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
|
||||
22
.github/workflows/github-release.yml
vendored
22
.github/workflows/github-release.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
@@ -115,6 +116,23 @@ jobs:
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "JIRA-API-EMAIL,JIRA-API-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Get product release notes
|
||||
id: get_release_notes
|
||||
env:
|
||||
@@ -122,8 +140,8 @@ jobs:
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
|
||||
_RELEASE_TICKET_ID: ${{ inputs.release-ticket-id }}
|
||||
_JIRA_API_EMAIL: ${{ secrets.JIRA_API_EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
|
||||
run: |
|
||||
echo "Getting product release notes"
|
||||
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
|
||||
|
||||
38
.github/workflows/publish-github-release.yml
vendored
38
.github/workflows/publish-github-release.yml
vendored
@@ -1,14 +1,36 @@
|
||||
name: Publish GitHub Release as newest
|
||||
name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1-5'
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
stub:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Stub
|
||||
steps:
|
||||
- name: Stub
|
||||
run: echo "This is a stub job to trigger the workflow."
|
||||
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
|
||||
with:
|
||||
release_name: "Authenticator"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "authenticator_play_store-creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
|
||||
secrets: inherit
|
||||
|
||||
157
.github/workflows/publish-store.yml
vendored
157
.github/workflows/publish-store.yml
vendored
@@ -1,16 +1,159 @@
|
||||
|
||||
name: Publish
|
||||
|
||||
name: Publish to Google Play
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
product:
|
||||
description: "Which app is being released."
|
||||
type: choice
|
||||
options:
|
||||
- Password Manager
|
||||
- Authenticator
|
||||
version-name:
|
||||
description: "Version name to promote to production ex 2025.1.1"
|
||||
type: string
|
||||
version-code:
|
||||
description: "Build number to promote to production."
|
||||
required: true
|
||||
type: string
|
||||
rollout-percentage:
|
||||
description: "Percentage of users who will receive this version update."
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 10%
|
||||
- 30%
|
||||
- 50%
|
||||
- 100%
|
||||
default: 10%
|
||||
release-notes:
|
||||
description: "Change notes to be included with this release."
|
||||
type: string
|
||||
default: "Bug fixes."
|
||||
required: true
|
||||
track-from:
|
||||
description: "Track to promote from."
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- Fastlane Automation Source
|
||||
required: true
|
||||
default: "internal"
|
||||
track-target:
|
||||
description: "Track to promote to."
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- Fastlane Automation Target
|
||||
required: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
promote:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Promote build to Production in Play Store
|
||||
|
||||
steps:
|
||||
- name: TEST STEP
|
||||
run: exit 0
|
||||
- name: Log inputs to job summary
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
mkdir -p ${{ github.workspace }}/app/src/standardRelease
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Format Release Notes
|
||||
run: |
|
||||
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Promote Play Store version to production
|
||||
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 }}
|
||||
VERSION_CODE_INPUT: ${{ inputs.version-code }}
|
||||
VERSION_NAME: ${{inputs.version-name}}
|
||||
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
TRACK_FROM: ${{ inputs.track-from }}
|
||||
TRACK_TARGET: ${{ inputs.track-target }}
|
||||
run: |
|
||||
if [ "$PRODUCT" = "Password Manager" ]; then
|
||||
PACKAGE_NAME="com.x8bit.bitwarden"
|
||||
elif [ "$PRODUCT" = "Authenticator" ]; then
|
||||
PACKAGE_NAME="com.bitwarden.authenticator"
|
||||
else
|
||||
echo "Unsupported product: $PRODUCT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
|
||||
|
||||
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
|
||||
|
||||
bundle exec fastlane updateReleaseNotes \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
versionCode:"$VERSION_CODE"
|
||||
|
||||
bundle exec fastlane promoteToProduction \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
versionName:"$VERSION_NAME" \
|
||||
rolloutPercentage:"$decimal" \
|
||||
packageName:"$PACKAGE_NAME" \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
track:"$TRACK_FROM" \
|
||||
trackPromoteTo:"$TRACK_TARGET"
|
||||
|
||||
45
.github/workflows/scan-ci.yml
vendored
45
.github/workflows/scan-ci.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -20,14 +21,31 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
|
||||
cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
@@ -43,17 +61,36 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "SONAR-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
|
||||
44
.github/workflows/scan.yml
vendored
44
.github/workflows/scan.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -35,16 +36,33 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
|
||||
cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
@@ -64,6 +82,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -72,10 +91,27 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "SONAR-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.3.1
|
||||
3.4.2
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -7,3 +7,12 @@ gem 'time'
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
||||
# Since ruby 3.4.0 these are not included in the standard library
|
||||
gem 'abbrev'
|
||||
gem 'logger'
|
||||
gem 'mutex_m'
|
||||
gem 'csv'
|
||||
|
||||
# Starting with Ruby 3.5.0, these are not included in the standard library
|
||||
gem 'ostruct'
|
||||
|
||||
22
Gemfile.lock
22
Gemfile.lock
@@ -5,13 +5,14 @@ GEM
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1125.0)
|
||||
aws-sdk-core (3.226.2)
|
||||
aws-partitions (1.1131.0)
|
||||
aws-sdk-core (3.226.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -21,7 +22,7 @@ GEM
|
||||
aws-sdk-kms (1.106.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.192.0)
|
||||
aws-sdk-s3 (1.193.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -34,6 +35,7 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
@@ -165,13 +167,13 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
json (2.13.0)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.17.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
@@ -179,6 +181,7 @@ GEM
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
@@ -230,12 +233,17 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
abbrev
|
||||
csv
|
||||
fastlane
|
||||
fastlane-plugin-firebase_app_distribution
|
||||
logger
|
||||
mutex_m
|
||||
ostruct
|
||||
time
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.1p55
|
||||
ruby 3.4.2p28
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.6
|
||||
2.6.9
|
||||
|
||||
@@ -56,7 +56,7 @@ android {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "2025.4.0"
|
||||
versionName = "2025.7.0"
|
||||
|
||||
setProperty("archivesBaseName", "com.x8bit.bitwarden")
|
||||
|
||||
@@ -65,7 +65,12 @@ android {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"\uD83D\uDCBB local\"")}",
|
||||
)
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "SDK_VERSION",
|
||||
value = "\"${libs.versions.bitwardenSdk.get()}\"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -257,11 +262,10 @@ dependencies {
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(platform(libs.square.okhttp.bom))
|
||||
implementation(libs.square.okhttp)
|
||||
implementation(libs.square.okhttp.logging)
|
||||
implementation(platform(libs.square.retrofit.bom))
|
||||
implementation(libs.square.retrofit)
|
||||
implementation(libs.square.retrofit.kotlinx.serialization)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.zxing.zxing.core)
|
||||
|
||||
@@ -289,7 +293,6 @@ dependencies {
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.mockk.mockk)
|
||||
testImplementation(libs.robolectric.robolectric)
|
||||
testImplementation(libs.square.okhttp.mockwebserver)
|
||||
testImplementation(libs.square.turbine)
|
||||
}
|
||||
|
||||
|
||||
27
app/src/debug/res/xml/network_security_config.xml
Normal file
27
app/src/debug/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust pre-installed CAs -->
|
||||
<certificates src="system" />
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates
|
||||
src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="true">bitwarden.com</domain>
|
||||
<domain includeSubdomains="true">bitwarden.eu</domain>
|
||||
<domain includeSubdomains="true">bitwarden.pw</domain>
|
||||
<trust-anchors>
|
||||
<!-- Only trust pre-installed CAs for Bitwarden domains and all subdomains -->
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
|
||||
</network-security-config>
|
||||
7
app/src/debug/res/xml/provider.xml
Normal file
7
app/src/debug/res/xml/provider.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider>
|
||||
<capabilities>
|
||||
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
@@ -86,6 +86,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
|
||||
/**
|
||||
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
|
||||
@@ -11,7 +14,16 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
@OmitFromCoverage
|
||||
class AccessibilityActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
@@ -21,6 +23,7 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
private val viewModel: AuthCallbackViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = intent))
|
||||
@@ -35,4 +38,12 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -26,6 +29,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
@@ -37,6 +41,14 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
autofillTotpCopyViewModel
|
||||
.eventFlow
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -23,6 +24,7 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.setupEdgeToEdge
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
@@ -67,10 +69,11 @@ class MainActivity : AppCompatActivity() {
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window.decorView.filterTouchesWhenObscured = true
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = intent))
|
||||
}
|
||||
@@ -114,8 +117,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = intent))
|
||||
val newIntent = intent.validate()
|
||||
super.onNewIntent(newIntent)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
val newIntent = intent.validate()
|
||||
super.onNewIntent(newIntent, caller)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
@@ -22,13 +23,12 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.util.getCreateCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getGetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getProviderGetPasswordRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -68,7 +68,6 @@ private const val ANIMATION_REFRESH_DELAY = 500L
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
@@ -85,9 +84,6 @@ class MainViewModel @Inject constructor(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
|
||||
key = FlagKey.MobileErrorReporting,
|
||||
),
|
||||
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
|
||||
),
|
||||
) {
|
||||
@@ -106,12 +102,6 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
|
||||
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
@@ -217,17 +207,6 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
|
||||
is MainAction.Internal.OnMobileErrorReportingReceive -> {
|
||||
handleOnMobileErrorReportingReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnMobileErrorReportingReceive(
|
||||
action: MainAction.Internal.OnMobileErrorReportingReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +304,7 @@ class MainViewModel @Inject constructor(
|
||||
val createCredentialRequest = intent.getCreateCredentialRequestOrNull()
|
||||
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
@@ -415,6 +395,19 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
providerGetPasswordRequest != null -> {
|
||||
// Set the user's verification status when a new GetPassword request is
|
||||
// received to force explicit verification if the user's vault is
|
||||
// unlocked when the request is received.
|
||||
bitwardenCredentialManager.isUserVerified =
|
||||
providerGetPasswordRequest.isUserPreVerified
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderGetPasswordRequest(
|
||||
passwordGetRequest = providerGetPasswordRequest,
|
||||
)
|
||||
}
|
||||
|
||||
getCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderGetCredentials(
|
||||
@@ -460,7 +453,8 @@ class MainViewModel @Inject constructor(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: R.string.there_was_an_issue_validating_the_registration_token
|
||||
?: BitwardenString
|
||||
.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
@@ -495,15 +489,12 @@ data class MainState(
|
||||
val theme: AppTheme,
|
||||
val isScreenCaptureAllowed: Boolean,
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
private val isErrorReportingDialogEnabled: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Contains all feature flags that are available to the UI.
|
||||
*/
|
||||
val featureFlagsState: FeatureFlagsState
|
||||
get() = FeatureFlagsState(
|
||||
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
|
||||
)
|
||||
get() = FeatureFlagsState
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -548,13 +539,6 @@ sealed class MainAction {
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the Mobile Error Reporting feature flag has been updated.
|
||||
*/
|
||||
data class OnMobileErrorReportingReceive(
|
||||
val isErrorReportingEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
|
||||
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
|
||||
@@ -49,14 +49,14 @@ class AuthRequestNotificationManagerImpl(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
.setName(context.getString(R.string.pending_log_in_requests))
|
||||
.setName(context.getString(BitwardenString.pending_log_in_requests))
|
||||
.build(),
|
||||
)
|
||||
if (!notificationManager.areNotificationsEnabled(NOTIFICATION_CHANNEL_ID)) return
|
||||
// Create the notification
|
||||
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(createContentIntent(data))
|
||||
.setContentTitle(context.getString(R.string.log_in_requested))
|
||||
.setContentTitle(context.getString(BitwardenString.log_in_requested))
|
||||
.setContentText(
|
||||
authDiskSource
|
||||
.userState
|
||||
@@ -64,8 +64,8 @@ class AuthRequestNotificationManagerImpl(
|
||||
?.get(data.userId)
|
||||
?.profile
|
||||
?.email
|
||||
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(R.string.confirm_log_in),
|
||||
?.let { context.getString(BitwardenString.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(BitwardenString.confirm_log_in),
|
||||
)
|
||||
.setSmallIcon(BitwardenDrawable.ic_notification)
|
||||
.setColor(Color.White.value.toInt())
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.StringRes
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
@@ -48,7 +48,7 @@ class UserLogoutManagerImpl(
|
||||
Timber.d("logout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
showToast(message = R.string.login_expired)
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
}
|
||||
|
||||
val ableToSwitchToNewAccount = switchUserIfAvailable(
|
||||
@@ -70,7 +70,7 @@ class UserLogoutManagerImpl(
|
||||
Timber.d("softLogout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
showToast(message = R.string.login_expired)
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
}
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
@@ -80,6 +80,7 @@ class UserLogoutManagerImpl(
|
||||
// Save any data that will still need to be retained after otherwise clearing all dat
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
|
||||
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
@@ -101,6 +102,10 @@ class UserLogoutManagerImpl(
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
authDiskSource.storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearData(userId: String) {
|
||||
@@ -135,7 +140,7 @@ class UserLogoutManagerImpl(
|
||||
// Check if there is a new active user
|
||||
return if (updatedAccounts.isNotEmpty()) {
|
||||
if (currentUserId == currentUserState.activeUserId && !isExpired) {
|
||||
showToast(message = R.string.account_switched_automatically)
|
||||
showToast(message = BitwardenString.account_switched_automatically)
|
||||
}
|
||||
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
|
||||
|
||||
private const val NOTIFICATION_DATA: String = "notificationData"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
@@ -113,7 +113,7 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
}
|
||||
?: run {
|
||||
toastManager.show(
|
||||
messageId = R.string.autofill_tile_uri_not_found,
|
||||
messageId = BitwardenString.autofill_tile_uri_not_found,
|
||||
duration = Toast.LENGTH_LONG,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
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
|
||||
@@ -59,12 +58,8 @@ object AutofillModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesBrowserAutofillEnabledManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -24,17 +24,18 @@ class AutofillTotpManagerImpl(
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
cipherView.login?.totp ?: return
|
||||
val cipherId = cipherView.id ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = clock.instant(),
|
||||
totpCode = totpCode,
|
||||
cipherId = cipherId,
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(
|
||||
text = totpResult.code,
|
||||
toastDescriptorOverride = R.string.verification_code_totp.asText(),
|
||||
toastDescriptorOverride = BitwardenString.verification_code_totp.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,14 @@ package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [BrowserThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class BrowserThirdPartyAutofillEnabledManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : BrowserThirdPartyAutofillEnabledManager {
|
||||
class BrowserThirdPartyAutofillEnabledManagerImpl : BrowserThirdPartyAutofillEnabledManager {
|
||||
override var browserThirdPartyAutofillStatus: BrowserThirdPartyAutofillStatus = DEFAULT_STATUS
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -29,15 +24,6 @@ class BrowserThirdPartyAutofillEnabledManagerImpl(
|
||||
|
||||
override val browserThirdPartyAutofillStatusFlow: Flow<BrowserThirdPartyAutofillStatus>
|
||||
get() = mutableBrowserThirdPartyAutofillStatusStateFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
|
||||
) { data, enabled ->
|
||||
if (enabled) {
|
||||
data
|
||||
} else {
|
||||
DEFAULT_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = BrowserThirdPartyAutofillStatus(
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.autofill.provider
|
||||
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
|
||||
@@ -49,31 +52,35 @@ class AutofillCipherProviderImpl(
|
||||
}
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
|
||||
return cipherViews
|
||||
.mapNotNull { cipherView ->
|
||||
cipherView
|
||||
return cipherListViews
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView
|
||||
// We only care about non-deleted card ciphers.
|
||||
.takeIf {
|
||||
// Must be card type.
|
||||
cipherView.type == CipherType.CARD &&
|
||||
it.type is CipherListViewType.Card &&
|
||||
// Must not be deleted.
|
||||
cipherView.deletedDate == null &&
|
||||
it.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = nonNullCipherView.name,
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
|
||||
code = nonNullCipherView.card?.code.orEmpty(),
|
||||
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
|
||||
number = nonNullCipherView.card?.number.orEmpty(),
|
||||
)
|
||||
?.let { nonNullCipherListView ->
|
||||
nonNullCipherListView.id?.let { cipherId ->
|
||||
decryptCipherOrNull(cipherId = cipherId)?.let { cipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = cipherView.name,
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
cardholderName = cipherView.card?.cardholderName.orEmpty(),
|
||||
code = cipherView.card?.code.orEmpty(),
|
||||
expirationMonth = cipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = cipherView.card?.expYear.orEmpty(),
|
||||
number = cipherView.card?.number.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,12 +88,12 @@ class AutofillCipherProviderImpl(
|
||||
override suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login> {
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
val cipherViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
// We only care about non-deleted login ciphers.
|
||||
val loginCiphers = cipherViews
|
||||
.filter {
|
||||
// Must be login type
|
||||
it.type == CipherType.LOGIN &&
|
||||
it.type is CipherListViewType.Login &&
|
||||
// Must not be deleted.
|
||||
it.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
@@ -96,9 +103,12 @@ class AutofillCipherProviderImpl(
|
||||
return cipherMatchingManager
|
||||
// Filter for ciphers that match the uri in some way.
|
||||
.filterCiphersForMatches(
|
||||
ciphers = loginCiphers,
|
||||
cipherListViews = loginCiphers,
|
||||
matchUri = uri,
|
||||
)
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView.id?.let { decryptCipherOrNull(cipherId = it) }
|
||||
}
|
||||
.map { cipherView ->
|
||||
AutofillCipher.Login(
|
||||
cipherId = cipherView.id,
|
||||
@@ -114,10 +124,24 @@ class AutofillCipherProviderImpl(
|
||||
/**
|
||||
* Get available [CipherView]s if possible.
|
||||
*/
|
||||
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
|
||||
private suspend fun getUnlockedCipherListViewsOrNull(): List<CipherListView>? =
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.decryptCipherListResultStateFlow
|
||||
.takeUnless { isVaultLocked() }
|
||||
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
|
||||
?.data
|
||||
?.successes
|
||||
|
||||
private suspend fun decryptCipherOrNull(cipherId: String): CipherView? =
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found for autofill.")
|
||||
null
|
||||
}
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(result.error, "Failed to decrypt cipher for autofill.")
|
||||
null
|
||||
}
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.bitwarden.vault.CardListView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CopyableCipherFields
|
||||
import com.bitwarden.vault.LoginListView
|
||||
|
||||
/**
|
||||
* Returns true when the cipher is not deleted and contains at least one FIDO 2 credential.
|
||||
*/
|
||||
val CipherListView.isActiveWithFido2Credentials: Boolean
|
||||
get() = deletedDate == null && login?.hasFido2 ?: false
|
||||
|
||||
/**
|
||||
* Returns true when the cipher type is not deleted and contains a copyable password.
|
||||
*/
|
||||
val CipherListView.isActiveWithCopyablePassword: Boolean
|
||||
get() = deletedDate == null && copyableFields.contains(CopyableCipherFields.LOGIN_PASSWORD)
|
||||
|
||||
/**
|
||||
* Returns the [LoginListView] if the cipher is of type [CipherListViewType.Login], otherwise null.
|
||||
*/
|
||||
val CipherListView.login: LoginListView?
|
||||
get() = (this.type as? CipherListViewType.Login)?.v1
|
||||
|
||||
/**
|
||||
* Returns the [CardListView] if the cipher is of type [CipherListViewType.Card], otherwise null.
|
||||
*/
|
||||
val CipherListView.card: CardListView?
|
||||
get() = (this.type as? CipherListViewType.Card)?.v1
|
||||
@@ -50,3 +50,9 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
*/
|
||||
val CipherView.isActiveWithFido2Credentials: Boolean
|
||||
get() = deletedDate == null && !(login?.fido2Credentials.isNullOrEmpty())
|
||||
|
||||
/**
|
||||
* Returns true when the cipher is not deleted and contains at least one Pasword credential.
|
||||
*/
|
||||
val CipherView.isActiveWithPasswordCredentials: Boolean
|
||||
get() = deletedDate == null && !(login?.password.isNullOrEmpty())
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.credentials.builder
|
||||
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PasswordCredentialEntry
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
|
||||
/**
|
||||
* Builder for credential entries.
|
||||
@@ -18,4 +21,14 @@ interface CredentialEntryBuilder {
|
||||
beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PublicKeyCredentialEntry>
|
||||
|
||||
/**
|
||||
* Build password credential entries from the given cipher views and options.
|
||||
*/
|
||||
fun buildPasswordCredentialEntries(
|
||||
userId: String,
|
||||
cipherListViews: List<CipherListView>,
|
||||
beginGetPasswordCredentialOptions: List<BeginGetPasswordOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PasswordCredentialEntry>
|
||||
}
|
||||
|
||||
@@ -3,12 +3,17 @@ package com.x8bit.bitwarden.data.credentials.builder
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PasswordCredentialEntry
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSWORD_INTENT
|
||||
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
@@ -41,6 +46,21 @@ class CredentialEntryBuilderImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildPasswordCredentialEntries(
|
||||
userId: String,
|
||||
cipherListViews: List<CipherListView>,
|
||||
beginGetPasswordCredentialOptions: List<BeginGetPasswordOption>,
|
||||
isUserVerified: Boolean,
|
||||
): List<PasswordCredentialEntry> = beginGetPasswordCredentialOptions
|
||||
.flatMap { option ->
|
||||
cipherListViews
|
||||
.toPasswordCredentialEntryList(
|
||||
userId = userId,
|
||||
option = option,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
|
||||
userId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
@@ -51,7 +71,7 @@ class CredentialEntryBuilderImpl(
|
||||
.Builder(
|
||||
context = context,
|
||||
username = fido2AutofillView.userNameForUi
|
||||
?: context.getString(R.string.no_username),
|
||||
?: context.getString(BitwardenString.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
@@ -81,16 +101,52 @@ class CredentialEntryBuilderImpl(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<CipherListView>.toPasswordCredentialEntryList(
|
||||
userId: String,
|
||||
option: BeginGetPasswordOption,
|
||||
isUserVerified: Boolean,
|
||||
): List<PasswordCredentialEntry> = this
|
||||
.map { cipherView ->
|
||||
PasswordCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = cipherView.login?.username
|
||||
?: context.getString(BitwardenString.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createPasswordGetCredentialPendingIntent(
|
||||
action = GET_PASSWORD_INTENT,
|
||||
userId = userId,
|
||||
cipherId = cipherView.id,
|
||||
isUserVerified = isUserVerified,
|
||||
requestCode = Random.nextInt(),
|
||||
),
|
||||
beginGetPasswordOption = option,
|
||||
)
|
||||
.setDisplayName(cipherView.name)
|
||||
.setAutoSelectAllowed(this.size == 1)
|
||||
.setIcon(getCredentialEntryIcon())
|
||||
.apply {
|
||||
if (!isUserVerified) {
|
||||
setBiometricPromptDataIfSupported(
|
||||
cipher = biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
// TODO: [PM-20176] Enable web icons in credential entries
|
||||
// Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
|
||||
// are addressed. See https://issuetracker.google.com/issues/355141766 for details.
|
||||
private fun getCredentialEntryIcon(isPasskey: Boolean): Icon = IconCompat
|
||||
private fun getCredentialEntryIcon(
|
||||
isPasskey: Boolean = false,
|
||||
): Icon = IconCompat
|
||||
.createWithResource(
|
||||
context,
|
||||
if (isPasskey) {
|
||||
BitwardenDrawable.ic_bw_passkey
|
||||
} else {
|
||||
BitwardenDrawable.ic_globe
|
||||
when {
|
||||
isPasskey -> BitwardenDrawable.ic_bw_passkey
|
||||
else -> BitwardenDrawable.ic_globe
|
||||
},
|
||||
)
|
||||
.toIcon(context)
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryIm
|
||||
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
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@@ -75,6 +76,7 @@ object CredentialProviderModule {
|
||||
vaultRepository: VaultRepository,
|
||||
dispatcherManager: DispatcherManager,
|
||||
credentialEntryBuilder: CredentialEntryBuilder,
|
||||
cipherMatchingManager: CipherMatchingManager,
|
||||
): BitwardenCredentialManager =
|
||||
BitwardenCredentialManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
@@ -83,6 +85,7 @@ object CredentialProviderModule {
|
||||
vaultRepository = vaultRepository,
|
||||
dispatcherManager = dispatcherManager,
|
||||
credentialEntryBuilder = credentialEntryBuilder,
|
||||
cipherMatchingManager = cipherMatchingManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.credentials.manager
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
@@ -18,8 +19,12 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
|
||||
import com.bitwarden.ui.platform.base.util.toAndroidAppUriString
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithCopyablePassword
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
|
||||
@@ -27,6 +32,7 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
@@ -35,8 +41,8 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.fold
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -53,6 +59,7 @@ class BitwardenCredentialManagerImpl(
|
||||
private val credentialEntryBuilder: CredentialEntryBuilder,
|
||||
private val json: Json,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val cipherMatchingManager: CipherMatchingManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : BitwardenCredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@@ -168,30 +175,48 @@ class BitwardenCredentialManagerImpl(
|
||||
override suspend fun getCredentialEntries(
|
||||
getCredentialsRequest: GetCredentialsRequest,
|
||||
): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
|
||||
val cipherViews = vaultRepository
|
||||
.ciphersStateFlow
|
||||
val cipherListViews = vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.takeUntilLoaded()
|
||||
.fold(initial = emptyList<CipherView>()) { _, dataState ->
|
||||
.fold(initial = emptyList<CipherListView>()) { _, dataState ->
|
||||
when (dataState) {
|
||||
is DataState.Loaded -> {
|
||||
dataState.data
|
||||
}
|
||||
|
||||
is DataState.Loaded -> dataState.data.successes
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
.ifEmpty {
|
||||
return@withContext emptyList<CredentialEntry>().asSuccess()
|
||||
}
|
||||
.filter { it.isActiveWithFido2Credentials || it.isActiveWithCopyablePassword }
|
||||
.ifEmpty { return@withContext emptyList<CredentialEntry>().asSuccess() }
|
||||
|
||||
getCredentialsRequest
|
||||
val passwordCredentialResult = getCredentialsRequest
|
||||
.callingAppInfo
|
||||
?.packageName
|
||||
?.let { packageName ->
|
||||
getCredentialsRequest
|
||||
.beginGetPasswordOptions
|
||||
.toPasswordCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherListViews = cipherMatchingManager.filterCiphersForMatches(
|
||||
cipherListViews = cipherListViews,
|
||||
matchUri = packageName.toAndroidAppUriString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
val passkeyCredentialResult = getCredentialsRequest
|
||||
.beginGetPublicKeyCredentialOptions
|
||||
.toPublicKeyCredentialEntries(
|
||||
userId = getCredentialsRequest.userId,
|
||||
cipherViewsWithPublicKeyCredentials = cipherViews,
|
||||
cipherListViews = cipherListViews
|
||||
.filter { it.isActiveWithFido2Credentials },
|
||||
)
|
||||
.onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
|
||||
|
||||
if (passkeyCredentialResult.isFailure && passwordCredentialResult.isNotEmpty()) {
|
||||
Result.success(passwordCredentialResult)
|
||||
} else {
|
||||
passkeyCredentialResult.map { it + passwordCredentialResult }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPasskeyAssertionOptionsOrNull(
|
||||
@@ -200,8 +225,10 @@ class BitwardenCredentialManagerImpl(
|
||||
|
||||
private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
|
||||
userId: String,
|
||||
cipherViewsWithPublicKeyCredentials: List<CipherView>,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): Result<List<CredentialEntry>> {
|
||||
if (this.isEmpty()) return emptyList<CredentialEntry>().asSuccess()
|
||||
|
||||
val relyingPartyIds = this
|
||||
.mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
|
||||
.distinct()
|
||||
@@ -209,27 +236,54 @@ class BitwardenCredentialManagerImpl(
|
||||
return GetCredentialUnknownException("Relying party id required.").asFailure()
|
||||
}
|
||||
|
||||
val decryptResult = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViewsWithPublicKeyCredentials)
|
||||
|
||||
return when (decryptResult) {
|
||||
is DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
val cipherViews = cipherListViews
|
||||
.filter { cipherListView ->
|
||||
cipherListView.login
|
||||
?.fido2Credentials
|
||||
.orEmpty()
|
||||
.any { credential -> credential.rpId in relyingPartyIds }
|
||||
}
|
||||
.mapNotNull { cipherListView ->
|
||||
when (val result = vaultRepository.getCipher(cipherListView.id.orEmpty())) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found while building public key credential entries.")
|
||||
null
|
||||
}
|
||||
|
||||
is DecryptFido2CredentialAutofillViewResult.Success -> {
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = decryptResult
|
||||
.fido2CredentialAutofillViews
|
||||
.filter { it.rpId in relyingPartyIds },
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.asSuccess()
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(
|
||||
result.error,
|
||||
"Failed to decrypt cipher while building credential entries.",
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
.ifEmpty { return emptyList<CredentialEntry>().asSuccess() }
|
||||
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
userId = userId,
|
||||
cipherViews = cipherViews,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { fido2AutofillViews ->
|
||||
credentialEntryBuilder
|
||||
.buildPublicKeyCredentialEntries(
|
||||
userId = userId,
|
||||
fido2CredentialAutofillViews = fido2AutofillViews,
|
||||
beginGetPublicKeyCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
.asSuccess()
|
||||
},
|
||||
onFailure = {
|
||||
GetCredentialUnknownException("Error decrypting credentials.").asFailure()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun registerFido2CredentialForUnprivilegedApp(
|
||||
@@ -321,6 +375,21 @@ class BitwardenCredentialManagerImpl(
|
||||
},
|
||||
)
|
||||
|
||||
private fun List<BeginGetPasswordOption>.toPasswordCredentialEntries(
|
||||
userId: String,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): List<CredentialEntry> {
|
||||
if (this.isEmpty()) return emptyList()
|
||||
|
||||
return credentialEntryBuilder
|
||||
.buildPasswordCredentialEntries(
|
||||
userId = userId,
|
||||
cipherListViews = cipherListViews,
|
||||
beginGetPasswordCredentialOptions = this,
|
||||
isUserVerified = isUserVerified,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPasswordOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
@@ -10,7 +11,7 @@ import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 request to retrieve FIDO credentials parsed from the launching intent.
|
||||
* Models a [CredentialManager] request to retrieve credentials parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the user's vault to search.
|
||||
* @param requestData Provider request data in the form of a [Bundle].
|
||||
|
||||
@@ -2,22 +2,51 @@ package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CredentialOption
|
||||
import androidx.credentials.GetPasswordOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* A wrapper around [ProviderGetCredentialRequest] that includes additional information needed to
|
||||
* fulfill the request.
|
||||
* Models a Password credential authentication request parsed from the launching intent.
|
||||
*
|
||||
* @param userId The ID of the user that owns the credential being requested.
|
||||
* @param cipherId The ID of the cipher containing the password to be retrieved.
|
||||
* @param isUserVerified Whether the user has been verified prior to this request.
|
||||
* @param requestData The original request data from the system.
|
||||
* @property userId ID of the user requesting credential authentication.
|
||||
* @property cipherId ID of the cipher to be authenticated against.
|
||||
* @property isUserPreVerified Whether the user has already been verified by the OS biometric
|
||||
* prompt.
|
||||
* @property requestData Provider request data in the form of a [Bundle].
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProviderGetPasswordCredentialRequest(
|
||||
val userId: String,
|
||||
val cipherId: String,
|
||||
val isUserVerified: Boolean,
|
||||
val requestData: Bundle,
|
||||
) : Parcelable
|
||||
val isUserPreVerified: Boolean,
|
||||
private val requestData: Bundle,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* The [ProviderGetCredentialRequest] from the [requestData].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val providerRequest: ProviderGetCredentialRequest by lazy {
|
||||
ProviderGetCredentialRequest.fromBundle(requestData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CallingAppInfo] from the [providerRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val callingAppInfo: CallingAppInfo by lazy { providerRequest.callingAppInfo }
|
||||
|
||||
/**
|
||||
* The [CredentialOption] from the [providerRequest], or null if one is not found
|
||||
* in the request options list.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val option: GetPasswordOption? by lazy {
|
||||
providerRequest.credentialOptions
|
||||
.firstNotNullOfOrNull { it as? GetPasswordOption }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
@@ -44,6 +44,7 @@ import javax.crypto.Cipher
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY"
|
||||
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY"
|
||||
const val GET_PASSWORD_INTENT = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD"
|
||||
const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT"
|
||||
|
||||
/**
|
||||
@@ -105,7 +106,7 @@ class CredentialProviderProcessorImpl(
|
||||
// Return an unlock action if the current account is locked.
|
||||
if (!userState.activeAccount.isVaultUnlocked) {
|
||||
val authenticationAction = AuthenticationAction(
|
||||
title = context.getString(R.string.unlock),
|
||||
title = context.getString(BitwardenString.unlock),
|
||||
pendingIntent = intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
userId = userState.activeUserId,
|
||||
@@ -192,7 +193,7 @@ class CredentialProviderProcessorImpl(
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
BitwardenString.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package com.x8bit.bitwarden.data.credentials.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.credentials.provider.PasswordCredentialEntry
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
@@ -25,3 +26,17 @@ fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the biometric prompt data on the [PasswordCredentialEntry.Builder] if supported.
|
||||
*/
|
||||
fun PasswordCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher?,
|
||||
): PasswordCredentialEntry.Builder =
|
||||
if (isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) && cipher != null) {
|
||||
setBiometricPromptData(
|
||||
biometricPromptData = buildPromptDataWithCipher(cipher),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
@@ -79,6 +80,38 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [ProviderGetPasswordCredentialRequest] related to an
|
||||
* ongoing password credential GetPassword process.
|
||||
*/
|
||||
fun Intent.getProviderGetPasswordRequestOrNull(): ProviderGetPasswordCredentialRequest? {
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveProviderGetCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
|
||||
?: return null
|
||||
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
// Extract the OS biometric prompt result from the request data because it is not included in
|
||||
// the bundle returned by `ProviderGetCredentialRequest.asBundle()`.
|
||||
val isUserPreVerified = systemRequest
|
||||
.biometricPromptResult
|
||||
?.isSuccessful
|
||||
?: getBooleanExtra(EXTRA_KEY_UV_PERFORMED_DURING_UNLOCK, false)
|
||||
|
||||
return ProviderGetPasswordCredentialRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
isUserPreVerified = isUserPreVerified,
|
||||
requestData = ProviderGetCredentialRequest.asBundle(systemRequest),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [GetCredentialsRequest] related to an ongoing
|
||||
* [CredentialManager] credential lookup process.
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.KeyStore
|
||||
@@ -45,9 +46,11 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
val cipher = try {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (nsae: NoSuchAlgorithmException) {
|
||||
Timber.w(nsae, "createCipherOrNull failed to get cipher instance")
|
||||
return null
|
||||
} catch (_: NoSuchPaddingException) {
|
||||
} catch (nspe: NoSuchPaddingException) {
|
||||
Timber.w(nspe, "createCipherOrNull failed to get cipher instance")
|
||||
return null
|
||||
}
|
||||
// Instantiate integrity values.
|
||||
@@ -124,20 +127,25 @@ class BiometricsEncryptionManagerImpl(
|
||||
private fun generateKeyOrNull(userId: String): SecretKey? {
|
||||
val keyGen = try {
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME)
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (nsae: NoSuchAlgorithmException) {
|
||||
Timber.w(nsae, "generateKeyOrNull failed to get key generator instance")
|
||||
return null
|
||||
} catch (_: NoSuchProviderException) {
|
||||
} catch (nspe: NoSuchProviderException) {
|
||||
Timber.w(nspe, "generateKeyOrNull failed to get key generator instance")
|
||||
return null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (iae: IllegalArgumentException) {
|
||||
Timber.w(iae, "generateKeyOrNull failed to get key generator instance")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
keyGen.init(getKeyGenParameterSpec(userId = userId))
|
||||
keyGen.generateKey()
|
||||
} catch (_: InvalidAlgorithmParameterException) {
|
||||
} catch (iape: InvalidAlgorithmParameterException) {
|
||||
Timber.w(iape, "generateKeyOrNull failed to initialize and generate key")
|
||||
null
|
||||
} catch (_: ProviderException) {
|
||||
} catch (pe: ProviderException) {
|
||||
Timber.w(pe, "generateKeyOrNull failed to initialize and generate key")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -150,14 +158,17 @@ class BiometricsEncryptionManagerImpl(
|
||||
keystore
|
||||
.getKey(encryptionKeyName(userId = userId), null)
|
||||
?.let { it as SecretKey }
|
||||
} catch (_: KeyStoreException) {
|
||||
} catch (kse: KeyStoreException) {
|
||||
// keystore was not loaded
|
||||
Timber.w(kse, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (nsae: NoSuchAlgorithmException) {
|
||||
// keystore algorithm cannot be found
|
||||
Timber.w(nsae, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
} catch (uke: UnrecoverableKeyException) {
|
||||
// key could not be recovered
|
||||
Timber.w(uke, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
}
|
||||
|
||||
@@ -174,16 +185,19 @@ class BiometricsEncryptionManagerImpl(
|
||||
?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) }
|
||||
?: init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
true
|
||||
} catch (_: KeyPermanentlyInvalidatedException) {
|
||||
} catch (kpie: KeyPermanentlyInvalidatedException) {
|
||||
// Biometric has changed
|
||||
Timber.w(kpie, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
false
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
} catch (uke: UnrecoverableKeyException) {
|
||||
// Biometric was disabled and re-enabled
|
||||
Timber.w(uke, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
false
|
||||
} catch (_: InvalidKeyException) {
|
||||
} catch (ike: InvalidKeyException) {
|
||||
// User has no key
|
||||
Timber.w(ike, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
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
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -31,7 +30,6 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
) : FirstTimeActionManager {
|
||||
|
||||
@@ -101,16 +99,9 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
combine(
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = it),
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
|
||||
) { showImportLogins, importLoginsEnabled ->
|
||||
val shouldShowImportLoginsSettings = showImportLogins && importLoginsEnabled
|
||||
listOf(shouldShowImportLoginsSettings)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showImportLogins -> showImportLogins }
|
||||
}
|
||||
getShowImportLoginsSettingBadgeFlowInternal(userId = it)
|
||||
.map { showImportLogins -> listOf(showImportLogins) }
|
||||
.map { list -> list.count { showImportLogins -> showImportLogins } }
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
|
||||
/**
|
||||
* A manager for matching ciphers based on special criteria.
|
||||
*/
|
||||
interface CipherMatchingManager {
|
||||
/**
|
||||
* Filter [ciphers] for entries that match the [matchUri] in some fashion.
|
||||
* Filter [cipherListViews] for entries that match the [matchUri] in some fashion.
|
||||
*/
|
||||
suspend fun filterCiphersForMatches(
|
||||
ciphers: List<CipherView>,
|
||||
cipherListViews: List<CipherListView>,
|
||||
matchUri: String,
|
||||
): List<CipherView>
|
||||
): List<CipherListView>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.LoginUriView
|
||||
import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
@@ -33,9 +34,9 @@ class CipherMatchingManagerImpl(
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : CipherMatchingManager {
|
||||
override suspend fun filterCiphersForMatches(
|
||||
ciphers: List<CipherView>,
|
||||
cipherListViews: List<CipherListView>,
|
||||
matchUri: String,
|
||||
): List<CipherView> {
|
||||
): List<CipherListView> {
|
||||
val equivalentDomainsData = vaultRepository
|
||||
.domainsStateFlow
|
||||
.mapNotNull { it.data }
|
||||
@@ -58,14 +59,14 @@ class CipherMatchingManagerImpl(
|
||||
matchUri = matchUri,
|
||||
)
|
||||
|
||||
val exactMatchingCiphers = mutableListOf<CipherView>()
|
||||
val fuzzyMatchingCiphers = mutableListOf<CipherView>()
|
||||
val exactMatchingCiphers = mutableListOf<CipherListView>()
|
||||
val fuzzyMatchingCiphers = mutableListOf<CipherListView>()
|
||||
|
||||
ciphers
|
||||
.forEach { cipherView ->
|
||||
cipherListViews
|
||||
.forEach { cipherListView ->
|
||||
val matchResult = checkForCipherMatch(
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
cipherView = cipherView,
|
||||
cipherListView = cipherListView,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
isAndroidApp = isAndroidApp,
|
||||
matchUri = matchUri,
|
||||
@@ -73,8 +74,8 @@ class CipherMatchingManagerImpl(
|
||||
)
|
||||
|
||||
when (matchResult) {
|
||||
MatchResult.EXACT -> exactMatchingCiphers.add(cipherView)
|
||||
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherView)
|
||||
MatchResult.EXACT -> exactMatchingCiphers.add(cipherListView)
|
||||
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherListView)
|
||||
MatchResult.NONE -> Unit
|
||||
}
|
||||
}
|
||||
@@ -135,10 +136,10 @@ private fun getMatchingDomains(
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if [cipherView] matches [matchUri] in some way. The returned [MatchResult] will
|
||||
* Check to see if [cipherListView] matches [matchUri] in some way. The returned [MatchResult] will
|
||||
* provide details on the match quality.
|
||||
*
|
||||
* @param cipherView The cipher to be judged for a match.
|
||||
* @param cipherListView The cipher to be judged for a match.
|
||||
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
|
||||
* @param defaultUriMatchType The global default [UriMatchType].
|
||||
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||
@@ -148,13 +149,13 @@ private fun getMatchingDomains(
|
||||
@Suppress("LongParameterList")
|
||||
private fun checkForCipherMatch(
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
cipherView: CipherView,
|
||||
cipherListView: CipherListView,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
isAndroidApp: Boolean,
|
||||
matchingDomains: MatchingDomains,
|
||||
matchUri: String,
|
||||
): MatchResult {
|
||||
val matchResults = cipherView
|
||||
val matchResults = cipherListView
|
||||
.login
|
||||
?.uris
|
||||
?.map { loginUriView ->
|
||||
|
||||
@@ -15,8 +15,8 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -54,10 +54,10 @@ class BitwardenClipboardManagerImpl(
|
||||
)
|
||||
if (!isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
val descriptor = toastDescriptorOverride
|
||||
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
|
||||
?.let { context.resources.getString(BitwardenString.value_has_been_copied, it) }
|
||||
?: context.resources.getString(
|
||||
R.string.value_has_been_copied,
|
||||
context.resources.getString(R.string.value),
|
||||
BitwardenString.value_has_been_copied,
|
||||
context.resources.getString(BitwardenString.value),
|
||||
)
|
||||
toastManager.show(message = descriptor)
|
||||
}
|
||||
|
||||
@@ -353,14 +353,12 @@ object PlatformManagerModule {
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
): FirstTimeActionManager = FirstTimeActionManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
)
|
||||
|
||||
|
||||
@@ -22,16 +22,10 @@ sealed class FlagKey<out T : Any> {
|
||||
val activeFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
EmailVerification,
|
||||
ImportLoginsFlow,
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
MutualTls,
|
||||
SingleTapPasskeyCreation,
|
||||
SingleTapPasskeyAuthentication,
|
||||
AnonAddySelfHostAlias,
|
||||
SimpleLoginSelfHostAlias,
|
||||
ChromeAutofill,
|
||||
MobileErrorReporting,
|
||||
RestrictCipherItemDeletion,
|
||||
UserManagedPrivilegedApps,
|
||||
RemoveCardPolicy,
|
||||
@@ -47,22 +41,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object MobileErrorReporting : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mobile-error-reporting"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the import logins feature.
|
||||
*/
|
||||
data object ImportLoginsFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "import-logins-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
|
||||
* feature.
|
||||
@@ -89,14 +67,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Mutual TLS feature.
|
||||
*/
|
||||
data object MutualTls : FlagKey<Boolean>() {
|
||||
override val keyName: String = "mutual-tls"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable single tap passkey creation.
|
||||
*/
|
||||
@@ -113,32 +83,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable AnonAddy (addy.io) self host alias
|
||||
* generation.
|
||||
*/
|
||||
data object AnonAddySelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "anon-addy-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable SimpleLogin self-host alias generation.
|
||||
*/
|
||||
data object SimpleLoginSelfHostAlias : FlagKey<Boolean>() {
|
||||
override val keyName: String = "simple-login-self-host-alias"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the checking for Chrome's third party
|
||||
* autofill.
|
||||
*/
|
||||
data object ChromeAutofill : FlagKey<Boolean>() {
|
||||
override val keyName: String = "android-chrome-autofill"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the restriction of cipher item deletion
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -70,7 +71,7 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the [CredentialManager] framework in order to authenticate a
|
||||
* The app was launched via the [CredentialManager] framework in order to authenticate a FIDO 2
|
||||
* credential saved to the user's vault.
|
||||
*/
|
||||
@Parcelize
|
||||
@@ -78,6 +79,15 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
val fido2AssertionRequest: Fido2CredentialAssertionRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the [CredentialManager] framework in order to retrieve a Password
|
||||
* credential saved to the user's vault.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProviderGetPasswordRequest(
|
||||
val passwordGetRequest: ProviderGetPasswordCredentialRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the [CredentialManager] framework request to retrieve credentials
|
||||
* associated with the requesting entity.
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
@@ -44,6 +45,15 @@ fun SpecialCircumstance.toFido2AssertionRequestOrNull(): Fido2CredentialAssertio
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ProviderGetPasswordCredentialRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toPasswordGetRequestOrNull(): ProviderGetPasswordCredentialRequest? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.ProviderGetPasswordRequest -> this.passwordGetRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [GetCredentialsRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
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.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthenticatorBridgeRepository].
|
||||
*/
|
||||
class AuthenticatorBridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
get() {
|
||||
val doAnyAccountsHaveAuthenticatorSyncEnabled = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
val doAnyAccountsHaveAuthenticatorSyncEnabled = authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.any {
|
||||
?.keys
|
||||
?.any { userId ->
|
||||
// Authenticator sync is enabled if any accounts have an authenticator
|
||||
// sync key stored:
|
||||
authDiskSource.getAuthenticatorSyncUnlockKey(it.userId) != null
|
||||
authDiskSource.getAuthenticatorSyncUnlockKey(userId = userId) != null
|
||||
}
|
||||
?: false
|
||||
return if (doAnyAccountsHaveAuthenticatorSyncEnabled) {
|
||||
@@ -43,54 +47,43 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
return authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
.orEmpty()
|
||||
.mapNotNull { (userId, account) ->
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
// the user has not enabled authenticator sync and we skip the account.
|
||||
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
|
||||
?: return@mapNotNull null
|
||||
|
||||
// Wait for any unlocking actions to finish:
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
// Unlock vault if necessary:
|
||||
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
val unlockResult = vaultRepository
|
||||
.unlockVaultWithDecryptedUserKey(
|
||||
val vaultUnlockResult = unlockClient(
|
||||
userId = userId,
|
||||
account = account,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
when (vaultUnlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
// Destroy our stand-alone instance of the vault.
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
@@ -99,38 +92,73 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
// Filter out any deleted ciphers.
|
||||
.filter { it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
val decryptedCipher = vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
)
|
||||
scopedVaultSdkSource
|
||||
.decryptCipher(userId = userId, cipher = it.toEncryptedSdkCipher())
|
||||
.getOrNull()
|
||||
|
||||
val rawTotp = decryptedCipher?.login?.totp
|
||||
val cipherName = decryptedCipher?.name
|
||||
val username = decryptedCipher?.login?.username
|
||||
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
?.let { decryptedCipher ->
|
||||
val rawTotp = decryptedCipher.login?.totp
|
||||
val cipherName = decryptedCipher.name
|
||||
val username = decryptedCipher.login?.username
|
||||
rawTotp.sanitizeTotpUri(issuer = cipherName, username = username)
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(
|
||||
userId = userId,
|
||||
isUserInitiated = false,
|
||||
)
|
||||
}
|
||||
// Lock and destroy our stand-alone instance of the vault:
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
userId = userId,
|
||||
name = account.profile.name,
|
||||
email = account.profile.email,
|
||||
environmentLabel = account
|
||||
.settings
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
.let {
|
||||
SharedAccountData(it)
|
||||
.let(::SharedAccountData)
|
||||
}
|
||||
|
||||
private suspend fun unlockClient(
|
||||
userId: String,
|
||||
account: AccountJson,
|
||||
decryptedUserKey: String,
|
||||
): VaultUnlockResult {
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError(MissingPropertyException("Private key"))
|
||||
return scopedVaultSdkSource
|
||||
.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = userId,
|
||||
kdfParams = account.profile.toSdkParams(),
|
||||
email = account.profile.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
),
|
||||
signingKey = null,
|
||||
securityState = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
// Initialize the SDK for organizations if necessary
|
||||
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
|
||||
if (organizationKeys != null && result is InitializeCryptoResult.Success) {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} else {
|
||||
result.asSuccess()
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it.toVaultUnlockResult() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.security.GeneralSecurityException
|
||||
import java.time.Instant
|
||||
import javax.crypto.Cipher
|
||||
|
||||
@@ -501,9 +503,14 @@ class SettingsRepositoryImpl(
|
||||
.onSuccess { biometricsKey ->
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = userId,
|
||||
biometricsKey = cipher
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1),
|
||||
biometricsKey = try {
|
||||
cipher
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Timber.w(e, "setupBiometricsKey failed encrypt the biometric key")
|
||||
return BiometricsKeyResult.Error(error = e)
|
||||
},
|
||||
)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.view.autofill.AutofillManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
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.platform.datasource.disk.EnvironmentDiskSource
|
||||
@@ -21,8 +20,8 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -39,17 +38,13 @@ object PlatformRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthenticatorBridgeRepository(
|
||||
authRepository: AuthRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
|
||||
|
||||
/**
|
||||
@@ -10,12 +10,12 @@ import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequenc
|
||||
*/
|
||||
val ClearClipboardFrequency.displayLabel: Text
|
||||
get() = when (this) {
|
||||
ClearClipboardFrequency.NEVER -> R.string.never
|
||||
ClearClipboardFrequency.TEN_SECONDS -> R.string.ten_seconds
|
||||
ClearClipboardFrequency.TWENTY_SECONDS -> R.string.twenty_seconds
|
||||
ClearClipboardFrequency.THIRTY_SECONDS -> R.string.thirty_seconds
|
||||
ClearClipboardFrequency.ONE_MINUTE -> R.string.one_minute
|
||||
ClearClipboardFrequency.TWO_MINUTES -> R.string.two_minutes
|
||||
ClearClipboardFrequency.FIVE_MINUTES -> R.string.five_minutes
|
||||
ClearClipboardFrequency.NEVER -> BitwardenString.never
|
||||
ClearClipboardFrequency.TEN_SECONDS -> BitwardenString.ten_seconds
|
||||
ClearClipboardFrequency.TWENTY_SECONDS -> BitwardenString.twenty_seconds
|
||||
ClearClipboardFrequency.THIRTY_SECONDS -> BitwardenString.thirty_seconds
|
||||
ClearClipboardFrequency.ONE_MINUTE -> BitwardenString.one_minute
|
||||
ClearClipboardFrequency.TWO_MINUTES -> BitwardenString.two_minutes
|
||||
ClearClipboardFrequency.FIVE_MINUTES -> BitwardenString.five_minutes
|
||||
}
|
||||
.asText()
|
||||
|
||||
@@ -21,6 +21,11 @@ val isDevBuild: Boolean
|
||||
val versionData: String
|
||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
|
||||
/**
|
||||
* A string that represents a displayable SDK version.
|
||||
*/
|
||||
val sdkData: String get() = BuildConfig.SDK_VERSION
|
||||
|
||||
/**
|
||||
* A string that represents device data.
|
||||
*/
|
||||
|
||||
@@ -10,8 +10,8 @@ import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.AccessibilityActivity
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
|
||||
@@ -62,8 +62,8 @@ class BitwardenAutofillTileService : TileService() {
|
||||
|
||||
private fun getAccessibilityServiceRequiredDialog(): Dialog =
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.autofill_tile_accessibility_required)
|
||||
.setMessage(BitwardenString.autofill_tile_accessibility_required)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.okay) { dialog, _ -> dialog.cancel() }
|
||||
.setPositiveButton(BitwardenString.okay) { dialog, _ -> dialog.cancel() }
|
||||
.create()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
/**
|
||||
* This is a non-singleton instance of the [VaultSdkSource] that is intentionally separate; this
|
||||
* allows you to temporarily unlock vaults for a given user within its own scope without affecting
|
||||
* the foreground behavior of the app.
|
||||
*
|
||||
* Users of this class must always call [ScopedVaultSdkSource.clearCrypto] when they are done using
|
||||
* the unlocked vault in order to ensure that this instance of the vault is re-locked.
|
||||
*/
|
||||
interface ScopedVaultSdkSource : VaultSdkSource
|
||||
@@ -0,0 +1,32 @@
|
||||
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.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
|
||||
|
||||
/**
|
||||
* The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the
|
||||
* [SdkClientManagerImpl] to keep it separate from the rest of the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
sdkClientManager = SdkClientManagerImpl(
|
||||
// We do not want to have the real NativeLibraryManager used here to avoid
|
||||
// initializing the library twice.
|
||||
nativeLibraryManager = object : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> = Unit.asSuccess()
|
||||
},
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
featureFlagManager = featureFlagManager,
|
||||
),
|
||||
dispatcherManager = dispatcherManager,
|
||||
),
|
||||
) : ScopedVaultSdkSource, VaultSdkSource by vaultSdkSource
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
@@ -35,6 +34,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.DeriveKeyConnectorRes
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Source of vault information and functionality from the Bitwarden SDK.
|
||||
@@ -204,30 +204,6 @@ interface VaultSdkSource {
|
||||
cipher: Cipher,
|
||||
): Result<CipherView>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s for the user with the given [userId], returning a list of
|
||||
* [CipherListView] wrapped in a [Result].
|
||||
*
|
||||
* This should only be called after a successful call to [initializeCrypto] for the associated
|
||||
* user.
|
||||
*/
|
||||
suspend fun decryptCipherListCollection(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherListView>>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s for the user with the given [userId], returning a list of
|
||||
* [CipherView] wrapped in a [Result].
|
||||
*
|
||||
* This should only be called after a successful call to [initializeCrypto] for the associated
|
||||
* user.
|
||||
*/
|
||||
suspend fun decryptCipherList(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherView>>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s for the user with the given [userId].
|
||||
*
|
||||
@@ -397,12 +373,12 @@ interface VaultSdkSource {
|
||||
): Result<List<PasswordHistoryView>>
|
||||
|
||||
/**
|
||||
* Generate a verification code and the period using the totp code.
|
||||
* Generate a verification code for the given [cipherListView] and [time].
|
||||
*/
|
||||
suspend fun generateTotp(
|
||||
suspend fun generateTotpForCipherListView(
|
||||
userId: String,
|
||||
totp: String,
|
||||
time: DateTime,
|
||||
cipherListView: CipherListView,
|
||||
time: Instant?,
|
||||
): Result<TotpResponse>
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
@@ -48,6 +47,7 @@ import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary implementation of [VaultSdkSource] that serves as a convenience wrapper around a
|
||||
@@ -290,28 +290,6 @@ class VaultSdkSourceImpl(
|
||||
.decrypt(cipher = cipher)
|
||||
}
|
||||
|
||||
override suspend fun decryptCipherListCollection(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherListView>> =
|
||||
runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.vault()
|
||||
.ciphers()
|
||||
.decryptList(ciphers = cipherList)
|
||||
}
|
||||
|
||||
override suspend fun decryptCipherList(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherView>> =
|
||||
runCatchingWithLogs {
|
||||
val ciphers = getClient(userId = userId).vault().ciphers()
|
||||
withContext(context = dispatcherManager.default) {
|
||||
cipherList.map { async { ciphers.decrypt(cipher = it) } }.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decryptCipherListWithFailures(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
@@ -438,15 +416,15 @@ class VaultSdkSourceImpl(
|
||||
.decryptList(list = passwordHistoryList)
|
||||
}
|
||||
|
||||
override suspend fun generateTotp(
|
||||
override suspend fun generateTotpForCipherListView(
|
||||
userId: String,
|
||||
totp: String,
|
||||
time: DateTime,
|
||||
cipherListView: CipherListView,
|
||||
time: Instant?,
|
||||
): Result<TotpResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.vault()
|
||||
.generateTotp(
|
||||
key = totp,
|
||||
.generateTotpCipherView(
|
||||
view = cipherListView,
|
||||
time = time,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialStoreImpl
|
||||
@@ -32,15 +36,27 @@ object VaultSdkModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun providesScopedVaultSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
): ScopedVaultSdkSource =
|
||||
ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
sdkRepositoryFactory = sdkRepositoryFactory,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFido2CredentialStore(
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authRepository: AuthRepository,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
vaultRepository: VaultRepository,
|
||||
): Fido2CredentialStore = Fido2CredentialStoreImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authRepository = authRepository,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.EncryptionContext
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialStore].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class Fido2CredentialStoreImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : Fido2CredentialStore {
|
||||
|
||||
@@ -32,17 +35,13 @@ class Fido2CredentialStoreImpl(
|
||||
?.let { throw it }
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
val activeCipherIds = vaultRepository.ciphersStateFlow.value.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
?.map { it.id }
|
||||
?: emptyList()
|
||||
|
||||
return vaultRepository.ciphersListViewStateFlow.value.data
|
||||
?.filter { clv ->
|
||||
activeCipherIds
|
||||
.contains(clv.id)
|
||||
}
|
||||
?: emptyList()
|
||||
return vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
?.successes
|
||||
.orEmpty()
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,8 +52,6 @@ class Fido2CredentialStoreImpl(
|
||||
* @param ripId Relying Party ID to find.
|
||||
*/
|
||||
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> {
|
||||
val userId = getActiveUserIdOrThrow()
|
||||
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
if (syncResult is SyncVaultDataResult.Error) {
|
||||
syncResult.throwable
|
||||
@@ -62,30 +59,25 @@ class Fido2CredentialStoreImpl(
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
|
||||
val ciphersWithFido2Credentials = vaultRepository.ciphersStateFlow.value.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
return vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
?.successes
|
||||
.orEmpty()
|
||||
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
userId = userId,
|
||||
cipherViews = ciphersWithFido2Credentials.toTypedArray(),
|
||||
.filter { it.isActiveWithFido2Credentials }
|
||||
.filterMatchingCredentials(
|
||||
credentialIds = ids,
|
||||
relyingPartyId = ripId,
|
||||
)
|
||||
.map { decryptedFido2CredentialViews ->
|
||||
decryptedFido2CredentialViews.filterMatchingCredentials(
|
||||
ids,
|
||||
ripId,
|
||||
)
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView.id
|
||||
?.let { cipherId ->
|
||||
vaultRepository
|
||||
.getCipher(cipherId = cipherId)
|
||||
.toCipherViewOrNull()
|
||||
}
|
||||
}
|
||||
.map { matchingFido2Credentials ->
|
||||
ciphersWithFido2Credentials.filter { cipherView ->
|
||||
matchingFido2Credentials.any { it.cipherId == cipherView.id }
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { throw it },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +86,7 @@ class Fido2CredentialStoreImpl(
|
||||
override suspend fun saveCredential(cred: EncryptionContext) {
|
||||
vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = cred.encryptedFor,
|
||||
userId = authRepository.activeUserId ?: throw NoActiveUserException(),
|
||||
cipher = cred.cipher,
|
||||
)
|
||||
.map { decryptedCipherView ->
|
||||
@@ -103,24 +95,51 @@ class Fido2CredentialStoreImpl(
|
||||
?: vaultRepository.createCipher(decryptedCipherView)
|
||||
}
|
||||
.onFailure { throw it }
|
||||
}
|
||||
|
||||
private fun getActiveUserIdOrThrow() = authRepository.userStateFlow.value?.activeUserId
|
||||
?: throw IllegalStateException("Active user is required.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filtered list containing elements that match the given [relyingPartyId] and a
|
||||
* credential ID contained in [credentialIds].
|
||||
*/
|
||||
private fun List<Fido2CredentialAutofillView>.filterMatchingCredentials(
|
||||
private fun List<CipherListView>.filterMatchingCredentials(
|
||||
credentialIds: List<ByteArray>?,
|
||||
relyingPartyId: String,
|
||||
): List<Fido2CredentialAutofillView> {
|
||||
): List<CipherListView> {
|
||||
val skipCredentialIdFiltering = credentialIds.isNullOrEmpty()
|
||||
return filter { fido2CredentialView ->
|
||||
fido2CredentialView.rpId == relyingPartyId &&
|
||||
(skipCredentialIdFiltering ||
|
||||
credentialIds?.contains(fido2CredentialView.credentialId) == true)
|
||||
return filter { cipherListView ->
|
||||
val hasMatchingRpId = cipherListView.login
|
||||
?.fido2Credentials
|
||||
.orEmpty()
|
||||
.any { it.rpId == relyingPartyId }
|
||||
|
||||
val fido2CredentialIds = cipherListView.login
|
||||
?.fido2Credentials
|
||||
.orEmpty()
|
||||
.map { it.credentialId.toByteArray() }
|
||||
|
||||
val hasIntersectingCredentials = credentialIds
|
||||
?.intersect(fido2CredentialIds)
|
||||
.orEmpty()
|
||||
.isNotEmpty()
|
||||
|
||||
hasMatchingRpId &&
|
||||
(skipCredentialIdFiltering || hasIntersectingCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
private fun GetCipherResult.toCipherViewOrNull(): CipherView? {
|
||||
return when (this) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found for FIDO 2 credential.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(this.error, "Failed to decrypt cipher for FIDO 2 credential.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> this.cipherView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -11,18 +11,20 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
interface TotpCodeManager {
|
||||
|
||||
/**
|
||||
* Flow for getting a DataState with multiple verification code items.
|
||||
* Flow for getting a DataState with multiple verification code items for the given
|
||||
* [cipherListViews].
|
||||
*/
|
||||
fun getTotpCodesStateFlow(
|
||||
fun getTotpCodesForCipherListViewsStateFlow(
|
||||
userId: String,
|
||||
cipherList: List<CipherView>,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||
|
||||
/**
|
||||
* Flow for getting a DataState with a single verification code item.
|
||||
* Flow for getting a DataState with a single verification code item for the given
|
||||
* [cipherListView].
|
||||
*/
|
||||
fun getTotpCodeStateFlow(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
cipherListView: CipherListView,
|
||||
): StateFlow<DataState<VerificationCodeItem?>>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -33,16 +33,16 @@ class TotpCodeManagerImpl(
|
||||
) : TotpCodeManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val mutableVerificationCodeStateFlowMap =
|
||||
mutableMapOf<CipherView, StateFlow<DataState<VerificationCodeItem?>>>()
|
||||
private val mutableCipherListViewVerificationCodeStateFlowMap =
|
||||
mutableMapOf<CipherListView, StateFlow<DataState<VerificationCodeItem?>>>()
|
||||
|
||||
override fun getTotpCodesStateFlow(
|
||||
override fun getTotpCodesForCipherListViewsStateFlow(
|
||||
userId: String,
|
||||
cipherList: List<CipherView>,
|
||||
cipherListViews: List<CipherListView>,
|
||||
): StateFlow<DataState<List<VerificationCodeItem>>> {
|
||||
// Generate state flows
|
||||
val stateFlows = cipherList.map { cipherView ->
|
||||
getTotpCodeStateFlowInternal(userId, cipherView)
|
||||
val stateFlows = cipherListViews.map { cipherListView ->
|
||||
getTotpCodeStateFlowInternal(userId, cipherListView)
|
||||
}
|
||||
return combine(stateFlows) { results ->
|
||||
when {
|
||||
@@ -66,62 +66,55 @@ class TotpCodeManagerImpl(
|
||||
|
||||
override fun getTotpCodeStateFlow(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
cipherListView: CipherListView,
|
||||
): StateFlow<DataState<VerificationCodeItem?>> =
|
||||
getTotpCodeStateFlowInternal(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
cipherListView = cipherListView,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun getTotpCodeStateFlowInternal(
|
||||
userId: String,
|
||||
cipher: CipherView?,
|
||||
cipherListView: CipherListView?,
|
||||
): StateFlow<DataState<VerificationCodeItem?>> {
|
||||
val cipherId = cipher?.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
val cipherId = cipherListView?.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
cipherListView.login?.totp ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
|
||||
return mutableVerificationCodeStateFlowMap.getOrPut(cipher) {
|
||||
return mutableCipherListViewVerificationCodeStateFlowMap.getOrPut(cipherListView) {
|
||||
// Define a per-item scope so that we can clear the Flow from the scope when it is
|
||||
// no longer needed.
|
||||
val itemScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
flow<DataState<VerificationCodeItem?>> {
|
||||
val totpCode = cipher
|
||||
.login
|
||||
?.totp
|
||||
?: run {
|
||||
emit(DataState.Loaded(null))
|
||||
return@flow
|
||||
}
|
||||
|
||||
var item: VerificationCodeItem? = null
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val time = (clock.millis() / ONE_SECOND_MILLISECOND).toInt()
|
||||
val dateTime = DateTime.now()
|
||||
val dateTime = clock.instant()
|
||||
val time = dateTime.epochSecond.toInt()
|
||||
if (item == null || item.isExpired(clock = clock)) {
|
||||
vaultSdkSource
|
||||
.generateTotp(
|
||||
totp = totpCode,
|
||||
.generateTotpForCipherListView(
|
||||
cipherListView = cipherListView,
|
||||
userId = userId,
|
||||
time = dateTime,
|
||||
)
|
||||
.onSuccess { response ->
|
||||
item = VerificationCodeItem(
|
||||
code = response.code,
|
||||
totpCode = totpCode,
|
||||
periodSeconds = response.period.toInt(),
|
||||
timeLeftSeconds = response.period.toInt() -
|
||||
time % response.period.toInt(),
|
||||
issueTime = clock.millis(),
|
||||
uriLoginViewList = cipher.login?.uris,
|
||||
uriLoginViewList = cipherListView.login?.uris,
|
||||
id = cipherId,
|
||||
name = cipher.name,
|
||||
username = cipher.login?.username,
|
||||
hasPasswordReprompt = when (cipher.reprompt) {
|
||||
name = cipherListView.name,
|
||||
username = cipherListView.login?.username,
|
||||
hasPasswordReprompt = when (cipherListView.reprompt) {
|
||||
CipherRepromptType.PASSWORD -> true
|
||||
CipherRepromptType.NONE -> false
|
||||
},
|
||||
orgUsesTotp = cipher.organizationUseTotp,
|
||||
orgUsesTotp = cipherListView.organizationUseTotp,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
@@ -129,12 +122,9 @@ class TotpCodeManagerImpl(
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
item?.let {
|
||||
item = it.copy(
|
||||
timeLeftSeconds = it.periodSeconds -
|
||||
(time % it.periodSeconds),
|
||||
)
|
||||
}
|
||||
item = item.copy(
|
||||
timeLeftSeconds = item.periodSeconds - (time % item.periodSeconds),
|
||||
)
|
||||
}
|
||||
|
||||
item?.let {
|
||||
@@ -144,7 +134,7 @@ class TotpCodeManagerImpl(
|
||||
}
|
||||
}
|
||||
.onCompletion {
|
||||
mutableVerificationCodeStateFlowMap.remove(cipher)
|
||||
mutableCipherListViewVerificationCodeStateFlowMap.remove(cipherListView)
|
||||
itemScope.cancel()
|
||||
}
|
||||
.stateIn(
|
||||
|
||||
@@ -183,6 +183,7 @@ class VaultLockManagerImpl(
|
||||
method = initUserCryptoMethod,
|
||||
userId = userId,
|
||||
signingKey = null,
|
||||
securityState = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.bitwarden.vault.LoginUriView
|
||||
* Models the items returned by the TotpCodeManager.
|
||||
*
|
||||
* @property code The verification code for the item.
|
||||
* @property totpCode The totp code for the item.
|
||||
* @property periodSeconds The time span where the code is valid in seconds.
|
||||
* @property timeLeftSeconds The seconds remaining until a new code is required.
|
||||
* @property issueTime The time the verification code was issued.
|
||||
@@ -19,7 +18,6 @@ import com.bitwarden.vault.LoginUriView
|
||||
*/
|
||||
data class VerificationCodeItem(
|
||||
val code: String,
|
||||
val totpCode: String,
|
||||
val periodSeconds: Int,
|
||||
val timeLeftSeconds: Int,
|
||||
val issueTime: Long,
|
||||
|
||||
@@ -9,15 +9,16 @@ 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.CollectionView
|
||||
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.VaultLockManager
|
||||
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.DecryptFido2CredentialAutofillViewResult
|
||||
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
|
||||
@@ -59,20 +60,13 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
val vaultDataStateFlow: StateFlow<DataState<VaultData>>
|
||||
|
||||
/**
|
||||
* Flow that represents all ciphers for the active user.
|
||||
* 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 ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
|
||||
|
||||
/**
|
||||
* Flow that represents all ciphers 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 ciphersListViewStateFlow: StateFlow<DataState<List<CipherListView>>>
|
||||
val decryptCipherListResultStateFlow: StateFlow<DataState<DecryptCipherListResult>>
|
||||
|
||||
/**
|
||||
* Flow that represents all collections for the active user.
|
||||
@@ -163,13 +157,6 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
*/
|
||||
fun getAuthCodesFlow(): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||
|
||||
/**
|
||||
* Get the decrypted list of fido credentials for the current ciphers and user id.
|
||||
*/
|
||||
suspend fun getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList: List<CipherView>,
|
||||
): DecryptFido2CredentialAutofillViewResult
|
||||
|
||||
/**
|
||||
* Silently discovers FIDO 2 credentials for a given [userId] and [relyingPartyId].
|
||||
*/
|
||||
@@ -238,7 +225,7 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
/**
|
||||
* Attempt to get the verification code and the period.
|
||||
*/
|
||||
suspend fun generateTotp(totpCode: String, time: DateTime): GenerateTotpResult
|
||||
suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a send.
|
||||
@@ -262,6 +249,18 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
|
||||
/**
|
||||
* Attempt to get the user's vault data for export.
|
||||
*
|
||||
* @param format The export format to use.
|
||||
* @param restrictedTypes A list of restricted types to export.
|
||||
*/
|
||||
suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult
|
||||
suspend fun exportVaultDataToString(
|
||||
format: ExportFormat,
|
||||
restrictedTypes: List<CipherType>,
|
||||
): ExportVaultDataResult
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun getVaultListItemStateFlow(itemId: String): StateFlow<DataState<CipherListView?>>
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@ import com.bitwarden.send.Send
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CollectionView
|
||||
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
|
||||
@@ -41,6 +43,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
@@ -61,10 +64,10 @@ import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
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.DecryptFido2CredentialAutofillViewResult
|
||||
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
|
||||
@@ -118,6 +121,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.security.GeneralSecurityException
|
||||
import java.time.Clock
|
||||
import java.time.temporal.ChronoUnit
|
||||
@@ -167,11 +171,8 @@ class VaultRepositoryImpl(
|
||||
|
||||
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendData>>(DataState.Loading)
|
||||
|
||||
private val mutableCiphersStateFlow =
|
||||
MutableStateFlow<DataState<List<CipherView>>>(DataState.Loading)
|
||||
|
||||
private val mutableCiphersListViewStateFlow =
|
||||
MutableStateFlow<DataState<List<CipherListView>>>(DataState.Loading)
|
||||
private val mutableDecryptCipherListResultFlow =
|
||||
MutableStateFlow<DataState<DecryptCipherListResult>>(DataState.Loading)
|
||||
|
||||
private val mutableFoldersStateFlow =
|
||||
MutableStateFlow<DataState<List<FolderView>>>(DataState.Loading)
|
||||
@@ -186,7 +187,7 @@ class VaultRepositoryImpl(
|
||||
|
||||
override val vaultDataStateFlow: StateFlow<DataState<VaultData>> =
|
||||
combine(
|
||||
ciphersStateFlow,
|
||||
decryptCipherListResultStateFlow,
|
||||
foldersStateFlow,
|
||||
collectionsStateFlow,
|
||||
sendDataStateFlow,
|
||||
@@ -198,10 +199,9 @@ class VaultRepositoryImpl(
|
||||
sendsDataState,
|
||||
) { ciphersData, foldersData, collectionsData, sendsData ->
|
||||
VaultData(
|
||||
cipherViewList = ciphersData,
|
||||
fido2CredentialAutofillViewList = null,
|
||||
folderViewList = foldersData,
|
||||
decryptCipherListResult = ciphersData,
|
||||
collectionViewList = collectionsData,
|
||||
folderViewList = foldersData,
|
||||
sendViewList = sendsData.sendViewList,
|
||||
)
|
||||
}
|
||||
@@ -215,11 +215,8 @@ class VaultRepositoryImpl(
|
||||
override val totpCodeFlow: Flow<TotpCodeResult>
|
||||
get() = mutableTotpCodeResultFlow.asSharedFlow()
|
||||
|
||||
override val ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
|
||||
get() = mutableCiphersStateFlow.asStateFlow()
|
||||
|
||||
override val ciphersListViewStateFlow: StateFlow<DataState<List<CipherListView>>>
|
||||
get() = mutableCiphersListViewStateFlow.asStateFlow()
|
||||
override val decryptCipherListResultStateFlow: StateFlow<DataState<DecryptCipherListResult>>
|
||||
get() = mutableDecryptCipherListResultFlow.asStateFlow()
|
||||
|
||||
override val domainsStateFlow: StateFlow<DataState<DomainsData>>
|
||||
get() = mutableDomainsStateFlow.asStateFlow()
|
||||
@@ -258,16 +255,7 @@ class VaultRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
// Setup ciphers MutableStateFlow
|
||||
mutableCiphersStateFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
) { activeUserId ->
|
||||
observeVaultDiskCiphers(activeUserId)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
mutableCiphersListViewStateFlow
|
||||
mutableDecryptCipherListResultFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
@@ -314,7 +302,7 @@ class VaultRepositoryImpl(
|
||||
|
||||
pushManager
|
||||
.fullSyncFlow
|
||||
.onEach { syncIfNecessary() }
|
||||
.onEach { sync(forced = false) }
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
@@ -354,7 +342,7 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
private fun clearUnlockedData() {
|
||||
mutableCiphersStateFlow.update { DataState.Loading }
|
||||
mutableDecryptCipherListResultFlow.update { DataState.Loading }
|
||||
mutableFoldersStateFlow.update { DataState.Loading }
|
||||
mutableCollectionsStateFlow.update { DataState.Loading }
|
||||
mutableSendDataStateFlow.update { DataState.Loading }
|
||||
@@ -369,7 +357,7 @@ class VaultRepositoryImpl(
|
||||
override fun sync(forced: Boolean) {
|
||||
val userId = activeUserId ?: return
|
||||
if (!syncJob.isCompleted) return
|
||||
mutableCiphersStateFlow.updateToPendingOrLoading()
|
||||
mutableDecryptCipherListResultFlow.updateToPendingOrLoading()
|
||||
mutableDomainsStateFlow.updateToPendingOrLoading()
|
||||
mutableFoldersStateFlow.updateToPendingOrLoading()
|
||||
mutableCollectionsStateFlow.updateToPendingOrLoading()
|
||||
@@ -406,11 +394,33 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getVaultItemStateFlow(itemId: String): StateFlow<DataState<CipherView?>> =
|
||||
vaultDataStateFlow
|
||||
.map { dataState ->
|
||||
dataState.map { vaultData ->
|
||||
val getCipherResult = vaultData
|
||||
.decryptCipherListResult
|
||||
.successes
|
||||
.find { it.id == itemId }
|
||||
.let { getCipher(itemId) }
|
||||
when (getCipherResult) {
|
||||
is GetCipherResult.Success -> getCipherResult.cipherView
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
override fun getVaultListItemStateFlow(itemId: String): StateFlow<DataState<CipherListView?>> =
|
||||
vaultDataStateFlow
|
||||
.map { dataState ->
|
||||
dataState.map { vaultData ->
|
||||
vaultData
|
||||
.cipherViewList
|
||||
.decryptCipherListResult
|
||||
.successes
|
||||
.find { it.id == itemId }
|
||||
}
|
||||
}
|
||||
@@ -457,9 +467,10 @@ class VaultRepositoryImpl(
|
||||
.map { dataState ->
|
||||
dataState.map { vaultData ->
|
||||
vaultData
|
||||
.cipherViewList
|
||||
.decryptCipherListResult
|
||||
.successes
|
||||
.filter {
|
||||
it.type == CipherType.LOGIN &&
|
||||
it.type is CipherListViewType.Login &&
|
||||
!it.login?.totp.isNullOrBlank() &&
|
||||
it.deletedDate == null
|
||||
}
|
||||
@@ -469,9 +480,9 @@ class VaultRepositoryImpl(
|
||||
.flatMapLatest { cipherDataState ->
|
||||
val cipherList = cipherDataState.data ?: emptyList()
|
||||
totpCodeManager
|
||||
.getTotpCodesStateFlow(
|
||||
.getTotpCodesForCipherListViewsStateFlow(
|
||||
userId = userId,
|
||||
cipherList = cipherList,
|
||||
cipherListViews = cipherList,
|
||||
)
|
||||
.map { verificationCodeDataStates ->
|
||||
combineDataStates(
|
||||
@@ -496,13 +507,13 @@ class VaultRepositoryImpl(
|
||||
val userId = activeUserId ?: return MutableStateFlow(
|
||||
DataState.Error(IllegalStateException("No active user"), null),
|
||||
)
|
||||
return getVaultItemStateFlow(cipherId)
|
||||
return getVaultListItemStateFlow(cipherId)
|
||||
.flatMapLatest { cipherDataState ->
|
||||
cipherDataState
|
||||
.data
|
||||
?.let {
|
||||
totpCodeManager
|
||||
.getTotpCodeStateFlow(userId = userId, cipher = it)
|
||||
.getTotpCodeStateFlow(userId = userId, cipherListView = it)
|
||||
.map { totpCodeDataState ->
|
||||
combineDataStates(totpCodeDataState, cipherDataState) { _, _ ->
|
||||
// We are only combining the DataStates to know the overall
|
||||
@@ -520,22 +531,6 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getDecryptedFido2CredentialAutofillViews(
|
||||
cipherViewList: List<CipherView>,
|
||||
): DecryptFido2CredentialAutofillViewResult {
|
||||
return vaultSdkSource
|
||||
.decryptFido2CredentialAutofillViews(
|
||||
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error(
|
||||
error = NoActiveUserException(),
|
||||
),
|
||||
cipherViews = cipherViewList.toTypedArray(),
|
||||
)
|
||||
.fold(
|
||||
onFailure = { DecryptFido2CredentialAutofillViewResult.Error(error = it) },
|
||||
onSuccess = { DecryptFido2CredentialAutofillViewResult.Success(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun silentlyDiscoverCredentials(
|
||||
userId: String,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
@@ -578,6 +573,7 @@ class VaultRepositoryImpl(
|
||||
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
|
||||
.decodeToString()
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Timber.w(e, "unlockVaultWithBiometrics failed when decrypting biometrics key")
|
||||
return VaultUnlockResult.BiometricDecodingError(error = e)
|
||||
}
|
||||
}
|
||||
@@ -591,6 +587,7 @@ class VaultRepositoryImpl(
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Timber.w(e, "unlockVaultWithBiometrics failed to migrate the user to IV encryption")
|
||||
return VaultUnlockResult.BiometricDecodingError(error = e)
|
||||
}
|
||||
} else {
|
||||
@@ -808,15 +805,24 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun generateTotp(
|
||||
totpCode: String,
|
||||
cipherId: String,
|
||||
time: DateTime,
|
||||
): GenerateTotpResult {
|
||||
val userId = activeUserId
|
||||
?: return GenerateTotpResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource.generateTotp(
|
||||
val cipherListView = decryptCipherListResultStateFlow
|
||||
.value
|
||||
.data
|
||||
?.successes
|
||||
?.find { it.id == cipherId }
|
||||
?: return GenerateTotpResult.Error(
|
||||
error = IllegalArgumentException(cipherId),
|
||||
)
|
||||
|
||||
return vaultSdkSource.generateTotpForCipherListView(
|
||||
time = time,
|
||||
userId = userId,
|
||||
totp = totpCode,
|
||||
cipherListView = cipherListView,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
@@ -931,7 +937,10 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult {
|
||||
override suspend fun exportVaultDataToString(
|
||||
format: ExportFormat,
|
||||
restrictedTypes: List<CipherType>,
|
||||
): ExportVaultDataResult {
|
||||
val userId = activeUserId
|
||||
?: return ExportVaultDataResult.Error(error = NoActiveUserException())
|
||||
val folders = vaultDiskSource
|
||||
@@ -945,7 +954,11 @@ class VaultRepositoryImpl(
|
||||
.firstOrNull()
|
||||
.orEmpty()
|
||||
.map { it.toEncryptedSdkCipher() }
|
||||
.filter { it.collectionIds.isEmpty() && it.deletedDate == null }
|
||||
.filter {
|
||||
it.collectionIds.isEmpty() &&
|
||||
it.deletedDate == null &&
|
||||
!restrictedTypes.contains(it.type)
|
||||
}
|
||||
|
||||
return vaultSdkSource
|
||||
.exportVaultDataToString(
|
||||
@@ -1063,47 +1076,35 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeVaultDiskCiphers(
|
||||
userId: String,
|
||||
): Flow<DataState<List<CipherView>>> =
|
||||
vaultDiskSource
|
||||
.getCiphersFlow(userId = userId)
|
||||
.onStart { mutableCiphersStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptCipherList(
|
||||
userId = userId,
|
||||
cipherList = it.toEncryptedSdkCipherList(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ciphers -> DataState.Loaded(ciphers.sortAlphabetically()) },
|
||||
onFailure = { throwable -> DataState.Error(throwable) },
|
||||
)
|
||||
}
|
||||
.map { it.orLoadingIfNotSynced(userId = userId) }
|
||||
.onEach { mutableCiphersStateFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskCiphersToCipherListView(
|
||||
userId: String,
|
||||
): Flow<DataState<List<CipherListView>>> =
|
||||
): Flow<DataState<DecryptCipherListResult>> =
|
||||
vaultDiskSource
|
||||
.getCiphersFlow(userId = userId)
|
||||
.onStart { mutableCiphersListViewStateFlow.updateToPendingOrLoading() }
|
||||
.onStart { mutableDecryptCipherListResultFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
waitUntilUnlocked(userId = userId)
|
||||
vaultSdkSource
|
||||
.decryptCipherListCollection(
|
||||
.decryptCipherListWithFailures(
|
||||
userId = userId,
|
||||
cipherList = it.toEncryptedSdkCipherList(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { ciphers -> DataState.Loaded(ciphers.sortAlphabetically()) },
|
||||
onSuccess = { result ->
|
||||
// TODO (PM-18210): Display decryption result failures
|
||||
DataState.Loaded(
|
||||
result.copy(successes = result.successes.sortAlphabetically()),
|
||||
)
|
||||
},
|
||||
onFailure = { throwable -> DataState.Error(throwable) },
|
||||
)
|
||||
}
|
||||
.map { it.orLoadingIfNotSynced(userId = userId) }
|
||||
.onEach { mutableCiphersListViewStateFlow.value = it }
|
||||
.map {
|
||||
it
|
||||
.takeUnless { settingsDiskSource.getLastSyncTime(userId = userId) == null }
|
||||
?: DataState.Loading
|
||||
}
|
||||
.onEach { mutableDecryptCipherListResultFlow.value = it }
|
||||
|
||||
private fun observeVaultDiskDomains(
|
||||
userId: String,
|
||||
@@ -1187,7 +1188,7 @@ class VaultRepositoryImpl(
|
||||
.onEach { mutableSendDataStateFlow.value = it }
|
||||
|
||||
private fun updateVaultStateFlowsToError(throwable: Throwable) {
|
||||
mutableCiphersStateFlow.update { currentState ->
|
||||
mutableDecryptCipherListResultFlow.update { currentState ->
|
||||
throwable.toNetworkOrErrorState(
|
||||
data = currentState.data,
|
||||
)
|
||||
@@ -1255,8 +1256,8 @@ class VaultRepositoryImpl(
|
||||
val revisionDate = syncCipherUpsertData.revisionDate
|
||||
val isUpdate = syncCipherUpsertData.isUpdate
|
||||
|
||||
val localCipher = ciphersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
val localCipher = decryptCipherListResultStateFlow
|
||||
.mapNotNull { it.data?.successes }
|
||||
.first()
|
||||
.find { it.id == cipherId }
|
||||
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CollectionView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
|
||||
/**
|
||||
* Represents decrypted vault data.
|
||||
*
|
||||
* @param cipherViewList List of decrypted ciphers.
|
||||
* @param decryptCipherListResult Contains the result of decrypting ciphers for display in a list.
|
||||
* @param collectionViewList List of decrypted collections.
|
||||
* @param folderViewList List of decrypted folders.
|
||||
* @param sendViewList List of decrypted sends.
|
||||
* @param fido2CredentialAutofillViewList List of decrypted fido 2 credentials.
|
||||
*/
|
||||
data class VaultData(
|
||||
val cipherViewList: List<CipherView>,
|
||||
val decryptCipherListResult: DecryptCipherListResult,
|
||||
val collectionViewList: List<CollectionView>,
|
||||
val folderViewList: List<FolderView>,
|
||||
val sendViewList: List<SendView>,
|
||||
val fido2CredentialAutofillViewList: List<Fido2CredentialAutofillView>? = null,
|
||||
)
|
||||
|
||||
@@ -40,9 +40,9 @@ import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.model.WindowSize
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.rememberSetupAutoFillHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
@@ -80,17 +80,19 @@ fun SetupAutoFillScreen(
|
||||
is SetupAutoFillDialogState.AutoFillFallbackDialog -> {
|
||||
BitwardenBasicDialog(
|
||||
title = null,
|
||||
message = stringResource(id = R.string.bitwarden_autofill_go_to_settings),
|
||||
message = stringResource(id = BitwardenString.bitwarden_autofill_go_to_settings),
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
is SetupAutoFillDialogState.TurnOnLaterDialog -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(R.string.turn_on_autofill_later),
|
||||
message = stringResource(R.string.return_to_complete_this_step_anytime_in_settings),
|
||||
confirmButtonText = stringResource(id = R.string.confirm),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
title = stringResource(BitwardenString.turn_on_autofill_later),
|
||||
message = stringResource(
|
||||
id = BitwardenString.return_to_complete_this_step_anytime_in_settings,
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = handler.onConfirmTurnOnLaterClick,
|
||||
onDismissClick = handler.onDismissDialog,
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
@@ -109,9 +111,9 @@ fun SetupAutoFillScreen(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(
|
||||
id = if (state.isInitialSetup) {
|
||||
R.string.account_setup
|
||||
BitwardenString.account_setup
|
||||
} else {
|
||||
R.string.turn_on_autofill
|
||||
BitwardenString.turn_on_autofill
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -120,7 +122,9 @@ fun SetupAutoFillScreen(
|
||||
} else {
|
||||
NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(
|
||||
id = BitwardenString.close,
|
||||
),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(SetupAutoFillAction.CloseClick)
|
||||
@@ -164,7 +168,7 @@ private fun SetupAutoFillContent(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(
|
||||
R.string.autofill_services,
|
||||
BitwardenString.autofill_services,
|
||||
),
|
||||
isChecked = state.autofillEnabled,
|
||||
onCheckedChange = onAutofillServiceChanged,
|
||||
@@ -175,7 +179,7 @@ private fun SetupAutoFillContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = onContinueClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -184,7 +188,7 @@ private fun SetupAutoFillContent(
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (state.isInitialSetup) {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(R.string.turn_on_later),
|
||||
label = stringResource(BitwardenString.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -243,14 +247,14 @@ private fun OrderedHeaderContent() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.turn_on_autofill),
|
||||
text = stringResource(BitwardenString.turn_on_autofill),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.use_autofill_to_log_into_your_accounts),
|
||||
text = stringResource(BitwardenString.use_autofill_to_log_into_your_accounts),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
||||
@@ -29,8 +29,8 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ fun SetupCompleteScreen(
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
title = stringResource(R.string.account_setup),
|
||||
title = stringResource(BitwardenString.account_setup),
|
||||
navigationIcon = null,
|
||||
)
|
||||
},
|
||||
@@ -89,7 +89,7 @@ private fun SetupCompleteContent(
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.youre_all_set),
|
||||
text = stringResource(BitwardenString.youre_all_set),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -99,7 +99,7 @@ private fun SetupCompleteContent(
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.what_bitwarden_has_to_offer),
|
||||
text = stringResource(BitwardenString.what_bitwarden_has_to_offer),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -109,7 +109,7 @@ private fun SetupCompleteContent(
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.continue_text),
|
||||
label = stringResource(BitwardenString.continue_text),
|
||||
onClick = onContinue,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -43,9 +43,9 @@ import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.model.WindowSize
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
@@ -107,9 +107,9 @@ fun SetupUnlockScreen(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(
|
||||
id = if (state.isInitialSetup) {
|
||||
R.string.account_setup
|
||||
BitwardenString.account_setup
|
||||
} else {
|
||||
R.string.set_up_unlock
|
||||
BitwardenString.set_up_unlock
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -118,7 +118,9 @@ fun SetupUnlockScreen(
|
||||
} else {
|
||||
NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(
|
||||
id = BitwardenString.close,
|
||||
),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(SetupUnlockAction.CloseClick)
|
||||
@@ -185,7 +187,7 @@ private fun SetupUnlockScreenContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = handler.onContinueClick,
|
||||
isEnabled = state.isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
@@ -218,12 +220,12 @@ private fun SetUpLaterButton(
|
||||
if (displayConfirmation) {
|
||||
@Suppress("MaxLineLength")
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.set_up_unlock_later),
|
||||
title = stringResource(id = BitwardenString.set_up_unlock_later),
|
||||
message = stringResource(
|
||||
id = R.string.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 = R.string.confirm),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
confirmButtonText = stringResource(id = BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = {
|
||||
onConfirmClick()
|
||||
displayConfirmation = false
|
||||
@@ -234,7 +236,7 @@ private fun SetUpLaterButton(
|
||||
}
|
||||
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.set_up_later),
|
||||
label = stringResource(id = BitwardenString.set_up_later),
|
||||
onClick = { displayConfirmation = true },
|
||||
modifier = modifier.testTag(tag = "SetUpLaterButton"),
|
||||
)
|
||||
@@ -254,7 +256,7 @@ private fun ColumnScope.SetupUnlockHeaderCompact() {
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
text = stringResource(id = BitwardenString.set_up_unlock),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -267,7 +269,7 @@ private fun ColumnScope.SetupUnlockHeaderCompact() {
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
id = BitwardenString.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -300,7 +302,7 @@ private fun SetupUnlockHeaderMedium(
|
||||
modifier = Modifier.align(alignment = Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
text = stringResource(id = BitwardenString.set_up_unlock),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -311,7 +313,7 @@ private fun SetupUnlockHeaderMedium(
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
id = BitwardenString.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
@@ -112,8 +112,8 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupUnlockState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupUnlockState.DialogState.Loading(R.string.saving.asText()),
|
||||
dialogState = SetupUnlockState.DialogState.Loading(BitwardenString.saving.asText()),
|
||||
isUnlockWithBiometricsEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.handlers.rememberCheckEmailHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
@@ -77,10 +77,10 @@ fun CheckEmailScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
title = stringResource(id = BitwardenString.create_account),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
},
|
||||
@@ -126,7 +126,7 @@ private fun CheckEmailContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.check_your_email),
|
||||
text = stringResource(id = BitwardenString.check_your_email),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -139,7 +139,7 @@ private fun CheckEmailContent(
|
||||
|
||||
Text(
|
||||
text = annotatedStringResource(
|
||||
id = R.string.we_sent_an_email_to,
|
||||
id = BitwardenString.we_sent_an_email_to,
|
||||
args = arrayOf(email),
|
||||
emphasisHighlightStyle = SpanStyle(
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -158,7 +158,7 @@ private fun CheckEmailContent(
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account,
|
||||
BitwardenString.select_the_link_in_the_email_to_verify_your_email_address_and_continue_creating_your_account,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -170,7 +170,7 @@ private fun CheckEmailContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.open_email_app),
|
||||
label = stringResource(id = BitwardenString.open_email_app),
|
||||
onClick = onOpenEmailAppClick,
|
||||
modifier = Modifier
|
||||
.testTag("OpenEmailApp")
|
||||
@@ -179,7 +179,7 @@ private fun CheckEmailContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.change_email_address),
|
||||
label = stringResource(BitwardenString.change_email_address),
|
||||
onClick = onChangeEmailClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -44,9 +44,9 @@ import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.model.WindowSize
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.handlers.CompleteRegistrationHandler
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.handlers.rememberCompleteRegistrationHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
|
||||
@@ -115,8 +115,8 @@ fun CompleteRegistrationScreen(
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
confirmButtonText = stringResource(id = R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.no),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.no),
|
||||
onConfirmClick = handler.onContinueWithBreachedPasswordClick,
|
||||
onDismissClick = handler.onDismissErrorDialog,
|
||||
onDismissRequest = handler.onDismissErrorDialog,
|
||||
@@ -124,7 +124,7 @@ fun CompleteRegistrationScreen(
|
||||
}
|
||||
|
||||
CompleteRegistrationDialog.Loading -> {
|
||||
BitwardenLoadingDialog(text = stringResource(id = R.string.create_account))
|
||||
BitwardenLoadingDialog(text = stringResource(id = BitwardenString.create_account))
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
@@ -137,10 +137,10 @@ fun CompleteRegistrationScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
title = stringResource(id = BitwardenString.create_account),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
},
|
||||
@@ -159,7 +159,7 @@ fun CompleteRegistrationScreen(
|
||||
isCheckDataBreachesToggled = state.isCheckDataBreachesToggled,
|
||||
handler = handler,
|
||||
nextButtonEnabled = state.validSubmissionReady,
|
||||
callToActionText = stringResource(R.string.next),
|
||||
callToActionText = stringResource(BitwardenString.next),
|
||||
minimumPasswordLength = state.minimumPasswordLength,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
@@ -194,8 +194,8 @@ private fun CompleteRegistrationContent(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenActionCardSmall(
|
||||
actionIcon = rememberVectorPainter(id = BitwardenDrawable.ic_question_circle),
|
||||
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
||||
callToActionText = stringResource(id = R.string.learn_more),
|
||||
actionText = stringResource(id = BitwardenString.what_makes_a_password_strong),
|
||||
callToActionText = stringResource(id = BitwardenString.learn_more),
|
||||
callToActionTextColor = BitwardenTheme.colorScheme.text.interaction,
|
||||
colors = bitwardenCardColors(
|
||||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
@@ -209,7 +209,7 @@ private fun CompleteRegistrationContent(
|
||||
|
||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password_required),
|
||||
label = stringResource(id = BitwardenString.master_password_required),
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
value = passwordInput,
|
||||
@@ -231,7 +231,7 @@ private fun CompleteRegistrationContent(
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password_required),
|
||||
label = stringResource(id = BitwardenString.retype_master_password_required),
|
||||
value = confirmPasswordInput,
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
@@ -245,14 +245,15 @@ private fun CompleteRegistrationContent(
|
||||
)
|
||||
BitwardenTextField(
|
||||
label = stringResource(
|
||||
id = R.string.master_password_hint_not_specified,
|
||||
id = BitwardenString.master_password_hint_not_specified,
|
||||
),
|
||||
value = passwordHintInput,
|
||||
onValueChange = handler.onPasswordHintChange,
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
|
||||
id = BitwardenString
|
||||
.bitwarden_cannot_recover_a_lost_or_forgotten_master_password,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
@@ -260,7 +261,7 @@ private fun CompleteRegistrationContent(
|
||||
)
|
||||
BitwardenClickableText(
|
||||
label = stringResource(
|
||||
id = R.string.learn_about_other_ways_to_prevent_account_lockout,
|
||||
id = BitwardenString.learn_about_other_ways_to_prevent_account_lockout,
|
||||
),
|
||||
onClick = handler.onLearnToPreventLockout,
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
@@ -275,7 +276,9 @@ private fun CompleteRegistrationContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||
label = stringResource(
|
||||
id = BitwardenString.check_known_data_breaches_for_this_password,
|
||||
),
|
||||
isChecked = isCheckDataBreachesToggled,
|
||||
onCheckedChange = handler.onCheckDataBreachesToggle,
|
||||
cardStyle = CardStyle.Full,
|
||||
@@ -336,7 +339,7 @@ private fun OrderedHeaderContent() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.choose_your_master_password),
|
||||
text = stringResource(BitwardenString.choose_your_master_password),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -344,7 +347,7 @@ private fun OrderedHeaderContent() {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.choose_a_unique_and_strong_password_to_keep_your_information_safe,
|
||||
BitwardenString.choose_a_unique_and_strong_password_to_keep_your_information_safe,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
|
||||
@@ -5,9 +5,9 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -145,7 +145,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = R.string.email_verified.asText(),
|
||||
message = BitwardenString.email_verified.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -188,9 +188,9 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = registerAccountResult.errorMessage?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
error = registerAccountResult.error,
|
||||
),
|
||||
)
|
||||
@@ -217,8 +217,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.HaveIBeenPwned(
|
||||
title = R.string.exposed_master_password.asText(),
|
||||
message = R.string.password_found_in_a_data_breach_alert_description.asText(),
|
||||
title = BitwardenString.exposed_master_password.asText(),
|
||||
message = BitwardenString.password_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -228,8 +228,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_and_exposed_master_password.asText(),
|
||||
message = R.string.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
||||
title = BitwardenString.weak_and_exposed_master_password.asText(),
|
||||
message = BitwardenString.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -239,8 +239,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_master_password.asText(),
|
||||
message = R.string.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
||||
title = BitwardenString.weak_master_password.asText(),
|
||||
message = BitwardenString.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -252,7 +252,7 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.ShowToast(
|
||||
message = R.string.account_created_success.asText(),
|
||||
message = BitwardenString.account_created_success.asText(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -307,9 +307,9 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.email_address.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -319,8 +319,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.invalid_email.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -330,8 +330,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
)
|
||||
@@ -342,8 +342,8 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CompleteRegistrationDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Draws a password indicator that displays password strength based on the given [state].
|
||||
@@ -75,11 +75,11 @@ fun PasswordStrengthIndicator(
|
||||
)
|
||||
val label = when (state) {
|
||||
PasswordStrengthState.NONE -> "".asText()
|
||||
PasswordStrengthState.WEAK_1 -> R.string.weak.asText()
|
||||
PasswordStrengthState.WEAK_2 -> R.string.weak.asText()
|
||||
PasswordStrengthState.WEAK_3 -> R.string.weak.asText()
|
||||
PasswordStrengthState.GOOD -> R.string.good.asText()
|
||||
PasswordStrengthState.STRONG -> R.string.strong.asText()
|
||||
PasswordStrengthState.WEAK_1 -> BitwardenString.weak.asText()
|
||||
PasswordStrengthState.WEAK_2 -> BitwardenString.weak.asText()
|
||||
PasswordStrengthState.WEAK_3 -> BitwardenString.weak.asText()
|
||||
PasswordStrengthState.GOOD -> BitwardenString.good.asText()
|
||||
PasswordStrengthState.STRONG -> BitwardenString.strong.asText()
|
||||
}
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@@ -154,7 +154,7 @@ private fun MinimumCharacterCount(
|
||||
}
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.minimum_characters, minimumCharacterCount),
|
||||
text = stringResource(BitwardenString.minimum_characters, minimumCharacterCount),
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
style = BitwardenTheme.typography.labelSmall,
|
||||
)
|
||||
|
||||
@@ -39,8 +39,8 @@ import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
@@ -120,8 +120,8 @@ fun CreateAccountScreen(
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
confirmButtonText = stringResource(id = R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.no),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.no),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ContinueWithBreachedPasswordClick) }
|
||||
},
|
||||
@@ -135,7 +135,7 @@ fun CreateAccountScreen(
|
||||
}
|
||||
|
||||
CreateAccountDialog.Loading -> {
|
||||
BitwardenLoadingDialog(text = stringResource(id = R.string.create_account))
|
||||
BitwardenLoadingDialog(text = stringResource(id = BitwardenString.create_account))
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
@@ -148,16 +148,16 @@ fun CreateAccountScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
title = stringResource(id = BitwardenString.create_account),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(CloseClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.submit),
|
||||
label = stringResource(id = BitwardenString.submit),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SubmitClick) }
|
||||
},
|
||||
@@ -174,7 +174,7 @@ fun CreateAccountScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.email_address),
|
||||
label = stringResource(id = BitwardenString.email_address),
|
||||
value = state.emailInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EmailInputChange(it)) }
|
||||
@@ -189,7 +189,7 @@ fun CreateAccountScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
label = stringResource(id = BitwardenString.master_password),
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
value = state.passwordInput,
|
||||
@@ -217,7 +217,7 @@ fun CreateAccountScreen(
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password),
|
||||
label = stringResource(id = BitwardenString.retype_master_password),
|
||||
value = state.confirmPasswordInput,
|
||||
showPassword = showPassword,
|
||||
showPasswordChange = { showPassword = it },
|
||||
@@ -232,12 +232,13 @@ fun CreateAccountScreen(
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.master_password_hint),
|
||||
label = stringResource(id = BitwardenString.master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordHintChange(it)) }
|
||||
},
|
||||
supportingText = stringResource(id = R.string.master_password_hint_description),
|
||||
supportingText = stringResource(
|
||||
id = BitwardenString.master_password_hint_description),
|
||||
textFieldTestTag = "MasterPasswordHintLabel",
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
@@ -246,7 +247,8 @@ fun CreateAccountScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||
label = stringResource(
|
||||
id = BitwardenString.check_known_data_breaches_for_this_password),
|
||||
isChecked = state.isCheckDataBreachesToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ newState ->
|
||||
@@ -288,8 +290,8 @@ private fun TermsAndPrivacySwitch(
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val strTerms = stringResource(id = R.string.terms_of_service)
|
||||
val strPrivacy = stringResource(id = R.string.privacy_policy)
|
||||
val strTerms = stringResource(id = BitwardenString.terms_of_service)
|
||||
val strPrivacy = stringResource(id = BitwardenString.privacy_policy)
|
||||
BitwardenSwitch(
|
||||
modifier = modifier.semantics(mergeDescendants = true) {
|
||||
customActions = listOf(
|
||||
@@ -310,7 +312,7 @@ private fun TermsAndPrivacySwitch(
|
||||
)
|
||||
},
|
||||
label = annotatedStringResource(
|
||||
id = R.string
|
||||
id = BitwardenString
|
||||
.by_activating_this_switch_you_agree_to_the_terms_of_service_and_privacy_policy,
|
||||
onAnnotationClick = {
|
||||
when (it) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
@@ -142,8 +142,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -177,9 +177,9 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = registerAccountResult.errorMessage?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
error = registerAccountResult.error,
|
||||
),
|
||||
)
|
||||
@@ -200,8 +200,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.exposed_master_password.asText(),
|
||||
message = R.string.password_found_in_a_data_breach_alert_description.asText(),
|
||||
title = BitwardenString.exposed_master_password.asText(),
|
||||
message = BitwardenString.password_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -211,8 +211,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_and_exposed_master_password.asText(),
|
||||
message = R.string.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
||||
title = BitwardenString.weak_and_exposed_master_password.asText(),
|
||||
message = BitwardenString.weak_password_identified_and_found_in_a_data_breach_alert_description.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -222,8 +222,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.HaveIBeenPwned(
|
||||
title = R.string.weak_master_password.asText(),
|
||||
message = R.string.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
||||
title = BitwardenString.weak_master_password.asText(),
|
||||
message = BitwardenString.weak_password_identified_use_a_strong_password_to_protect_your_account.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -295,9 +295,9 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.email_address.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -307,8 +307,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_email.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.invalid_email.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -318,8 +318,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
)
|
||||
@@ -330,8 +330,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -341,8 +341,8 @@ class CreateAccountViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = CreateAccountDialog.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.accept_policies_error.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.accept_policies_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -412,10 +412,10 @@ data class CreateAccountState(
|
||||
// Important: Your master password cannot be recovered if you forget it! 12
|
||||
// characters minimum
|
||||
@Suppress("MaxLineLength")
|
||||
get() = R.string.important.asText()
|
||||
get() = BitwardenString.important.asText()
|
||||
.concat(
|
||||
": ".asText(),
|
||||
R.string.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
|
||||
BitwardenString.your_master_password_cannot_be_recovered_if_you_forget_it_x_characters_minimum
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
@@ -100,13 +101,13 @@ fun EnterpriseSignOnScreen(
|
||||
title = stringResource(id = R.string.app_name),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.CloseButtonClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.log_in_verb),
|
||||
label = stringResource(id = BitwardenString.log_in_verb),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) }
|
||||
},
|
||||
@@ -140,7 +141,7 @@ private fun EnterpriseSignOnScreenContent(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_sso_summary),
|
||||
text = stringResource(id = BitwardenString.log_in_sso_summary),
|
||||
textAlign = TextAlign.Start,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -157,7 +158,7 @@ private fun EnterpriseSignOnScreenContent(
|
||||
.fillMaxWidth(),
|
||||
value = state.orgIdentifierInput,
|
||||
onValueChange = onOrgIdentifierInputChange,
|
||||
label = stringResource(id = R.string.org_identifier),
|
||||
label = stringResource(id = BitwardenString.org_identifier),
|
||||
textFieldTestTag = "OrgSSOIdentifierEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
@@ -190,13 +191,13 @@ private fun EnterpriseSignOnDialogs(
|
||||
|
||||
is EnterpriseSignOnState.DialogState.KeyConnectorDomain -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(R.string.confirm_key_connector_domain),
|
||||
title = stringResource(BitwardenString.confirm_key_connector_domain),
|
||||
message = stringResource(
|
||||
R.string.please_confirm_domain_with_admin,
|
||||
BitwardenString.please_confirm_domain_with_admin,
|
||||
dialogState.keyConnectorDomain,
|
||||
),
|
||||
confirmButtonText = stringResource(R.string.confirm),
|
||||
dismissButtonText = stringResource(R.string.cancel),
|
||||
confirmButtonText = stringResource(BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(BitwardenString.cancel),
|
||||
onConfirmClick = onConfirmKeyConnectorDomain,
|
||||
onDismissRequest = onDismissKeyConnectorDomain,
|
||||
onDismissClick = onDismissKeyConnectorDomain,
|
||||
|
||||
@@ -7,9 +7,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.data.repository.util.baseIdentityUrl
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
@@ -163,7 +163,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
is LoginResult.Error -> {
|
||||
showError(
|
||||
message = loginResult.errorMessage?.asText()
|
||||
?: R.string.login_sso_error.asText(),
|
||||
?: BitwardenString.login_sso_error.asText(),
|
||||
error = loginResult.error,
|
||||
)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
is LoginResult.UnofficialServerError -> {
|
||||
@Suppress("MaxLineLength")
|
||||
showError(
|
||||
message = R.string
|
||||
message = BitwardenString
|
||||
.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
|
||||
.asText(),
|
||||
)
|
||||
@@ -200,20 +200,22 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
.baseWebVaultUrlOrDefault
|
||||
|
||||
showError(
|
||||
message = R.string
|
||||
message = BitwardenString
|
||||
.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden
|
||||
.asText(vaultUrl.toUriOrNull()?.host ?: vaultUrl),
|
||||
)
|
||||
}
|
||||
|
||||
LoginResult.CertificateError -> {
|
||||
showError(message = R.string.we_couldnt_verify_the_servers_certificate.asText())
|
||||
showError(
|
||||
message = BitwardenString.we_couldnt_verify_the_servers_certificate.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
is LoginResult.NewDeviceVerification -> {
|
||||
showError(
|
||||
message = loginResult.errorMessage?.asText()
|
||||
?: R.string.login_sso_error.asText(),
|
||||
?: BitwardenString.login_sso_error.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
action: EnterpriseSignOnAction.Internal.OnSsoPrevalidationFailure,
|
||||
) {
|
||||
showError(
|
||||
message = action.message?.asText() ?: R.string.login_sso_error.asText(),
|
||||
message = action.message?.asText() ?: BitwardenString.login_sso_error.asText(),
|
||||
error = action.error,
|
||||
)
|
||||
}
|
||||
@@ -310,8 +312,8 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
title = R.string.log_in_denied.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -331,8 +333,8 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
title = R.string.internet_connection_required_title.asText(),
|
||||
message = R.string.internet_connection_required_message.asText(),
|
||||
title = BitwardenString.internet_connection_required_title.asText(),
|
||||
message = BitwardenString.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -342,8 +344,8 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
val organizationIdentifier = state.orgIdentifierInput
|
||||
if (organizationIdentifier.isBlank()) {
|
||||
showError(
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.org_identifier.asText(),
|
||||
message = BitwardenString.validation_field_required.asText(
|
||||
BitwardenString.org_identifier.asText(),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -406,7 +408,9 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
private fun checkOrganizationDomainSsoDetails() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(R.string.loading.asText()),
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
BitwardenString.loading.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
@@ -451,8 +455,8 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun showError(
|
||||
title: Text = R.string.an_error_has_occurred.asText(),
|
||||
message: Text = R.string.login_sso_error.asText(),
|
||||
title: Text = BitwardenString.an_error_has_occurred.asText(),
|
||||
message: Text = BitwardenString.login_sso_error.asText(),
|
||||
error: Throwable? = null,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
@@ -470,7 +474,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
R.string.logging_in.asText(),
|
||||
BitwardenString.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenClientCertificateDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
@@ -99,7 +99,7 @@ fun EnvironmentScreen(
|
||||
when (val dialog = state.dialog) {
|
||||
is EnvironmentState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
title = stringResource(id = R.string.an_error_has_occurred),
|
||||
title = stringResource(id = BitwardenString.an_error_has_occurred),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.DialogDismiss) }
|
||||
@@ -134,15 +134,15 @@ fun EnvironmentScreen(
|
||||
is EnvironmentState.DialogState.SystemCertificateWarningDialog -> {
|
||||
@Suppress("MaxLineLength")
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(R.string.warning),
|
||||
title = stringResource(BitwardenString.warning),
|
||||
message = stringResource(
|
||||
R.string.system_certificates_are_not_as_secure_as_importing_certificates_to_bitwarden,
|
||||
BitwardenString.system_certificates_are_not_as_secure_as_importing_certificates_to_bitwarden,
|
||||
),
|
||||
confirmButtonText = stringResource(R.string.continue_text),
|
||||
confirmButtonText = stringResource(BitwardenString.continue_text),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ConfirmChooseSystemCertificateClick) }
|
||||
},
|
||||
dismissButtonText = stringResource(R.string.cancel),
|
||||
dismissButtonText = stringResource(BitwardenString.cancel),
|
||||
onDismissClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.DialogDismiss) }
|
||||
},
|
||||
@@ -156,8 +156,8 @@ fun EnvironmentScreen(
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
confirmButtonText = stringResource(R.string.replace_certificate),
|
||||
dismissButtonText = stringResource(R.string.cancel),
|
||||
confirmButtonText = stringResource(BitwardenString.replace_certificate),
|
||||
dismissButtonText = stringResource(BitwardenString.cancel),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
@@ -185,16 +185,16 @@ fun EnvironmentScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.settings),
|
||||
title = stringResource(id = BitwardenString.settings),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.CloseClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.save),
|
||||
label = stringResource(id = BitwardenString.save),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.SaveClick) }
|
||||
},
|
||||
@@ -214,7 +214,7 @@ fun EnvironmentScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.self_hosted_environment),
|
||||
label = stringResource(id = BitwardenString.self_hosted_environment),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
@@ -224,10 +224,12 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.server_url),
|
||||
label = stringResource(id = BitwardenString.server_url),
|
||||
value = state.serverUrl,
|
||||
placeholder = "ex. https://bitwarden.company.com",
|
||||
supportingText = stringResource(id = R.string.self_hosted_environment_footer),
|
||||
supportingText = stringResource(
|
||||
id = BitwardenString.self_hosted_environment_footer,
|
||||
),
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ServerUrlChange(it)) }
|
||||
},
|
||||
@@ -251,7 +253,7 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.custom_environment),
|
||||
label = stringResource(id = BitwardenString.custom_environment),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
@@ -261,7 +263,7 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.web_vault_url),
|
||||
label = stringResource(id = BitwardenString.web_vault_url),
|
||||
value = state.webVaultServerUrl,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.WebVaultServerUrlChange(it)) }
|
||||
@@ -277,7 +279,7 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.api_url),
|
||||
label = stringResource(id = BitwardenString.api_url),
|
||||
value = state.apiServerUrl,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ApiServerUrlChange(it)) }
|
||||
@@ -293,7 +295,7 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.identity_url),
|
||||
label = stringResource(id = BitwardenString.identity_url),
|
||||
value = state.identityServerUrl,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.IdentityServerUrlChange(it)) }
|
||||
@@ -309,12 +311,12 @@ fun EnvironmentScreen(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.icons_url),
|
||||
label = stringResource(id = BitwardenString.icons_url),
|
||||
value = state.iconsServerUrl,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.IconsServerUrlChange(it)) }
|
||||
},
|
||||
supportingText = stringResource(id = R.string.custom_environment_footer),
|
||||
supportingText = stringResource(id = BitwardenString.custom_environment_footer),
|
||||
keyboardType = KeyboardType.Uri,
|
||||
textFieldTestTag = "IconsUrlEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
@@ -323,59 +325,57 @@ fun EnvironmentScreen(
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
if (state.showMutualTlsOptions) {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.client_certificate_mtls),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.client_certificate_mtls),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.certificate_alias),
|
||||
value = state.keyAlias,
|
||||
supportingText = stringResource(
|
||||
id = R.string.certificate_used_for_client_authentication,
|
||||
),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
cardStyle = CardStyle.Full,
|
||||
textFieldTestTag = "KeyAliasEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusProperties { canFocus = false }
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.certificate_alias),
|
||||
value = state.keyAlias,
|
||||
supportingText = stringResource(
|
||||
id = BitwardenString.certificate_used_for_client_authentication,
|
||||
),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
cardStyle = CardStyle.Full,
|
||||
textFieldTestTag = "KeyAliasEntry",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusProperties { canFocus = false }
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.import_certificate),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ImportCertificateClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.testTag("ImportCertificateButton"),
|
||||
)
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.import_certificate),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ImportCertificateClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.testTag("ImportCertificateButton"),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.choose_system_certificate),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ChooseSystemCertificateClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.testTag("ChooseSystemCertificateButton"),
|
||||
)
|
||||
}
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = BitwardenString.choose_system_certificate),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EnvironmentAction.ChooseSystemCertificateClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.testTag("ChooseSystemCertificateButton"),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
|
||||
@@ -11,13 +11,11 @@ import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidUri
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
@@ -28,7 +26,6 @@ import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -46,7 +43,6 @@ class EnvironmentViewModel @Inject constructor(
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val fileManager: FileManager,
|
||||
private val certificateManager: CertificateManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val snackbarRelayManager: SnackbarRelayManager,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<EnvironmentState, EnvironmentEvent, EnvironmentAction>(
|
||||
@@ -70,21 +66,13 @@ class EnvironmentViewModel @Inject constructor(
|
||||
keyAlias = keyAlias,
|
||||
keyHost = keyHost,
|
||||
dialog = null,
|
||||
showMutualTlsOptions = featureFlagManager.getFeatureFlag(FlagKey.MutualTls),
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach {
|
||||
savedStateHandle[KEY_STATE] = it
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.MutualTls)
|
||||
.map { EnvironmentAction.Internal.MutualTlsFeatureFlagUpdate(it) }
|
||||
.onEach(::handleAction)
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@@ -151,7 +139,7 @@ class EnvironmentViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
if (!urlsAreAllNullOrValid) {
|
||||
showErrorDialog(message = R.string.environment_page_urls_error.asText())
|
||||
showErrorDialog(message = BitwardenString.environment_page_urls_error.asText())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,7 +161,7 @@ class EnvironmentViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(message = R.string.environment_saved.asText()),
|
||||
data = BitwardenSnackbarData(message = BitwardenString.environment_saved.asText()),
|
||||
relay = SnackbarRelay.ENVIRONMENT_SAVED,
|
||||
)
|
||||
sendEvent(EnvironmentEvent.NavigateBack)
|
||||
@@ -196,9 +184,9 @@ class EnvironmentViewModel @Inject constructor(
|
||||
) {
|
||||
showSnackbar(
|
||||
message = if (action.success) {
|
||||
R.string.certificate_installed.asText()
|
||||
BitwardenString.certificate_installed.asText()
|
||||
} else {
|
||||
R.string.certificate_installation_failed.asText()
|
||||
BitwardenString.certificate_installation_failed.asText()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -275,10 +263,6 @@ class EnvironmentViewModel @Inject constructor(
|
||||
is EnvironmentAction.Internal.ImportKeyResultReceive -> {
|
||||
handleSaveKeyResultReceive(action)
|
||||
}
|
||||
|
||||
is EnvironmentAction.Internal.MutualTlsFeatureFlagUpdate -> {
|
||||
handleMutualTlsFeatureFlagUpdate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,8 +271,8 @@ class EnvironmentViewModel @Inject constructor(
|
||||
) {
|
||||
if (action.password.isBlank()) {
|
||||
showErrorDialog(
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.password.asText(),
|
||||
message = BitwardenString.validation_field_required.asText(
|
||||
BitwardenString.password.asText(),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -296,8 +280,8 @@ class EnvironmentViewModel @Inject constructor(
|
||||
|
||||
if (action.alias.isBlank()) {
|
||||
showErrorDialog(
|
||||
message = R.string.validation_field_required.asText(
|
||||
R.string.alias.asText(),
|
||||
message = BitwardenString.validation_field_required.asText(
|
||||
BitwardenString.alias.asText(),
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -308,9 +292,9 @@ class EnvironmentViewModel @Inject constructor(
|
||||
@Suppress("MaxLineLength")
|
||||
it.copy(
|
||||
dialog = EnvironmentState.DialogState.ConfirmOverwriteAlias(
|
||||
title = R.string.replace_existing_certificate.asText(),
|
||||
title = BitwardenString.replace_existing_certificate.asText(),
|
||||
message =
|
||||
R.string.a_certificate_with_the_alias_x_already_exists_do_you_want_to_replace_it
|
||||
BitwardenString.a_certificate_with_the_alias_x_already_exists_do_you_want_to_replace_it
|
||||
.asText(action.alias),
|
||||
triggeringAction = action,
|
||||
),
|
||||
@@ -344,33 +328,23 @@ class EnvironmentViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is ImportPrivateKeyResult.Error.UnsupportedKey -> {
|
||||
showSnackbar(message = R.string.unsupported_certificate_type.asText())
|
||||
showSnackbar(message = BitwardenString.unsupported_certificate_type.asText())
|
||||
}
|
||||
|
||||
is ImportPrivateKeyResult.Error.KeyStoreOperationFailed -> {
|
||||
showSnackbar(message = R.string.certificate_installation_failed.asText())
|
||||
showSnackbar(message = BitwardenString.certificate_installation_failed.asText())
|
||||
}
|
||||
|
||||
is ImportPrivateKeyResult.Error.UnrecoverableKey -> {
|
||||
showSnackbar(message = R.string.certificate_password_incorrect.asText())
|
||||
showSnackbar(message = BitwardenString.certificate_password_incorrect.asText())
|
||||
}
|
||||
|
||||
is ImportPrivateKeyResult.Error.InvalidCertificateChain -> {
|
||||
showSnackbar(message = R.string.invalid_certificate_chain.asText())
|
||||
showSnackbar(message = BitwardenString.invalid_certificate_chain.asText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMutualTlsFeatureFlagUpdate(
|
||||
action: EnvironmentAction.Internal.MutualTlsFeatureFlagUpdate,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
showMutualTlsOptions = action.enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChooseSystemCertificateClickAction() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@@ -395,7 +369,7 @@ class EnvironmentViewModel @Inject constructor(
|
||||
is PrivateKeyAliasSelectionResult.Error -> {
|
||||
sendEvent(
|
||||
EnvironmentEvent.ShowSnackbar(
|
||||
message = R.string.error_loading_certificate.asText(),
|
||||
message = BitwardenString.error_loading_certificate.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -441,7 +415,7 @@ class EnvironmentViewModel @Inject constructor(
|
||||
.fold(
|
||||
onFailure = {
|
||||
EnvironmentAction.Internal.ShowErrorDialog(
|
||||
message = R.string.unable_to_read_certificate.asText(),
|
||||
message = BitwardenString.unable_to_read_certificate.asText(),
|
||||
throwable = it,
|
||||
)
|
||||
},
|
||||
@@ -472,7 +446,6 @@ data class EnvironmentState(
|
||||
val iconsServerUrl: String,
|
||||
val keyAlias: String,
|
||||
val dialog: DialogState?,
|
||||
val showMutualTlsOptions: Boolean,
|
||||
// internal
|
||||
private val keyHost: MutualTlsKeyHost?,
|
||||
) : Parcelable {
|
||||
@@ -692,12 +665,5 @@ sealed class EnvironmentAction {
|
||||
data class ImportKeyResultReceive(
|
||||
val result: ImportPrivateKeyResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the mutual TLS feature flag was updated.
|
||||
*/
|
||||
data class MutualTlsFeatureFlagUpdate(
|
||||
val enabled: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
|
||||
/**
|
||||
@@ -63,11 +63,11 @@ fun ExpiredRegistrationLinkScreen(
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
title = stringResource(id = BitwardenString.create_account),
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = sendCloseClicked,
|
||||
),
|
||||
)
|
||||
@@ -104,7 +104,7 @@ private fun ExpiredRegistrationLinkContent(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.expired_link),
|
||||
text = stringResource(BitwardenString.expired_link),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -114,7 +114,7 @@ private fun ExpiredRegistrationLinkContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.please_restart_registration_or_try_logging_in),
|
||||
text = stringResource(BitwardenString.please_restart_registration_or_try_logging_in),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -124,7 +124,7 @@ private fun ExpiredRegistrationLinkContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.restart_registration),
|
||||
label = stringResource(BitwardenString.restart_registration),
|
||||
onClick = onNavigateToStartRegistration,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -132,7 +132,7 @@ private fun ExpiredRegistrationLinkContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.log_in_verb),
|
||||
label = stringResource(id = BitwardenString.log_in_verb),
|
||||
onClick = onNavigateToLogin,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -45,8 +45,8 @@ import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
|
||||
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenPlaceholderAccountActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
@@ -88,12 +88,12 @@ fun LandingScreen(
|
||||
when (val dialog = state.dialog) {
|
||||
is LandingState.DialogState.AccountAlreadyAdded -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.account_already_added),
|
||||
title = stringResource(id = BitwardenString.account_already_added),
|
||||
message = stringResource(
|
||||
id = R.string.switch_to_already_added_account_confirmation,
|
||||
id = BitwardenString.switch_to_already_added_account_confirmation,
|
||||
),
|
||||
confirmButtonText = stringResource(id = R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
@@ -114,7 +114,7 @@ fun LandingScreen(
|
||||
|
||||
is LandingState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
title = stringResource(id = R.string.an_error_has_occurred),
|
||||
title = stringResource(id = BitwardenString.an_error_has_occurred),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LandingAction.DialogDismiss) }
|
||||
@@ -234,7 +234,7 @@ private fun LandingScreenContent(
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.login_to_bitwarden),
|
||||
text = stringResource(id = BitwardenString.login_to_bitwarden),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -251,15 +251,15 @@ private fun LandingScreenContent(
|
||||
.fillMaxWidth(),
|
||||
value = state.emailInput,
|
||||
onValueChange = onEmailInputChange,
|
||||
label = stringResource(id = R.string.email_address),
|
||||
label = stringResource(id = BitwardenString.email_address),
|
||||
keyboardType = KeyboardType.Email,
|
||||
textFieldTestTag = "EmailAddressEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
supportingContentPadding = PaddingValues(),
|
||||
supportingContent = {
|
||||
EnvironmentSelector(
|
||||
labelText = stringResource(id = R.string.logging_in_on_with_colon),
|
||||
dialogTitle = stringResource(id = R.string.logging_in_on),
|
||||
labelText = stringResource(id = BitwardenString.logging_in_on_with_colon),
|
||||
dialogTitle = stringResource(id = BitwardenString.logging_in_on),
|
||||
selectedOption = state.selectedEnvironmentType,
|
||||
onOptionSelected = onEnvironmentTypeSelect,
|
||||
isHelpEnabled = false,
|
||||
@@ -274,7 +274,7 @@ private fun LandingScreenContent(
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.remember_email),
|
||||
label = stringResource(id = BitwardenString.remember_email),
|
||||
isChecked = state.isRememberEmailEnabled,
|
||||
onCheckedChange = onRememberMeToggle,
|
||||
cardStyle = CardStyle.Full,
|
||||
@@ -287,7 +287,7 @@ private fun LandingScreenContent(
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = onContinueClick,
|
||||
isEnabled = state.isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
@@ -307,13 +307,13 @@ private fun LandingScreenContent(
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.new_to_bitwarden),
|
||||
text = stringResource(id = BitwardenString.new_to_bitwarden),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
)
|
||||
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.create_an_account),
|
||||
label = stringResource(id = BitwardenString.create_an_account),
|
||||
onClick = onCreateAccountClick,
|
||||
modifier = Modifier
|
||||
.testTag("CreateAccountLabel"),
|
||||
@@ -321,7 +321,7 @@ private fun LandingScreenContent(
|
||||
}
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.app_settings),
|
||||
label = stringResource(id = BitwardenString.app_settings),
|
||||
onClick = onAppSettingsClick,
|
||||
icon = rememberVectorPainter(id = BitwardenDrawable.ic_cog),
|
||||
modifier = Modifier
|
||||
|
||||
@@ -7,9 +7,9 @@ import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isValidEmail
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
@@ -187,7 +187,7 @@ class LandingViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = LandingState.DialogState.Error(
|
||||
message = R.string.invalid_email.asText(),
|
||||
message = BitwardenString.invalid_email.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
|
||||
@@ -119,7 +120,7 @@ fun LoginScreen(
|
||||
title = stringResource(id = R.string.app_name),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.CloseButtonClick) }
|
||||
},
|
||||
@@ -130,10 +131,10 @@ fun LoginScreen(
|
||||
)
|
||||
}
|
||||
BitwardenOverflowActionItem(
|
||||
contentDescription = stringResource(R.string.more),
|
||||
contentDescription = stringResource(BitwardenString.more),
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.get_password_hint),
|
||||
text = stringResource(id = BitwardenString.get_password_hint),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.MasterPasswordHintClick) }
|
||||
},
|
||||
@@ -235,12 +236,12 @@ private fun LoginScreenContent(
|
||||
autoFocus = true,
|
||||
value = state.passwordInput,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
label = stringResource(id = R.string.master_password),
|
||||
label = stringResource(id = BitwardenString.master_password),
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
supportingContentPadding = PaddingValues(),
|
||||
supportingContent = {
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = R.string.get_master_passwordword_hint),
|
||||
label = stringResource(id = BitwardenString.get_master_passwordword_hint),
|
||||
onClick = onMasterPasswordClick,
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
innerPadding = PaddingValues(all = 16.dp),
|
||||
@@ -260,7 +261,7 @@ private fun LoginScreenContent(
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.log_in_with_master_password),
|
||||
label = stringResource(id = BitwardenString.log_in_with_master_password),
|
||||
onClick = onLoginButtonClick,
|
||||
isEnabled = state.isLoginButtonEnabled,
|
||||
modifier = Modifier
|
||||
@@ -273,7 +274,7 @@ private fun LoginScreenContent(
|
||||
|
||||
if (state.shouldShowLoginWithDevice) {
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.log_in_with_device),
|
||||
label = stringResource(id = BitwardenString.log_in_with_device),
|
||||
icon = rememberVectorPainter(id = BitwardenDrawable.ic_mobile_small),
|
||||
onClick = onLoginWithDeviceClick,
|
||||
modifier = Modifier
|
||||
@@ -286,7 +287,7 @@ private fun LoginScreenContent(
|
||||
}
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.log_in_sso),
|
||||
label = stringResource(id = BitwardenString.log_in_sso),
|
||||
icon = rememberVectorPainter(id = BitwardenDrawable.ic_enterprise_small),
|
||||
onClick = onSingleSignOnClick,
|
||||
modifier = Modifier
|
||||
@@ -299,7 +300,7 @@ private fun LoginScreenContent(
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.logging_in_as_x_on_y,
|
||||
id = BitwardenString.logging_in_as_x_on_y,
|
||||
state.emailAddress,
|
||||
state.environmentLabel,
|
||||
),
|
||||
@@ -313,7 +314,7 @@ private fun LoginScreenContent(
|
||||
)
|
||||
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = R.string.not_you),
|
||||
label = stringResource(id = BitwardenString.not_you),
|
||||
onClick = onNotYouButtonClick,
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
|
||||
@@ -8,9 +8,9 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
@@ -52,7 +52,7 @@ class LoginViewModel @Inject constructor(
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
environmentLabel = environmentRepository.environment.label,
|
||||
dialogState = LoginState.DialogState.Loading(R.string.loading.asText()),
|
||||
dialogState = LoginState.DialogState.Loading(BitwardenString.loading.asText()),
|
||||
captchaToken = args.captchaToken,
|
||||
accountSummaries = authRepository
|
||||
.userStateFlow
|
||||
@@ -178,8 +178,8 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString
|
||||
.this_account_will_soon_be_deleted_log_in_at_x_to_continue_using_bitwarden
|
||||
.asText(vaultUrl.toUriOrNull()?.host ?: vaultUrl),
|
||||
),
|
||||
@@ -204,9 +204,9 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = loginResult.errorMessage?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
error = loginResult.error,
|
||||
),
|
||||
)
|
||||
@@ -217,8 +217,8 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -232,8 +232,8 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.we_couldnt_verify_the_servers_certificate.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -262,8 +262,8 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = R.string.log_in_denied.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -294,7 +294,7 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
message = BitwardenString.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
@@ -86,7 +86,7 @@ fun LoginWithDeviceScreen(
|
||||
title = state.toolbarTitle(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginWithDeviceAction.CloseButtonClick) }
|
||||
},
|
||||
@@ -163,7 +163,7 @@ private fun LoginWithDeviceScreenContent(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.fingerprint_phrase),
|
||||
text = stringResource(id = BitwardenString.fingerprint_phrase),
|
||||
textAlign = TextAlign.Start,
|
||||
style = BitwardenTheme.typography.titleLarge,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -202,7 +202,7 @@ private fun LoginWithDeviceScreenContent(
|
||||
} else {
|
||||
BitwardenClickableText(
|
||||
modifier = Modifier.testTag("ResendNotificationButton"),
|
||||
label = stringResource(id = R.string.resend_notification),
|
||||
label = stringResource(id = BitwardenString.resend_notification),
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
onClick = onResendNotificationClick,
|
||||
@@ -225,7 +225,7 @@ private fun LoginWithDeviceScreenContent(
|
||||
|
||||
BitwardenClickableText(
|
||||
modifier = Modifier.testTag("ViewAllLoginOptionsButton"),
|
||||
label = stringResource(id = R.string.view_all_login_options),
|
||||
label = stringResource(id = BitwardenString.view_all_login_options),
|
||||
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
onClick = onViewAllLogInOptionsClick,
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
@@ -154,8 +154,8 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
@@ -175,7 +175,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
),
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.login_request_has_already_expired.asText(),
|
||||
message = BitwardenString.login_request_has_already_expired.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -191,8 +191,8 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.log_in_denied.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -234,11 +234,11 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = loginResult
|
||||
.errorMessage
|
||||
?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
error = loginResult.error,
|
||||
),
|
||||
)
|
||||
@@ -249,8 +249,8 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
@@ -259,7 +259,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
|
||||
is LoginResult.Success -> {
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(message = R.string.login_approved.asText()),
|
||||
data = BitwardenSnackbarData(message = BitwardenString.login_approved.asText()),
|
||||
relay = SnackbarRelay.LOGIN_SUCCESS,
|
||||
)
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
@@ -269,8 +269,8 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.we_couldnt_verify_the_servers_certificate.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.we_couldnt_verify_the_servers_certificate.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -280,11 +280,11 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = loginResult
|
||||
.errorMessage
|
||||
?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -301,7 +301,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
message = BitwardenString.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -381,9 +381,9 @@ data class LoginWithDeviceState(
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_with_device.asText()
|
||||
-> BitwardenString.log_in_with_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> R.string.log_in_initiated.asText()
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> BitwardenString.log_in_initiated.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,10 +418,10 @@ data class LoginWithDeviceState(
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_initiated.asText()
|
||||
-> BitwardenString.log_in_initiated.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.admin_approval_requested.asText()
|
||||
-> BitwardenString.admin_approval_requested.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,10 +431,10 @@ data class LoginWithDeviceState(
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.a_notification_has_been_sent_to_your_device.asText()
|
||||
-> BitwardenString.a_notification_has_been_sent_to_your_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.your_request_has_been_sent_to_your_admin.asText()
|
||||
-> BitwardenString.your_request_has_been_sent_to_your_admin.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,10 +445,10 @@ data class LoginWithDeviceState(
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.please_make_sure_your_vault_is_unlocked_and_the_fingerprint_phrase_matches_on_the_other_device.asText()
|
||||
-> BitwardenString.please_make_sure_your_vault_is_unlocked_and_the_fingerprint_phrase_matches_on_the_other_device.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
-> R.string.you_will_be_notified_once_approved.asText()
|
||||
-> BitwardenString.you_will_be_notified_once_approved.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,9 +459,9 @@ data class LoginWithDeviceState(
|
||||
get() = when (loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> R.string.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option.asText()
|
||||
-> BitwardenString.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option.asText()
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> R.string.trouble_logging_in.asText()
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> BitwardenString.trouble_logging_in.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,8 +34,8 @@ import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
@@ -155,19 +155,23 @@ private fun MasterPasswordGeneratorContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.generate_button_label),
|
||||
label = stringResource(BitwardenString.generate_button_label),
|
||||
onClick = onGenerateNewPassword,
|
||||
icon = rememberVectorPainter(id = BitwardenDrawable.ic_generate),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.write_this_password_down_and_keep_it_somewhere_safe),
|
||||
text = stringResource(
|
||||
BitwardenString.write_this_password_down_and_keep_it_somewhere_safe,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
BitwardenClickableText(
|
||||
label = stringResource(R.string.learn_about_other_ways_to_prevent_account_lockout),
|
||||
label = stringResource(
|
||||
BitwardenString.learn_about_other_ways_to_prevent_account_lockout,
|
||||
),
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
onClick = onLearnToPreventLockout,
|
||||
innerPadding = PaddingValues(horizontal = 0.dp, vertical = 4.dp),
|
||||
@@ -183,14 +187,14 @@ private fun MasterPasswordGeneratorTopBar(
|
||||
onSaveClick: () -> Unit,
|
||||
) {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(R.string.generate_master_password),
|
||||
title = stringResource(BitwardenString.generate_master_password),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
onNavigationIconClick = onBackClick,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.save),
|
||||
label = stringResource(id = BitwardenString.save),
|
||||
onClick = onSaveClick,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -6,9 +6,9 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.generators.PassphraseGeneratorRequest
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
@@ -94,7 +94,7 @@ class MasterPasswordGeneratorViewModel @Inject constructor(
|
||||
is GeneratedPassphraseResult.InvalidRequest -> {
|
||||
sendEvent(
|
||||
MasterPasswordGeneratorEvent.ShowSnackbar(
|
||||
R.string.an_error_has_occurred.asText(),
|
||||
BitwardenString.an_error_has_occurred.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.content.model.ContentBlockData
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
@@ -63,10 +63,10 @@ fun MasterPasswordGuidanceScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.master_password),
|
||||
title = stringResource(id = BitwardenString.master_password),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(MasterPasswordGuidanceAction.CloseAction)
|
||||
@@ -99,7 +99,7 @@ private fun MasterPasswordGuidanceContent(
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.a_secure_memorable_password),
|
||||
text = stringResource(BitwardenString.a_secure_memorable_password),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -110,7 +110,7 @@ private fun MasterPasswordGuidanceContent(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.one_of_the_best_ways_to_create_a_secure_and_memorable_password,
|
||||
BitwardenString.one_of_the_best_ways_to_create_a_secure_and_memorable_password,
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
@@ -137,25 +137,25 @@ private fun MasterPasswordGuidanceContentBlocks(modifier: Modifier = Modifier) {
|
||||
BitwardenContentCard(
|
||||
contentItems = persistentListOf(
|
||||
ContentBlockData(
|
||||
headerText = stringResource(R.string.choose_three_or_four_random_words)
|
||||
headerText = stringResource(BitwardenString.choose_three_or_four_random_words)
|
||||
.toAnnotatedString(),
|
||||
subtitleText = annotatedStringResource(
|
||||
id = R.string.pick_three_or_four_random_unrelated_words,
|
||||
id = BitwardenString.pick_three_or_four_random_unrelated_words,
|
||||
),
|
||||
iconVectorResource = BitwardenDrawable.ic_number1,
|
||||
),
|
||||
ContentBlockData(
|
||||
headerText = stringResource(R.string.combine_those_words_together)
|
||||
headerText = stringResource(BitwardenString.combine_those_words_together)
|
||||
.toAnnotatedString(),
|
||||
subtitleText = annotatedStringResource(
|
||||
id = R.string.put_the_words_together_in_any_order_to_form_your_passphrase,
|
||||
id = BitwardenString.put_the_words_together_in_any_order_to_form_your_passphrase,
|
||||
),
|
||||
iconVectorResource = BitwardenDrawable.ic_number2,
|
||||
),
|
||||
ContentBlockData(
|
||||
headerText = stringResource(R.string.make_it_yours).toAnnotatedString(),
|
||||
headerText = stringResource(BitwardenString.make_it_yours).toAnnotatedString(),
|
||||
subtitleText = annotatedStringResource(
|
||||
id = R.string.add_a_number_or_symbol_to_make_it_even_stronger,
|
||||
id = BitwardenString.add_a_number_or_symbol_to_make_it_even_stronger,
|
||||
),
|
||||
iconVectorResource = BitwardenDrawable.ic_number3,
|
||||
),
|
||||
@@ -171,8 +171,8 @@ private fun NeedSomeInspirationCard(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenActionCard(
|
||||
cardTitle = stringResource(R.string.need_some_inspiration),
|
||||
actionText = stringResource(R.string.check_out_the_passphrase_generator),
|
||||
cardTitle = stringResource(BitwardenString.need_some_inspiration),
|
||||
actionText = stringResource(BitwardenString.check_out_the_passphrase_generator),
|
||||
onActionClick = onActionClicked,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
@@ -53,8 +53,8 @@ fun MasterPasswordHintScreen(
|
||||
when (val dialogState = state.dialog) {
|
||||
is MasterPasswordHintState.DialogState.PasswordHintSent -> {
|
||||
BitwardenBasicDialog(
|
||||
title = stringResource(id = R.string.password_hint),
|
||||
message = stringResource(id = R.string.password_hint_alert),
|
||||
title = stringResource(id = BitwardenString.password_hint),
|
||||
message = stringResource(id = BitwardenString.password_hint_alert),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MasterPasswordHintAction.DismissDialog) }
|
||||
},
|
||||
@@ -70,7 +70,7 @@ fun MasterPasswordHintScreen(
|
||||
title = dialogState
|
||||
.title
|
||||
?.invoke()
|
||||
?: stringResource(id = R.string.an_error_has_occurred),
|
||||
?: stringResource(id = BitwardenString.an_error_has_occurred),
|
||||
message = dialogState.message(),
|
||||
throwable = dialogState.error,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
@@ -89,16 +89,16 @@ fun MasterPasswordHintScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.password_hint),
|
||||
title = stringResource(id = BitwardenString.password_hint),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MasterPasswordHintAction.CloseClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.submit),
|
||||
label = stringResource(id = BitwardenString.submit),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MasterPasswordHintAction.SubmitClick) }
|
||||
},
|
||||
@@ -120,10 +120,10 @@ fun MasterPasswordHintScreen(
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MasterPasswordHintAction.EmailInputChange(it)) }
|
||||
},
|
||||
label = stringResource(id = R.string.email_address),
|
||||
label = stringResource(id = BitwardenString.email_address),
|
||||
keyboardType = KeyboardType.Email,
|
||||
textFieldTestTag = "MasterPasswordHintEmailField",
|
||||
supportingText = stringResource(id = R.string.enter_email_for_hint),
|
||||
supportingText = stringResource(id = BitwardenString.enter_email_for_hint),
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
@@ -68,8 +68,8 @@ class MasterPasswordHintViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MasterPasswordHintState.DialogState.Error(
|
||||
title = R.string.internet_connection_required_title.asText(),
|
||||
message = R.string.internet_connection_required_message.asText(),
|
||||
title = BitwardenString.internet_connection_required_title.asText(),
|
||||
message = BitwardenString.internet_connection_required_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -80,9 +80,9 @@ class MasterPasswordHintViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MasterPasswordHintState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.email_address.asText()),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.email_address.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -90,11 +90,11 @@ class MasterPasswordHintViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
if (!email.contains("@")) {
|
||||
val errorMessage = R.string.invalid_email.asText()
|
||||
val errorMessage = BitwardenString.invalid_email.asText()
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MasterPasswordHintState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = errorMessage,
|
||||
),
|
||||
)
|
||||
@@ -105,7 +105,7 @@ class MasterPasswordHintViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MasterPasswordHintState.DialogState.Loading(
|
||||
R.string.submitting.asText(),
|
||||
BitwardenString.submitting.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -130,9 +130,9 @@ class MasterPasswordHintViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MasterPasswordHintState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = result.message?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
?: BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -28,8 +28,8 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.content.model.ContentBlockData
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -57,10 +57,10 @@ fun PreventAccountLockoutScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(R.string.prevent_account_lockout),
|
||||
title = stringResource(BitwardenString.prevent_account_lockout),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(PreventAccountLockoutAction.CloseClickAction)
|
||||
@@ -83,7 +83,7 @@ private fun PreventAccountLockoutContent(modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.never_lose_access_to_your_vault),
|
||||
text = stringResource(BitwardenString.never_lose_access_to_your_vault),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
@@ -94,7 +94,7 @@ private fun PreventAccountLockoutContent(modifier: Modifier = Modifier) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.the_best_way_to_make_sure_you_can_always_access_your_vault,
|
||||
BitwardenString.the_best_way_to_make_sure_you_can_always_access_your_vault,
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
@@ -105,15 +105,15 @@ private fun PreventAccountLockoutContent(modifier: Modifier = Modifier) {
|
||||
BitwardenContentCard(
|
||||
contentItems = persistentListOf(
|
||||
ContentBlockData(
|
||||
headerText = stringResource(R.string.create_a_hint),
|
||||
headerText = stringResource(BitwardenString.create_a_hint),
|
||||
subtitleText = stringResource(
|
||||
R.string.your_hint_will_be_send_to_you_via_email_when_you_request_it,
|
||||
BitwardenString.your_hint_will_be_send_to_you_via_email_when_you_request_it,
|
||||
),
|
||||
iconVectorResource = BitwardenDrawable.ic_light_bulb,
|
||||
),
|
||||
ContentBlockData(
|
||||
headerText = stringResource(R.string.write_your_password_down),
|
||||
subtitleText = stringResource(R.string.keep_it_secret_keep_it_safe),
|
||||
headerText = stringResource(BitwardenString.write_your_password_down),
|
||||
subtitleText = stringResource(BitwardenString.keep_it_secret_keep_it_safe),
|
||||
iconVectorResource = BitwardenDrawable.ic_pencil,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -28,9 +28,9 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
@@ -65,7 +65,7 @@ fun RemovePasswordScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.remove_master_password),
|
||||
title = stringResource(id = BitwardenString.remove_master_password),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
)
|
||||
@@ -149,7 +149,7 @@ private fun RemovePasswordScreenContent(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
label = stringResource(id = BitwardenString.master_password),
|
||||
value = state.input,
|
||||
onValueChange = onInputChanged,
|
||||
showPasswordTestTag = "PasswordVisibilityToggle",
|
||||
@@ -163,7 +163,7 @@ private fun RemovePasswordScreenContent(
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
label = stringResource(id = BitwardenString.continue_text),
|
||||
onClick = onContinueClick,
|
||||
isEnabled = state.input.isNotEmpty(),
|
||||
modifier = Modifier
|
||||
@@ -175,7 +175,7 @@ private fun RemovePasswordScreenContent(
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.leave_organization),
|
||||
label = stringResource(id = BitwardenString.leave_organization),
|
||||
onClick = onLeaveOrganizationClick,
|
||||
modifier = Modifier
|
||||
.testTag("LeaveOrganizationButton")
|
||||
@@ -209,10 +209,10 @@ private fun RemovePasswordDialogs(
|
||||
|
||||
is RemovePasswordState.DialogState.LeaveConfirmationPrompt -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.leave_organization),
|
||||
title = stringResource(id = BitwardenString.leave_organization),
|
||||
message = dialogState.message.invoke(),
|
||||
confirmButtonText = stringResource(id = R.string.confirm),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
confirmButtonText = stringResource(id = BitwardenString.confirm),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = onConfirmLeaveClick,
|
||||
onDismissClick = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
||||
@@ -5,9 +5,9 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
@@ -36,10 +36,10 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
|
||||
RemovePasswordState(
|
||||
input = "",
|
||||
description = R.string.password_no_longer_required_confirm_domain.asText(),
|
||||
labelOrg = R.string.key_connector_organization.asText(),
|
||||
description = BitwardenString.password_no_longer_required_confirm_domain.asText(),
|
||||
labelOrg = BitwardenString.key_connector_organization.asText(),
|
||||
orgName = org?.name?.asText(),
|
||||
labelDomain = R.string.key_connector_domain.asText(),
|
||||
labelDomain = BitwardenString.key_connector_domain.asText(),
|
||||
domainName = org?.keyConnectorUrl?.asText(),
|
||||
dialogState = null,
|
||||
organizationId = org?.id.orNullIfBlank(),
|
||||
@@ -71,7 +71,7 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.LeaveConfirmationPrompt(
|
||||
message = R.string.leave_organization_name.asText(state.orgName ?: ""),
|
||||
message = BitwardenString.leave_organization_name.asText(state.orgName ?: ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -82,9 +82,9 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.master_password.asText()),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.master_password.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Loading(
|
||||
title = R.string.deleting.asText(),
|
||||
title = BitwardenString.deleting.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -119,8 +119,8 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
@@ -131,8 +131,8 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Loading(
|
||||
title = R.string.loading.asText(),
|
||||
title = BitwardenString.loading.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -173,8 +173,8 @@ class RemovePasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = RemovePasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -35,8 +35,8 @@ import com.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActio
|
||||
import com.bitwarden.ui.platform.components.appbar.model.OverflowMenuItemData
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthIndicator
|
||||
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState
|
||||
@@ -94,10 +94,10 @@ fun ResetPasswordScreen(
|
||||
}
|
||||
if (shouldShowLogoutConfirmationDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.log_out),
|
||||
message = stringResource(id = R.string.logout_confirmation),
|
||||
confirmButtonText = stringResource(id = R.string.yes),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
title = stringResource(id = BitwardenString.log_out),
|
||||
message = stringResource(id = BitwardenString.logout_confirmation),
|
||||
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = {
|
||||
shouldShowLogoutConfirmationDialog = false
|
||||
onLogoutClicked()
|
||||
@@ -114,22 +114,22 @@ fun ResetPasswordScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.update_master_password),
|
||||
title = stringResource(id = BitwardenString.update_master_password),
|
||||
navigationIcon = null,
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.save),
|
||||
label = stringResource(id = BitwardenString.save),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ResetPasswordAction.SaveClick) }
|
||||
},
|
||||
modifier = Modifier.testTag("SaveButton"),
|
||||
)
|
||||
BitwardenOverflowActionItem(
|
||||
contentDescription = stringResource(R.string.more),
|
||||
contentDescription = stringResource(BitwardenString.more),
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(R.string.log_out),
|
||||
text = stringResource(BitwardenString.log_out),
|
||||
onClick = { shouldShowLogoutConfirmationDialog = true },
|
||||
),
|
||||
),
|
||||
@@ -179,9 +179,9 @@ private fun ResetPasswordScreenContent(
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
val instructionsTextId =
|
||||
if (state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
|
||||
R.string.update_weak_master_password_warning
|
||||
BitwardenString.update_weak_master_password_warning
|
||||
} else {
|
||||
R.string.update_master_password_warning
|
||||
BitwardenString.update_master_password_warning
|
||||
}
|
||||
BitwardenInfoCalloutCard(
|
||||
text = stringResource(id = instructionsTextId),
|
||||
@@ -194,7 +194,7 @@ private fun ResetPasswordScreenContent(
|
||||
|
||||
if (state.resetReason == ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN) {
|
||||
val passwordPolicyContent = listOf(
|
||||
stringResource(id = R.string.master_password_policy_in_effect),
|
||||
stringResource(id = BitwardenString.master_password_policy_in_effect),
|
||||
)
|
||||
.plus(state.policies.map { it() })
|
||||
.joinToString("\n • ")
|
||||
@@ -207,7 +207,7 @@ private fun ResetPasswordScreenContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.current_master_password_required),
|
||||
label = stringResource(id = BitwardenString.current_master_password_required),
|
||||
value = state.currentPasswordInput,
|
||||
onValueChange = onCurrentPasswordInputChanged,
|
||||
passwordFieldTestTag = "MasterPasswordField",
|
||||
@@ -220,7 +220,7 @@ private fun ResetPasswordScreenContent(
|
||||
|
||||
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.new_master_password_required),
|
||||
label = stringResource(id = BitwardenString.new_master_password_required),
|
||||
value = state.passwordInput,
|
||||
onValueChange = onPasswordInputChanged,
|
||||
showPassword = isPasswordVisible,
|
||||
@@ -246,7 +246,7 @@ private fun ResetPasswordScreenContent(
|
||||
)
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_new_master_password_required),
|
||||
label = stringResource(id = BitwardenString.retype_new_master_password_required),
|
||||
value = state.retypePasswordInput,
|
||||
onValueChange = onRetypePasswordInputChanged,
|
||||
showPassword = isPasswordVisible,
|
||||
@@ -259,24 +259,26 @@ private fun ResetPasswordScreenContent(
|
||||
)
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.new_master_password_hint),
|
||||
label = stringResource(id = BitwardenString.new_master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = onPasswordHintInputChanged,
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.bitwarden_cannot_reset_a_lost_or_forgotten_master_password,
|
||||
BitwardenString
|
||||
.bitwarden_cannot_reset_a_lost_or_forgotten_master_password,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
BitwardenHyperTextLink(
|
||||
annotatedResId = R.string.learn_about_ways_to_prevent_account_lockout,
|
||||
annotatedResId =
|
||||
BitwardenString.learn_about_ways_to_prevent_account_lockout,
|
||||
annotationKey = "onPreventAccountLockout",
|
||||
accessibilityString = stringResource(
|
||||
R.string.learn_about_ways_to_prevent_account_lockout,
|
||||
BitwardenString.learn_about_ways_to_prevent_account_lockout,
|
||||
),
|
||||
onClick = onLearnToPreventLockout,
|
||||
)
|
||||
|
||||
@@ -5,9 +5,9 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -173,9 +173,9 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.validation_field_required
|
||||
.asText(R.string.master_password.asText()),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.master_password.asText()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -198,8 +198,8 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_length_val_message_x
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_length_val_message_x
|
||||
.asText(MIN_PASSWORD_LENGTH),
|
||||
),
|
||||
)
|
||||
@@ -291,8 +291,8 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
@@ -317,8 +317,8 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
@@ -331,8 +331,8 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.invalid_master_password.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.invalid_master_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -355,8 +355,9 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.master_password_policy_validation_title.asText(),
|
||||
message = R.string.master_password_policy_validation_message.asText(),
|
||||
title = BitwardenString.master_password_policy_validation_title.asText(),
|
||||
message = BitwardenString.master_password_policy_validation_message
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -384,8 +385,8 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.master_password_confirmation_val_message.asText(),
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.master_password_confirmation_val_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -400,7 +401,7 @@ class ResetPasswordViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = ResetPasswordState.DialogState.Loading(
|
||||
message = R.string.updating_password.asText(),
|
||||
message = BitwardenString.updating_password.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user