mirror of
https://github.com/bitwarden/android.git
synced 2026-05-18 05:55:39 -05:00
Compare commits
1 Commits
PM-36952-u
...
renovate/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91343236e |
@@ -12,12 +12,12 @@ runs:
|
||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
4
.github/workflows/_version.yml
vendored
4
.github/workflows/_version.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Check out repository
|
||||
if: ${{ !inputs.skip_checkout || false }}
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload version info artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: version-info
|
||||
path: version_info.json
|
||||
|
||||
10
.github/workflows/build-authenticator.yml
vendored
10
.github/workflows/build-authenticator.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.bitwarden.authenticator.aab
|
||||
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.bitwarden.authenticator.apk
|
||||
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: authenticator-android-apk-sha256.txt
|
||||
path: ./authenticator-android-apk-sha256.txt
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: authenticator-android-aab-sha256.txt
|
||||
path: ./authenticator-android-aab-sha256.txt
|
||||
|
||||
6
.github/workflows/build-testharness.yml
vendored
6
.github/workflows/build-testharness.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
run: ./gradlew :testharness:assembleDebug
|
||||
|
||||
- name: Upload Test Harness APK
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev-debug.apk
|
||||
path: testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
> ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
|
||||
- name: Upload Test Harness SHA file
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
path: ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
|
||||
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.aab
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.apk
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload to GitHub Artifacts - dev.apk
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -271,7 +271,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.apk-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.aab-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - debug.apk-sha256.txt
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -424,7 +424,7 @@ jobs:
|
||||
keyPassword:$FDROID_BETA_KEY_PASSWORD
|
||||
|
||||
- name: Upload to GitHub Artifacts - fdroid.apk
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -436,14 +436,14 @@ jobs:
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload to GitHub Artifacts - fdroid.apk-sha256.txt
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.fdroid.apk
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
@@ -455,7 +455,7 @@ jobs:
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.fdroid.apk-sha256.txt
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
|
||||
6
.github/workflows/crowdin-pull.yml
vendored
6
.github/workflows/crowdin-pull.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
permission-pull-requests: write # for creating pull request
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.CROWDIN-API-TOKEN }}
|
||||
|
||||
4
.github/workflows/crowdin-push.yml
vendored
4
.github/workflows/crowdin-push.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
secrets: "CROWDIN-API-TOKEN"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.CROWDIN-API-TOKEN }}
|
||||
|
||||
2
.github/workflows/github-release.yml
vendored
2
.github/workflows/github-release.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
4
.github/workflows/publish-store.yml
vendored
4
.github/workflows/publish-store.yml
vendored
@@ -73,11 +73,11 @@ jobs:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
2
.github/workflows/release-branch.yml
vendored
2
.github/workflows/release-branch.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
2
.github/workflows/review-code.yml
vendored
2
.github/workflows/review-code.yml
vendored
@@ -2,7 +2,7 @@ name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened, ready_for_review, reopened, synchronize]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/sdlc-label-pr.yml
vendored
2
.github/workflows/sdlc-label-pr.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
6
.github/workflows/sdlc-sdk-update.yml
vendored
6
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
disable_search: true
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports-${{ matrix.group }}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
android:name="android.hardware.nfc"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
@@ -41,8 +41,6 @@ import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCooki
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.localNetworkAccessDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.navigateToLocalNetworkAccess
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
@@ -153,10 +151,6 @@ class MainActivity : AppCompatActivity() {
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
localNetworkAccessDestination(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,9 +235,6 @@ class MainActivity : AppCompatActivity() {
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
|
||||
MainEvent.NavigateToLocalNetworkAccess -> {
|
||||
navController.navigateToLocalNetworkAccess()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
|
||||
@@ -37,7 +37,6 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
|
||||
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.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
@@ -81,7 +80,6 @@ class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
networkPermissionManager: NetworkPermissionManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
@@ -170,13 +168,6 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
networkPermissionManager
|
||||
.isLocalNetworkAccessRequiredStateFlow
|
||||
.filter { it }
|
||||
.map { MainAction.Internal.LocalNetworkAccessRequired }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
cookieAcquisitionRequestManager
|
||||
.cookieAcquisitionRequestFlow
|
||||
.filterNotNull()
|
||||
@@ -232,7 +223,6 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
|
||||
is MainAction.Internal.CookieAcquisitionReady -> handleCookieAcquisitionReady()
|
||||
is MainAction.Internal.LocalNetworkAccessRequired -> handleLocalNetworkAccessRequired()
|
||||
is MainAction.Internal.ResizeHasBeenRequested -> handleResizeHasBeenRequested()
|
||||
}
|
||||
}
|
||||
@@ -314,10 +304,6 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.NavigateToCookieAcquisition)
|
||||
}
|
||||
|
||||
private fun handleLocalNetworkAccessRequired() {
|
||||
sendEvent(MainEvent.NavigateToLocalNetworkAccess)
|
||||
}
|
||||
|
||||
private fun handleResizeHasBeenRequested() {
|
||||
mutableStateFlow.update { it.copy(hasResizeBeenRequested = true) }
|
||||
}
|
||||
@@ -670,11 +656,6 @@ sealed class MainAction {
|
||||
*/
|
||||
data object CookieAcquisitionReady : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the local network access is required.
|
||||
*/
|
||||
data object LocalNetworkAccessRequired : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that resize has been requested on the Activity
|
||||
*/
|
||||
@@ -713,11 +694,6 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToCookieAcquisition : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the local network access screen.
|
||||
*/
|
||||
data object NavigateToLocalNetworkAccess : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CL
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -59,7 +58,6 @@ object PlatformNetworkModule {
|
||||
certificateManager: CertificateManager,
|
||||
buildInfoManager: BuildInfoManager,
|
||||
networkCookieManager: NetworkCookieManager,
|
||||
networkPermissionManager: NetworkPermissionManager,
|
||||
clock: Clock,
|
||||
): BitwardenServiceClientConfig = BitwardenServiceClientConfig(
|
||||
clock = clock,
|
||||
@@ -74,7 +72,6 @@ object PlatformNetworkModule {
|
||||
certificateProvider = certificateManager,
|
||||
enableHttpBodyLogging = buildInfoManager.isDevBuild,
|
||||
cookieProvider = networkCookieManager,
|
||||
permissionProvider = networkPermissionManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -74,8 +74,6 @@ import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManage
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkPlatformApiFactory
|
||||
@@ -452,14 +450,4 @@ object PlatformManagerModule {
|
||||
cookieDiskSource = cookieDiskSource,
|
||||
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkPermissionManager(
|
||||
@ApplicationContext context: Context,
|
||||
resourceManager: ResourceManager,
|
||||
): NetworkPermissionManager = NetworkPermissionManagerImpl(
|
||||
context = context,
|
||||
resourceManager = resourceManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
|
||||
import com.bitwarden.network.provider.PermissionProvider
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A manager class for handling network permissions.
|
||||
*/
|
||||
interface NetworkPermissionManager : PermissionProvider {
|
||||
/**
|
||||
* StateFlow indicating if local network access is being requested at this moment.
|
||||
*
|
||||
* Emits `true` when local network access is required, `false` otherwise.
|
||||
*/
|
||||
val isLocalNetworkAccessRequiredStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Sets the local network access required state to `false`.
|
||||
*/
|
||||
fun clearIsLocalNetworkAccessRequired()
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* The default implementation of [NetworkPermissionManager].
|
||||
*/
|
||||
internal class NetworkPermissionManagerImpl(
|
||||
private val context: Context,
|
||||
private val resourceManager: ResourceManager,
|
||||
) : NetworkPermissionManager {
|
||||
private val mutableIsLocalNetworkAccessRequiredStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override val errorMessageString: String
|
||||
get() = resourceManager.getString(
|
||||
resId = BitwardenString
|
||||
.your_request_was_interrupted_because_the_app_needs_local_network_access,
|
||||
)
|
||||
|
||||
override val hasLocalNetworkAccessPermission: Boolean
|
||||
get() = if (isBuildVersionAtLeast(version = Build.VERSION_CODES.CINNAMON_BUN)) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
override val isLocalNetworkAccessRequiredStateFlow: StateFlow<Boolean> =
|
||||
mutableIsLocalNetworkAccessRequiredStateFlow
|
||||
|
||||
override fun acquireLocalNetworkAccessPermission() {
|
||||
mutableIsLocalNetworkAccessRequiredStateFlow.value = true
|
||||
}
|
||||
|
||||
override fun clearIsLocalNetworkAccessRequired() {
|
||||
mutableIsLocalNetworkAccessRequiredStateFlow.value = false
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
|
||||
/**
|
||||
* Returns a user-friendly error message if this [Throwable] is an allow-listed
|
||||
@@ -9,7 +8,6 @@ import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
*/
|
||||
val Throwable.userFriendlyMessage: String?
|
||||
get() = when (this) {
|
||||
is LocalNetworkAccessException -> message
|
||||
is CookieRedirectException -> message
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the local network access screen.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Serializable
|
||||
data object LocalNetworkAccessRoute
|
||||
|
||||
/**
|
||||
* Add the local network access screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.localNetworkAccessDestination(
|
||||
onDismiss: () -> Unit,
|
||||
onSplashScreenRemoved: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<LocalNetworkAccessRoute> {
|
||||
LocalNetworkAccessScreen(onDismiss = onDismiss)
|
||||
// If we are displaying the local network access screen, then we can just hide
|
||||
// the splash screen.
|
||||
onSplashScreenRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the local network access screen.
|
||||
*/
|
||||
fun NavController.navigateToLocalNetworkAccess() {
|
||||
this.navigate(route = LocalNetworkAccessRoute) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.LifecycleEventEffect
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.manager.util.startAppSettingsActivity
|
||||
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.ui.platform.composition.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.handlers.LocalNetworkAccessHandler
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.handlers.rememberLocalNetworkAccessHandler
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private const val LOCAL_NETWORK_PERMISSION: String = Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
|
||||
/**
|
||||
* Top-level composable for the Local Network Access screen.
|
||||
*/
|
||||
@Composable
|
||||
fun LocalNetworkAccessScreen(
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: LocalNetworkAccessViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
) {
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
LocalNetworkAccessEvent.NavigateBack -> onDismiss()
|
||||
LocalNetworkAccessEvent.NavigateToSettings -> intentManager.startAppSettingsActivity()
|
||||
}
|
||||
}
|
||||
val handler = rememberLocalNetworkAccessHandler(viewModel)
|
||||
LifecycleEventEffect { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
handler.onResumed(permissionsManager.checkPermission(LOCAL_NETWORK_PERMISSION))
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = handler.onCloseClick)
|
||||
|
||||
BitwardenScaffold(
|
||||
contentWindowInsets = ScaffoldDefaults
|
||||
.contentWindowInsets
|
||||
.union(WindowInsets.displayCutout)
|
||||
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
|
||||
) {
|
||||
LocalNetworkAccessContent(
|
||||
permissionsManager = permissionsManager,
|
||||
handler = handler,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun LocalNetworkAccessContent(
|
||||
permissionsManager: PermissionsManager,
|
||||
handler: LocalNetworkAccessHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowPermissionDialog by rememberSaveable { mutableStateOf(value = false) }
|
||||
val localNetworkAccessPermissionLauncher = permissionsManager.getLauncher { isGranted ->
|
||||
if (isGranted) {
|
||||
handler.onCloseClick()
|
||||
} else if (
|
||||
!permissionsManager.shouldShowRequestPermissionRationale(LOCAL_NETWORK_PERMISSION)
|
||||
) {
|
||||
// "shouldShowRequestPermissionRationale" will only be 'true' after you have declined
|
||||
// the first OS prompt but have not seen the second prompt attempt. We do not want
|
||||
// to display the dialog after the first time we were declined, but we do after that.
|
||||
shouldShowPermissionDialog = true
|
||||
}
|
||||
}
|
||||
if (shouldShowPermissionDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = BitwardenString.local_network_access_required),
|
||||
message = stringResource(
|
||||
id = BitwardenString
|
||||
.without_this_permission_bitwarden_wont_be_able_to_sync_with_your_server,
|
||||
),
|
||||
confirmButtonText = stringResource(id = BitwardenString.go_to_settings),
|
||||
dismissButtonText = stringResource(id = BitwardenString.no_thanks),
|
||||
onConfirmClick = {
|
||||
shouldShowPermissionDialog = false
|
||||
handler.onSettingsClick()
|
||||
},
|
||||
onDismissClick = { shouldShowPermissionDialog = false },
|
||||
onDismissRequest = { shouldShowPermissionDialog = false },
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(state = rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 32.dp))
|
||||
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.ill_sso_cookie_sync),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.size(size = 100.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.access_your_local_network),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = BitwardenString.bitwarden_needs_local_network_access_to_sync_with_your_server,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.enable_local_network_access),
|
||||
onClick = {
|
||||
localNetworkAccessPermissionLauncher.launch(input = LOCAL_NETWORK_PERMISSION)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = BitwardenString.ask_again_later),
|
||||
onClick = handler.onContinueWithoutPermissionClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel for the Local Network Access screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class LocalNetworkAccessViewModel @Inject constructor(
|
||||
private val networkPermissionManager: NetworkPermissionManager,
|
||||
) : BaseViewModel<LocalNetworkAccessState, LocalNetworkAccessEvent, LocalNetworkAccessAction>(
|
||||
initialState = LocalNetworkAccessState,
|
||||
) {
|
||||
override fun handleAction(action: LocalNetworkAccessAction) {
|
||||
when (action) {
|
||||
LocalNetworkAccessAction.CloseClick -> handleCloseClick()
|
||||
LocalNetworkAccessAction.ContinueWithoutPermissionClick -> {
|
||||
handleContinueWithoutPermissionClick()
|
||||
}
|
||||
|
||||
is LocalNetworkAccessAction.Resumed -> handleResumed(action)
|
||||
LocalNetworkAccessAction.SettingsClick -> handleSettingsClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
networkPermissionManager.clearIsLocalNetworkAccessRequired()
|
||||
sendEvent(LocalNetworkAccessEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleContinueWithoutPermissionClick() {
|
||||
networkPermissionManager.clearIsLocalNetworkAccessRequired()
|
||||
sendEvent(LocalNetworkAccessEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleResumed(action: LocalNetworkAccessAction.Resumed) {
|
||||
if (action.hasLocalNetworkAccessPermission) {
|
||||
networkPermissionManager.clearIsLocalNetworkAccessRequired()
|
||||
sendEvent(LocalNetworkAccessEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSettingsClick() {
|
||||
sendEvent(LocalNetworkAccessEvent.NavigateToSettings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the Local Network Access screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data object LocalNetworkAccessState : Parcelable
|
||||
|
||||
/**
|
||||
* Events for the Local Network Access screen.
|
||||
*/
|
||||
sealed class LocalNetworkAccessEvent {
|
||||
/**
|
||||
* Navigate away from this screen.
|
||||
*/
|
||||
data object NavigateBack : LocalNetworkAccessEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the OS settings.
|
||||
*/
|
||||
data object NavigateToSettings : LocalNetworkAccessEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions for the Local Network Access screen.
|
||||
*/
|
||||
sealed class LocalNetworkAccessAction {
|
||||
/**
|
||||
* The user has clicked the close button.
|
||||
*/
|
||||
data object CloseClick : LocalNetworkAccessAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the continue without permission button.
|
||||
*/
|
||||
data object ContinueWithoutPermissionClick : LocalNetworkAccessAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the Settings button.
|
||||
*/
|
||||
data object SettingsClick : LocalNetworkAccessAction()
|
||||
|
||||
/**
|
||||
* The screen has resumed.
|
||||
*/
|
||||
data class Resumed(
|
||||
val hasLocalNetworkAccessPermission: Boolean,
|
||||
) : LocalNetworkAccessAction()
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.LocalNetworkAccessAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess.LocalNetworkAccessViewModel
|
||||
|
||||
/**
|
||||
* A class to handle user interactions for the Local Network Access screen.
|
||||
*/
|
||||
data class LocalNetworkAccessHandler(
|
||||
val onCloseClick: () -> Unit,
|
||||
val onContinueWithoutPermissionClick: () -> Unit,
|
||||
val onSettingsClick: () -> Unit,
|
||||
val onResumed: (hasPermission: Boolean) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [LocalNetworkAccessHandler] using the provided
|
||||
* [LocalNetworkAccessViewModel].
|
||||
*/
|
||||
fun create(
|
||||
viewModel: LocalNetworkAccessViewModel,
|
||||
): LocalNetworkAccessHandler = LocalNetworkAccessHandler(
|
||||
onCloseClick = { viewModel.trySendAction(LocalNetworkAccessAction.CloseClick) },
|
||||
onContinueWithoutPermissionClick = {
|
||||
viewModel.trySendAction(LocalNetworkAccessAction.ContinueWithoutPermissionClick)
|
||||
},
|
||||
onSettingsClick = { viewModel.trySendAction(LocalNetworkAccessAction.SettingsClick) },
|
||||
onResumed = { viewModel.trySendAction(LocalNetworkAccessAction.Resumed(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create and remember a [LocalNetworkAccessHandler] instance.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLocalNetworkAccessHandler(
|
||||
viewModel: LocalNetworkAccessViewModel,
|
||||
): LocalNetworkAccessHandler = remember(viewModel) {
|
||||
LocalNetworkAccessHandler.create(viewModel)
|
||||
}
|
||||
@@ -38,7 +38,6 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonH
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLicenseTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditPassportTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||
|
||||
/**
|
||||
@@ -57,7 +56,6 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||
sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
|
||||
bankAccountItemTypeHandlers: VaultAddEditBankAccountTypeHandlers,
|
||||
licenseItemTypeHandlers: VaultAddEditLicenseTypeHandlers,
|
||||
passportItemTypeHandlers: VaultAddEditPassportTypeHandlers,
|
||||
isCardScannerEnabled: Boolean,
|
||||
cardHolderNameFocusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -298,13 +296,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||
licenseHandlers = licenseItemTypeHandlers,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.ViewState.Content.ItemType.Passport -> {
|
||||
vaultAddEditPassportItems(
|
||||
passportState = state.type,
|
||||
passportHandlers = passportItemTypeHandlers,
|
||||
)
|
||||
}
|
||||
is VaultAddEditState.ViewState.Content.ItemType.Passport -> Unit
|
||||
}
|
||||
|
||||
vaultAddEditAdditionalOptions(
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditPassportTypeHandlers
|
||||
|
||||
/**
|
||||
* The UI for adding and editing a passport cipher.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun LazyListScope.vaultAddEditPassportItems(
|
||||
passportState: VaultAddEditState.ViewState.Content.ItemType.Passport,
|
||||
passportHandlers: VaultAddEditPassportTypeHandlers,
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.passport_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.first_name),
|
||||
value = passportState.givenName,
|
||||
onValueChange = passportHandlers.onGivenNameTextChange,
|
||||
textFieldTestTag = "PassportGivenNameEntry",
|
||||
cardStyle = CardStyle.Top(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.last_name),
|
||||
value = passportState.surname,
|
||||
onValueChange = passportHandlers.onSurnameTextChange,
|
||||
textFieldTestTag = "PassportSurnameEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextSelectionButton(
|
||||
label = stringResource(id = BitwardenString.date_of_birth),
|
||||
selectedOption = passportState.dateOfBirth,
|
||||
// TODO: Open a native Material date picker (separate ticket TBD).
|
||||
onClick = {},
|
||||
textFieldTestTag = "PassportDateOfBirthEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.sex),
|
||||
value = passportState.sex,
|
||||
onValueChange = passportHandlers.onSexTextChange,
|
||||
textFieldTestTag = "PassportSexEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.birth_place),
|
||||
value = passportState.birthPlace,
|
||||
onValueChange = passportHandlers.onBirthPlaceTextChange,
|
||||
textFieldTestTag = "PassportBirthPlaceEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.nationality),
|
||||
value = passportState.nationality,
|
||||
onValueChange = passportHandlers.onNationalityTextChange,
|
||||
textFieldTestTag = "PassportNationalityEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.passport_number),
|
||||
value = passportState.passportNumber,
|
||||
onValueChange = passportHandlers.onPassportNumberTextChange,
|
||||
passwordFieldTestTag = "PassportPassportNumberEntry",
|
||||
showPasswordTestTag = "PassportShowPassportNumberButton",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.passport_type),
|
||||
value = passportState.passportType,
|
||||
onValueChange = passportHandlers.onPassportTypeTextChange,
|
||||
textFieldTestTag = "PassportPassportTypeEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.national_identification_number),
|
||||
value = passportState.nationalIdentificationNumber,
|
||||
onValueChange = passportHandlers.onNationalIdentificationNumberTextChange,
|
||||
passwordFieldTestTag = "PassportNationalIdentificationNumberEntry",
|
||||
showPasswordTestTag = "PassportShowNationalIdentificationNumberButton",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.issuing_country),
|
||||
value = passportState.issuingCountry,
|
||||
onValueChange = passportHandlers.onIssuingCountryTextChange,
|
||||
textFieldTestTag = "PassportIssuingCountryEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.issuing_authority),
|
||||
value = passportState.issuingAuthority,
|
||||
onValueChange = passportHandlers.onIssuingAuthorityTextChange,
|
||||
textFieldTestTag = "PassportIssuingAuthorityEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextSelectionButton(
|
||||
label = stringResource(id = BitwardenString.issue_date),
|
||||
selectedOption = passportState.issueDate,
|
||||
// TODO: Open a native Material date picker (separate ticket TBD).
|
||||
onClick = {},
|
||||
textFieldTestTag = "PassportIssueDateEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenTextSelectionButton(
|
||||
label = stringResource(id = BitwardenString.expiration_date),
|
||||
selectedOption = passportState.expirationDate,
|
||||
// TODO: Open a native Material date picker (separate ticket TBD).
|
||||
onClick = {},
|
||||
textFieldTestTag = "PassportExpirationDateEntry",
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,6 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTyp
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentityTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLicenseTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.rememberVaultAddEditPassportTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
|
||||
@@ -247,8 +246,6 @@ fun VaultAddEditScreen(
|
||||
VaultAddEditLicenseTypeHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
|
||||
val passportItemTypeHandlers = rememberVaultAddEditPassportTypeHandlers(viewModel = viewModel)
|
||||
|
||||
val archiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.ArchiveClick) }
|
||||
|
||||
val unarchiveClickAction = { viewModel.trySendAction(VaultAddEditAction.Common.UnarchiveClick) }
|
||||
@@ -432,7 +429,6 @@ fun VaultAddEditScreen(
|
||||
sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
|
||||
bankAccountItemTypeHandlers = bankAccountItemTypeHandlers,
|
||||
licenseItemTypeHandlers = licenseItemTypeHandlers,
|
||||
passportItemTypeHandlers = passportItemTypeHandlers,
|
||||
isCardScannerEnabled = state.isCardScannerEnabled,
|
||||
cardHolderNameFocusRequester = cardHolderNameFocusRequester,
|
||||
lazyListState = lazyListState,
|
||||
|
||||
@@ -318,10 +318,6 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
handleLicenseTypeActions(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType -> {
|
||||
handlePassportTypeActions(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Internal -> handleInternalActions(action)
|
||||
}
|
||||
}
|
||||
@@ -1797,59 +1793,6 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
//endregion License Type Handlers
|
||||
|
||||
//region Passport Type Handlers
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handlePassportTypeActions(
|
||||
action: VaultAddEditAction.ItemType.PassportType,
|
||||
) {
|
||||
when (action) {
|
||||
is VaultAddEditAction.ItemType.PassportType.GivenNameTextChange -> {
|
||||
updatePassportContent { it.copy(givenName = action.givenName) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.SurnameTextChange -> {
|
||||
updatePassportContent { it.copy(surname = action.surname) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.SexTextChange -> {
|
||||
updatePassportContent { it.copy(sex = action.sex) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange -> {
|
||||
updatePassportContent { it.copy(birthPlace = action.birthPlace) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.NationalityTextChange -> {
|
||||
updatePassportContent { it.copy(nationality = action.nationality) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange -> {
|
||||
updatePassportContent { it.copy(passportNumber = action.passportNumber) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange -> {
|
||||
updatePassportContent { it.copy(passportType = action.passportType) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.NationalIdentificationNumberTextChange -> {
|
||||
updatePassportContent {
|
||||
it.copy(nationalIdentificationNumber = action.nationalIdentificationNumber)
|
||||
}
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange -> {
|
||||
updatePassportContent { it.copy(issuingCountry = action.country) }
|
||||
}
|
||||
|
||||
is VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange -> {
|
||||
updatePassportContent { it.copy(issuingAuthority = action.authority) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Passport Type Handlers
|
||||
|
||||
//region Internal Type Handlers
|
||||
|
||||
private fun handleInternalActions(action: VaultAddEditAction.Internal) {
|
||||
@@ -2627,16 +2570,6 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updatePassportContent(
|
||||
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.Passport) ->
|
||||
VaultAddEditState.ViewState.Content.ItemType.Passport,
|
||||
) {
|
||||
updateContent { currentContent ->
|
||||
(currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.Passport)
|
||||
?.let { currentContent.copy(type = block(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private suspend fun VaultAddEditState.ViewState.Content.createCipherForAddAndCloneItemStates(): CreateCipherResult {
|
||||
return common.selectedOwner?.collections
|
||||
@@ -3221,6 +3154,8 @@ data class VaultAddEditState(
|
||||
override val itemTypeOption: ItemTypeOption
|
||||
get() = ItemTypeOption.PASSPORT
|
||||
|
||||
override val isSdkSupported: Boolean get() = false
|
||||
|
||||
override val vaultLinkedFieldTypes: ImmutableList<VaultLinkedFieldType>
|
||||
get() = persistentListOf()
|
||||
}
|
||||
@@ -4205,64 +4140,6 @@ sealed class VaultAddEditAction {
|
||||
data class BankContactPhoneTextChange(val phone: String) : BankAccountType()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions specific to the Passport type.
|
||||
*/
|
||||
sealed class PassportType : ItemType() {
|
||||
|
||||
/**
|
||||
* Fired when the given name text input is changed.
|
||||
*/
|
||||
data class GivenNameTextChange(val givenName: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the surname text input is changed.
|
||||
*/
|
||||
data class SurnameTextChange(val surname: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the sex text input is changed.
|
||||
*/
|
||||
data class SexTextChange(val sex: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the birth place text input is changed.
|
||||
*/
|
||||
data class BirthPlaceTextChange(val birthPlace: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the nationality text input is changed.
|
||||
*/
|
||||
data class NationalityTextChange(val nationality: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the passport number text input is changed.
|
||||
*/
|
||||
data class PassportNumberTextChange(val passportNumber: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the passport type text input is changed.
|
||||
*/
|
||||
data class PassportTypeTextChange(val passportType: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the national identification number text input is changed.
|
||||
*/
|
||||
data class NationalIdentificationNumberTextChange(
|
||||
val nationalIdentificationNumber: String,
|
||||
) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the issuing country text input is changed.
|
||||
*/
|
||||
data class IssuingCountryTextChange(val country: String) : PassportType()
|
||||
|
||||
/**
|
||||
* Fired when the issuing authority text input is changed.
|
||||
*/
|
||||
data class IssuingAuthorityTextChange(val authority: String) : PassportType()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions specific to the License type.
|
||||
*/
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing user interactions on the Passport portion of the
|
||||
* Add/Edit cipher screen.
|
||||
*
|
||||
* @property onGivenNameTextChange Handles changes to the given name (first name) text input.
|
||||
* @property onSurnameTextChange Handles changes to the surname (last name) text input.
|
||||
* @property onSexTextChange Handles changes to the sex text input.
|
||||
* @property onBirthPlaceTextChange Handles changes to the birth place text input.
|
||||
* @property onNationalityTextChange Handles changes to the nationality text input.
|
||||
* @property onPassportNumberTextChange Handles changes to the passport number text input.
|
||||
* @property onPassportTypeTextChange Handles changes to the passport type text input.
|
||||
* @property onNationalIdentificationNumberTextChange Handles changes to the national identification
|
||||
* number text input.
|
||||
* @property onIssuingCountryTextChange Handles changes to the issuing country text input.
|
||||
* @property onIssuingAuthorityTextChange Handles changes to the issuing authority text input.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
data class VaultAddEditPassportTypeHandlers(
|
||||
val onGivenNameTextChange: (String) -> Unit,
|
||||
val onSurnameTextChange: (String) -> Unit,
|
||||
val onSexTextChange: (String) -> Unit,
|
||||
val onBirthPlaceTextChange: (String) -> Unit,
|
||||
val onNationalityTextChange: (String) -> Unit,
|
||||
val onPassportNumberTextChange: (String) -> Unit,
|
||||
val onPassportTypeTextChange: (String) -> Unit,
|
||||
val onNationalIdentificationNumberTextChange: (String) -> Unit,
|
||||
val onIssuingCountryTextChange: (String) -> Unit,
|
||||
val onIssuingAuthorityTextChange: (String) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates an instance of [VaultAddEditPassportTypeHandlers] by binding actions to
|
||||
* the provided [VaultAddEditViewModel].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun create(
|
||||
viewModel: VaultAddEditViewModel,
|
||||
): VaultAddEditPassportTypeHandlers =
|
||||
VaultAddEditPassportTypeHandlers(
|
||||
onGivenNameTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
|
||||
givenName = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onSurnameTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
|
||||
surname = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onSexTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = it),
|
||||
)
|
||||
},
|
||||
onBirthPlaceTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
|
||||
birthPlace = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onNationalityTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
|
||||
nationality = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onPassportNumberTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
|
||||
passportNumber = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onPassportTypeTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
|
||||
passportType = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onNationalIdentificationNumberTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction
|
||||
.ItemType
|
||||
.PassportType
|
||||
.NationalIdentificationNumberTextChange(
|
||||
nationalIdentificationNumber = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onIssuingCountryTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
|
||||
country = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onIssuingAuthorityTextChange = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
|
||||
authority = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remember a [VaultAddEditPassportTypeHandlers] instance in a [Composable]
|
||||
* scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberVaultAddEditPassportTypeHandlers(
|
||||
viewModel: VaultAddEditViewModel,
|
||||
): VaultAddEditPassportTypeHandlers =
|
||||
remember(viewModel) {
|
||||
VaultAddEditPassportTypeHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.util
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.core.data.util.toFormattedDateStyle
|
||||
import com.bitwarden.core.data.util.toFormattedDateTimeStyle
|
||||
import com.bitwarden.ui.platform.base.util.nullIfAllEqual
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
@@ -33,8 +32,6 @@ import com.x8bit.bitwarden.ui.vault.util.formatCardNumber
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.time.Clock
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
@@ -239,12 +236,12 @@ fun CipherView.toViewState(
|
||||
middleName = driversLicense?.middleName,
|
||||
lastName = driversLicense?.lastName,
|
||||
licenseNumber = driversLicense?.licenseNumber,
|
||||
dateOfBirth = driversLicense?.dateOfBirth?.toFormattedDate(clock = clock),
|
||||
dateOfBirth = driversLicense?.dateOfBirth,
|
||||
issuingCountry = driversLicense?.issuingCountry,
|
||||
issuingState = driversLicense?.issuingState,
|
||||
issuingAuthority = driversLicense?.issuingAuthority,
|
||||
issueDate = driversLicense?.issueDate?.toFormattedDate(clock = clock),
|
||||
expirationDate = driversLicense?.expirationDate?.toFormattedDate(clock = clock),
|
||||
issueDate = driversLicense?.issueDate,
|
||||
expirationDate = driversLicense?.expirationDate,
|
||||
licenseClass = driversLicense?.licenseClass,
|
||||
)
|
||||
}
|
||||
@@ -252,7 +249,7 @@ fun CipherView.toViewState(
|
||||
CipherType.PASSPORT -> VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = passport?.givenName,
|
||||
surname = passport?.surname,
|
||||
dateOfBirth = passport?.dateOfBirth?.toFormattedDate(clock = clock),
|
||||
dateOfBirth = passport?.dateOfBirth,
|
||||
sex = passport?.sex,
|
||||
birthPlace = passport?.birthPlace,
|
||||
nationality = passport?.nationality,
|
||||
@@ -261,8 +258,8 @@ fun CipherView.toViewState(
|
||||
nationalIdentificationNumber = passport?.nationalIdentificationNumber,
|
||||
issuingCountry = passport?.issuingCountry,
|
||||
issuingAuthority = passport?.issuingAuthority,
|
||||
issueDate = passport?.issueDate?.toFormattedDate(clock = clock),
|
||||
expirationDate = passport?.expirationDate?.toFormattedDate(clock = clock),
|
||||
issueDate = passport?.issueDate,
|
||||
expirationDate = passport?.expirationDate,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -305,24 +302,6 @@ fun FieldView.toCustomField(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string date that is formatted in the default ISO-8601 format (uuuu-MM-dd) and converts
|
||||
* it to appropriate human-readable format.
|
||||
*/
|
||||
private fun String.toFormattedDate(
|
||||
clock: Clock,
|
||||
): String? {
|
||||
val localDate = try {
|
||||
LocalDate.parse(this)
|
||||
} catch (_: DateTimeParseException) {
|
||||
null
|
||||
}
|
||||
return localDate?.toFormattedDateStyle(
|
||||
dateStyle = FormatStyle.LONG,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
private fun LoginUriView.toUriData() =
|
||||
VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = uri.orZeroWidthSpace(),
|
||||
|
||||
@@ -88,10 +88,9 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
|
||||
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE
|
||||
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> CipherType.SSH_KEY
|
||||
is VaultAddEditState.ViewState.Content.ItemType.BankAccount -> CipherType.BANK_ACCOUNT
|
||||
is VaultAddEditState.ViewState.Content.ItemType.Passport -> CipherType.PASSPORT
|
||||
is VaultAddEditState.ViewState.Content.ItemType.License -> {
|
||||
throw IllegalArgumentException("SDK mapping not yet available for $this")
|
||||
}
|
||||
is VaultAddEditState.ViewState.Content.ItemType.License,
|
||||
is VaultAddEditState.ViewState.Content.ItemType.Passport,
|
||||
-> throw IllegalArgumentException("SDK mapping not yet available for $this")
|
||||
}
|
||||
|
||||
private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? =
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.auth.AuthTabIntent
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import com.x8bit.bitwarden.data.billing.util.PremiumCheckoutCallbackResult
|
||||
import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
|
||||
import androidx.credentials.provider.BiometricPromptResult
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
@@ -46,8 +48,6 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.billing.util.PremiumCheckoutCallbackResult
|
||||
import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
|
||||
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
@@ -65,7 +65,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.CookieAcquisitionRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
@@ -183,12 +182,6 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager = mockk {
|
||||
every { cookieAcquisitionRequestFlow } returns mutableCookieAcquisitionRequestFlow
|
||||
}
|
||||
private val mutableIsLocalNetworkAccessRequiredStateFlow = MutableStateFlow(false)
|
||||
private val networkPermissionManager: NetworkPermissionManager = mockk {
|
||||
every {
|
||||
isLocalNetworkAccessRequiredStateFlow
|
||||
} returns mutableIsLocalNetworkAccessRequiredStateFlow
|
||||
}
|
||||
private val credentialProviderRequestManager: CredentialProviderRequestManager = mockk {
|
||||
every { getPendingCredentialRequest() } returns null
|
||||
}
|
||||
@@ -1319,20 +1312,6 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `local network access required should emit NavigateToLocalNetworkAccess when stateflow emits`() =
|
||||
runTest {
|
||||
mutableIsLocalNetworkAccessRequiredStateFlow.value = true
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
// Skip init events (appLanguage + appTheme)
|
||||
skipItems(2)
|
||||
assertEquals(MainEvent.NavigateToLocalNetworkAccess, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cookie acquisition should not emit event when conditions are false`() =
|
||||
runTest {
|
||||
@@ -1372,12 +1351,11 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
): MainViewModel = MainViewModel(
|
||||
) = MainViewModel(
|
||||
accessibilitySelectionManager = accessibilitySelectionManager,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemAuthenticatorManager,
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
cookieAcquisitionRequestManager = cookieAcquisitionRequestManager,
|
||||
networkPermissionManager = networkPermissionManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
garbageCollectionManager = garbageCollectionManager,
|
||||
credentialProviderRequestManager = credentialProviderRequestManager,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.network
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class NetworkPermissionManagerTest {
|
||||
private val context: Context = mockk()
|
||||
private val resourceManager: ResourceManager = mockk()
|
||||
|
||||
private val networkPermissionManager: NetworkPermissionManager = NetworkPermissionManagerImpl(
|
||||
context = context,
|
||||
resourceManager = resourceManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(
|
||||
ContextCompat::checkSelfPermission,
|
||||
::isBuildVersionAtLeast,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
ContextCompat::checkSelfPermission,
|
||||
::isBuildVersionAtLeast,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `errorMessageString should return the string from context`() {
|
||||
every {
|
||||
resourceManager.getString(
|
||||
resId = BitwardenString
|
||||
.your_request_was_interrupted_because_the_app_needs_local_network_access,
|
||||
)
|
||||
} returns ERROR_MESSAGE
|
||||
assertEquals(ERROR_MESSAGE, networkPermissionManager.errorMessageString)
|
||||
verify(exactly = 1) {
|
||||
resourceManager.getString(
|
||||
resId = BitwardenString
|
||||
.your_request_was_interrupted_because_the_app_needs_local_network_access,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasLocalNetworkAccessPermission returns true when below CINNAMON_BUN`() {
|
||||
every { isBuildVersionAtLeast(Build.VERSION_CODES.CINNAMON_BUN) } returns false
|
||||
|
||||
assertTrue(networkPermissionManager.hasLocalNetworkAccessPermission)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasLocalNetworkAccessPermission returns true when permission granted on CINNAMON_BUN+`() {
|
||||
every { isBuildVersionAtLeast(Build.VERSION_CODES.CINNAMON_BUN) } returns true
|
||||
every {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
} returns PackageManager.PERMISSION_GRANTED
|
||||
|
||||
assertTrue(networkPermissionManager.hasLocalNetworkAccessPermission)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasLocalNetworkAccessPermission returns false when permission denied on CINNAMON_BUN+`() {
|
||||
every { isBuildVersionAtLeast(Build.VERSION_CODES.CINNAMON_BUN) } returns true
|
||||
every {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
} returns PackageManager.PERMISSION_DENIED
|
||||
|
||||
assertFalse(networkPermissionManager.hasLocalNetworkAccessPermission)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isLocalNetworkAccessRequiredStateFlow initial value is false`() = runTest {
|
||||
networkPermissionManager.isLocalNetworkAccessRequiredStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `acquireLocalNetworkAccessPermission sets isLocalNetworkAccessRequired to true`() =
|
||||
runTest {
|
||||
networkPermissionManager.isLocalNetworkAccessRequiredStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
|
||||
networkPermissionManager.acquireLocalNetworkAccessPermission()
|
||||
|
||||
assertTrue(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearIsLocalNetworkAccessRequired sets isLocalNetworkAccessRequired to false`() =
|
||||
runTest {
|
||||
networkPermissionManager.isLocalNetworkAccessRequiredStateFlow.test {
|
||||
assertFalse(awaitItem())
|
||||
|
||||
networkPermissionManager.acquireLocalNetworkAccessPermission()
|
||||
assertTrue(awaitItem())
|
||||
|
||||
networkPermissionManager.clearIsLocalNetworkAccessRequired()
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ERROR_MESSAGE = "error message"
|
||||
@@ -40,7 +40,6 @@ class SdkRepositoryFactoryTests {
|
||||
authTokenProvider = mockk(),
|
||||
certificateProvider = mockk(),
|
||||
cookieProvider = mockk(),
|
||||
permissionProvider = mockk(),
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -23,13 +22,6 @@ class ThrowableExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userFriendlyMessage should return message for LocalNetworkAccessException`() {
|
||||
val message = "Fail!"
|
||||
val exception = LocalNetworkAccessException(message = message)
|
||||
assertEquals(message, exception.userFriendlyMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userFriendlyMessage should return null for IOException`() {
|
||||
val exception = IOException("io error")
|
||||
|
||||
@@ -271,13 +271,13 @@ fun createMockDriversLicenseView(
|
||||
firstName: String? = "mockFirstName-$number",
|
||||
middleName: String? = "mockMiddleName-$number",
|
||||
lastName: String? = "mockLastName-$number",
|
||||
dateOfBirth: String? = "2006-05-11",
|
||||
dateOfBirth: String? = "mockDateOfBirth-$number",
|
||||
licenseNumber: String? = "mockLicenseNumber-$number",
|
||||
issuingCountry: String? = "mockIssuingCountry-$number",
|
||||
issuingState: String? = "mockIssuingState-$number",
|
||||
issuingAuthority: String? = "mockIssuingAuthority-$number",
|
||||
issueDate: String? = "2024-06-15",
|
||||
expirationDate: String? = "2031-11-25",
|
||||
issueDate: String? = "mockIssueDate-$number",
|
||||
expirationDate: String? = "mockExpirationDate-$number",
|
||||
licenseClass: String? = "mockLicenseClass-$number",
|
||||
): DriversLicenseView =
|
||||
DriversLicenseView(
|
||||
@@ -302,7 +302,7 @@ fun createMockPassportView(
|
||||
number: Int,
|
||||
surname: String? = "mockSurname-$number",
|
||||
givenName: String? = "mockGivenName-$number",
|
||||
dateOfBirth: String? = "2006-05-11",
|
||||
dateOfBirth: String? = "mockDateOfBirth-$number",
|
||||
birthPlace: String? = "mockBirthPlace-$number",
|
||||
sex: String? = "mockSex-$number",
|
||||
nationality: String? = "mockNationality-$number",
|
||||
@@ -310,8 +310,8 @@ fun createMockPassportView(
|
||||
passportType: String? = "mockPassportType-$number",
|
||||
issuingCountry: String? = "mockIssuingCountry-$number",
|
||||
issuingAuthority: String? = "mockIssuingAuthority-$number",
|
||||
issueDate: String? = "2024-06-15",
|
||||
expirationDate: String? = "2031-11-25",
|
||||
issueDate: String? = "mockIssueDate-$number",
|
||||
expirationDate: String? = "mockExpirationDate-$number",
|
||||
nationalIdentificationNumber: String? = "mockNationalIdentificationNumber-$number",
|
||||
): PassportView =
|
||||
PassportView(
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.manager.util.startAppSettingsActivity
|
||||
import com.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class LocalNetworkAccessScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onDismissCalled: Boolean = false
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(value = LocalNetworkAccessState)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<LocalNetworkAccessEvent>()
|
||||
private val viewModel = mockk<LocalNetworkAccessViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
}
|
||||
private val intentManager = mockk<IntentManager> {
|
||||
every { startActivity(intent = any()) } returns true
|
||||
}
|
||||
private val permissionsManager = FakePermissionManager()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setContent(
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
) {
|
||||
LocalNetworkAccessScreen(
|
||||
onDismiss = { onDismissCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `screen displays title, description, and buttons`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Access your local network")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "Bitwarden needs local network access to sync with your server. Without " +
|
||||
"this permission, the app won’t be able to connect.",
|
||||
)
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Ask again later")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `system back press sends CloseClick action`() {
|
||||
backDispatcher?.onBackPressed()
|
||||
verify { viewModel.trySendAction(LocalNetworkAccessAction.CloseClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Ask again later click sends ContinueWithoutPermissionClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Ask again later")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(LocalNetworkAccessAction.ContinueWithoutPermissionClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Enable local network access click with permission granted sends CloseClick action`() {
|
||||
permissionsManager.getPermissionsResult = true
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(LocalNetworkAccessAction.CloseClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Enable local network access click with permission denied shows dialog`() {
|
||||
permissionsManager.getPermissionsResult = false
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "Without this permission, Bitwarden won’t be able to connect and sync " +
|
||||
"with your server. You can enable this in your device settings.",
|
||||
)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Enable local network access click with permission denied and rationale shows does not show dialog`() {
|
||||
permissionsManager.getPermissionsResult = false
|
||||
permissionsManager.shouldShowRequestRationale = true
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dialog Go to settings click sends SettingsClick and hides dialog`() {
|
||||
permissionsManager.getPermissionsResult = false
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Go to settings")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(LocalNetworkAccessAction.SettingsClick) }
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dialog No thanks click hides dialog`() {
|
||||
permissionsManager.getPermissionsResult = false
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Enable local network access")
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "No thanks")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event calls onDismiss`() {
|
||||
mutableEventFlow.tryEmit(LocalNetworkAccessEvent.NavigateBack)
|
||||
assertTrue(onDismissCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSettings event calls startAppSettingsActivity`() {
|
||||
mockkStatic(IntentManager::startAppSettingsActivity) {
|
||||
every { intentManager.startAppSettingsActivity() } returns true
|
||||
mutableEventFlow.tryEmit(LocalNetworkAccessEvent.NavigateToSettings)
|
||||
verify(exactly = 1) { intentManager.startAppSettingsActivity() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.localnetworkaccess
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkPermissionManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LocalNetworkAccessViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val networkPermissionManager = mockk<NetworkPermissionManager> {
|
||||
every { clearIsLocalNetworkAccessRequired() } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state is LocalNetworkAccessState`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(LocalNetworkAccessState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick clears permission and sends NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LocalNetworkAccessAction.CloseClick)
|
||||
assertEquals(LocalNetworkAccessEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) { networkPermissionManager.clearIsLocalNetworkAccessRequired() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueWithoutPermissionClick clears permission and sends NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LocalNetworkAccessAction.ContinueWithoutPermissionClick)
|
||||
assertEquals(LocalNetworkAccessEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) { networkPermissionManager.clearIsLocalNetworkAccessRequired() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SettingsClick sends NavigateToSettings`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LocalNetworkAccessAction.SettingsClick)
|
||||
assertEquals(LocalNetworkAccessEvent.NavigateToSettings, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Resumed with permission granted clears permission and sends NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
LocalNetworkAccessAction.Resumed(hasLocalNetworkAccessPermission = true),
|
||||
)
|
||||
assertEquals(LocalNetworkAccessEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) { networkPermissionManager.clearIsLocalNetworkAccessRequired() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Resumed with permission not granted does not send any event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
LocalNetworkAccessAction.Resumed(hasLocalNetworkAccessPermission = false),
|
||||
)
|
||||
expectNoEvents()
|
||||
}
|
||||
verify(exactly = 0) { networkPermissionManager.clearIsLocalNetworkAccessRequired() }
|
||||
}
|
||||
|
||||
private fun createViewModel(): LocalNetworkAccessViewModel = LocalNetworkAccessViewModel(
|
||||
networkPermissionManager = networkPermissionManager,
|
||||
)
|
||||
}
|
||||
@@ -2911,171 +2911,6 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing first name should trigger GivenNameTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "First name")
|
||||
.performTextInput(text = "Bruce")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
|
||||
givenName = "Bruce",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing last name should trigger SurnameTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Last name")
|
||||
.performTextInput(text = "Wayne")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
|
||||
surname = "Wayne",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing sex should trigger SexTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Sex")
|
||||
.performTextInput(text = "M")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = "M"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing birth place should trigger BirthPlaceTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Birth place")
|
||||
.performTextInput(text = "Gotham City")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
|
||||
birthPlace = "Gotham City",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing nationality should trigger NationalityTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Nationality")
|
||||
.performTextInput(text = "American")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
|
||||
nationality = "American",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in ItemType_Passport changing the passport number text field should trigger PassportNumberTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Passport number")
|
||||
.performTextInput(text = "X12345678")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
|
||||
passportNumber = "X12345678",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in ItemType_Passport changing passport type should trigger PassportTypeTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Passport type")
|
||||
.performTextInput(text = "Regular")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
|
||||
passportType = "Regular",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in ItemType_Passport changing the national identification number text field should trigger NationalIdentificationNumberTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "National identification number")
|
||||
.performTextInput(text = "987-65-4321")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction
|
||||
.ItemType
|
||||
.PassportType
|
||||
.NationalIdentificationNumberTextChange(
|
||||
nationalIdentificationNumber = "987-65-4321",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in ItemType_Passport changing the issuing country text field should trigger IssuingCountryTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Issuing country")
|
||||
.performTextInput(text = "USA")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
|
||||
country = "USA",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in ItemType_Passport changing the issuing authority text field should trigger IssuingAuthorityTextChange`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_PASSPORT
|
||||
composeTestRule
|
||||
.onNodeWithTextAfterScroll(text = "Issuing authority")
|
||||
.performTextInput(text = "U.S. Department of State")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
|
||||
authority = "U.S. Department of State",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking Add field button should allow creation of Linked type`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN
|
||||
@@ -5202,22 +5037,6 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
isCardScannerEnabled = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_PASSPORT = VaultAddEditState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
cipherType = VaultItemCipherType.PASSPORT,
|
||||
viewState = VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(),
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Passport(),
|
||||
isIndividualVaultDisabled = false,
|
||||
),
|
||||
dialog = null,
|
||||
bottomSheetState = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
defaultUriMatchType = UriMatchTypeModel.EXACT,
|
||||
hasPremium = false,
|
||||
isCardScannerEnabled = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
|
||||
viewState = VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
|
||||
@@ -2444,21 +2444,14 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode, SaveClick with a Passport item should not short-circuit and should run validation`() =
|
||||
fun `in add mode, SaveClick with a Passport item should emit ShowSnackbar without saving`() =
|
||||
runTest {
|
||||
mutableVaultDataFlow.value = DataState.Loaded(createVaultData())
|
||||
val passportState = createVaultAddItemState(
|
||||
vaultItemCipherType = VaultItemCipherType.PASSPORT,
|
||||
commonContentViewState = createCommonContentViewState(name = ""),
|
||||
commonContentViewState = createCommonContentViewState(name = "mockName-1"),
|
||||
typeContentViewState = VaultAddEditState.ViewState.Content.ItemType.Passport(),
|
||||
)
|
||||
val expectedValidationDialogState = passportState.copy(
|
||||
dialog = VaultAddEditState.DialogState.Generic(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.name.asText()),
|
||||
),
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
state = passportState,
|
||||
@@ -2467,11 +2460,19 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
assertEquals(passportState, stateFlow.awaitItem())
|
||||
assertEquals(expectedValidationDialogState, stateFlow.awaitItem())
|
||||
eventFlow.expectNoEvents()
|
||||
assertEquals(
|
||||
VaultAddEditEvent.ShowSnackbar(
|
||||
message = BitwardenString.an_error_has_occurred.asText(),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(passportState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.createCipher(any())
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2532,13 +2533,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemType Passport should expose PASSPORT itemTypeOption and be SDK supported`() {
|
||||
fun `ItemType Passport should expose PASSPORT itemTypeOption and not be SDK supported`() {
|
||||
val itemType = VaultAddEditState.ViewState.Content.ItemType.Passport()
|
||||
assertEquals(
|
||||
VaultAddEditState.ItemTypeOption.PASSPORT,
|
||||
itemType.itemTypeOption,
|
||||
)
|
||||
assertTrue(itemType.isSdkSupported)
|
||||
assertFalse(itemType.isSdkSupported)
|
||||
assertTrue(itemType.vaultLinkedFieldTypes.isEmpty())
|
||||
}
|
||||
|
||||
@@ -4339,188 +4340,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class VaultAddEditPassportTypeItemActions {
|
||||
private lateinit var viewModel: VaultAddEditViewModel
|
||||
private lateinit var vaultAddItemInitialState: VaultAddEditState
|
||||
private lateinit var passportInitialSavedStateHandle: SavedStateHandle
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mutableVaultDataFlow.value = DataState.Loaded(
|
||||
createVaultData(cipherListView = createMockCipherListView(1)),
|
||||
)
|
||||
vaultAddItemInitialState = createVaultAddItemState(
|
||||
vaultItemCipherType = VaultItemCipherType.PASSPORT,
|
||||
typeContentViewState =
|
||||
VaultAddEditState.ViewState.Content.ItemType.Passport(),
|
||||
)
|
||||
passportInitialSavedStateHandle = createSavedStateHandleWithState(
|
||||
state = vaultAddItemInitialState,
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
vaultItemCipherType = VaultItemCipherType.PASSPORT,
|
||||
)
|
||||
viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = passportInitialSavedStateHandle,
|
||||
)
|
||||
}
|
||||
|
||||
private fun expectedPassport(
|
||||
block: VaultAddEditState.ViewState.Content.ItemType.Passport.() ->
|
||||
VaultAddEditState.ViewState.Content.ItemType.Passport,
|
||||
): VaultAddEditState =
|
||||
createVaultAddItemState(
|
||||
vaultItemCipherType = VaultItemCipherType.PASSPORT,
|
||||
typeContentViewState = VaultAddEditState
|
||||
.ViewState
|
||||
.Content
|
||||
.ItemType
|
||||
.Passport()
|
||||
.block(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GivenNameTextChange should update given name`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.GivenNameTextChange(
|
||||
givenName = "Bruce",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(givenName = "Bruce") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SurnameTextChange should update surname`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SurnameTextChange(
|
||||
surname = "Wayne",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(surname = "Wayne") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SexTextChange should update sex`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.SexTextChange(sex = "M"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(sex = "M") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BirthPlaceTextChange should update birth place`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.BirthPlaceTextChange(
|
||||
birthPlace = "Gotham City",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(birthPlace = "Gotham City") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NationalityTextChange should update nationality`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.NationalityTextChange(
|
||||
nationality = "American",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(nationality = "American") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PassportNumberTextChange should update passport number`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportNumberTextChange(
|
||||
passportNumber = "X12345678",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(passportNumber = "X12345678") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PassportTypeTextChange should update passport type`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.PassportTypeTextChange(
|
||||
passportType = "Regular",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(passportType = "Regular") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NationalIdentificationNumberTextChange should update national id number`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction
|
||||
.ItemType
|
||||
.PassportType
|
||||
.NationalIdentificationNumberTextChange(
|
||||
nationalIdentificationNumber = "987-65-4321",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(nationalIdentificationNumber = "987-65-4321") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `IssuingCountryTextChange should update issuing country`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingCountryTextChange(
|
||||
country = "USA",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(issuingCountry = "USA") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `IssuingAuthorityTextChange should update issuing authority`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.PassportType.IssuingAuthorityTextChange(
|
||||
authority = "U.S. Department of State",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedPassport { copy(issuingAuthority = "U.S. Department of State") },
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NumberVisibilityChange should log an event when in edit mode and password is visible`() =
|
||||
runTest {
|
||||
|
||||
@@ -452,6 +452,7 @@ class CipherViewExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Drivers License Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.DRIVERS_LICENSE, isEmpty = false)
|
||||
@@ -484,12 +485,12 @@ class CipherViewExtensionsTest {
|
||||
middleName = "mockMiddleName-1",
|
||||
lastName = "mockLastName-1",
|
||||
licenseNumber = "mockLicenseNumber-1",
|
||||
dateOfBirth = "May 11, 2006",
|
||||
dateOfBirth = "mockDateOfBirth-1",
|
||||
issuingCountry = "mockIssuingCountry-1",
|
||||
issuingState = "mockIssuingState-1",
|
||||
issuingAuthority = "mockIssuingAuthority-1",
|
||||
issueDate = "June 15, 2024",
|
||||
expirationDate = "November 25, 2031",
|
||||
issueDate = "mockIssueDate-1",
|
||||
expirationDate = "mockExpirationDate-1",
|
||||
licenseClass = "mockLicenseClass-1",
|
||||
),
|
||||
),
|
||||
@@ -497,6 +498,7 @@ class CipherViewExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform empty CipherView into ViewState Drivers License Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.DRIVERS_LICENSE, isEmpty = true)
|
||||
@@ -571,7 +573,7 @@ class CipherViewExtensionsTest {
|
||||
type = VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "mockGivenName-1",
|
||||
surname = "mockSurname-1",
|
||||
dateOfBirth = "May 11, 2006",
|
||||
dateOfBirth = "mockDateOfBirth-1",
|
||||
sex = "mockSex-1",
|
||||
birthPlace = "mockBirthPlace-1",
|
||||
nationality = "mockNationality-1",
|
||||
@@ -580,8 +582,8 @@ class CipherViewExtensionsTest {
|
||||
nationalIdentificationNumber = "mockNationalIdentificationNumber-1",
|
||||
issuingCountry = "mockIssuingCountry-1",
|
||||
issuingAuthority = "mockIssuingAuthority-1",
|
||||
issueDate = "June 15, 2024",
|
||||
expirationDate = "November 25, 2031",
|
||||
issueDate = "mockIssueDate-1",
|
||||
expirationDate = "mockExpirationDate-1",
|
||||
),
|
||||
),
|
||||
viewState,
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.bitwarden.vault.FieldView
|
||||
import com.bitwarden.vault.IdentityView
|
||||
import com.bitwarden.vault.LoginUriView
|
||||
import com.bitwarden.vault.LoginView
|
||||
import com.bitwarden.vault.PassportView
|
||||
import com.bitwarden.vault.PasswordHistoryView
|
||||
import com.bitwarden.vault.SecureNoteType
|
||||
import com.bitwarden.vault.SecureNoteView
|
||||
@@ -555,89 +554,6 @@ class VaultAddItemStateExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCipherView should transform Passport ItemType to CipherView`() {
|
||||
val viewState = VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
name = "mockName-1",
|
||||
selectedFolderId = "mockId-1",
|
||||
favorite = false,
|
||||
masterPasswordReprompt = false,
|
||||
notes = "mockNotes-1",
|
||||
selectedOwnerId = "mockOwnerId-1",
|
||||
),
|
||||
isIndividualVaultDisabled = false,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "Bruce",
|
||||
surname = "Wayne",
|
||||
dateOfBirth = "1939-05-27",
|
||||
sex = "M",
|
||||
birthPlace = "Gotham City",
|
||||
nationality = "American",
|
||||
passportNumber = "X12345678",
|
||||
passportType = "Regular",
|
||||
nationalIdentificationNumber = "987-65-4321",
|
||||
issuingCountry = "USA",
|
||||
issuingAuthority = "U.S. Department of State",
|
||||
issueDate = "2020-01-15",
|
||||
expirationDate = "2030-01-15",
|
||||
),
|
||||
)
|
||||
|
||||
val result = viewState.toCipherView(clock = FIXED_CLOCK, isPremiumUser = true)
|
||||
|
||||
assertEquals(
|
||||
CipherView(
|
||||
id = null,
|
||||
organizationId = "mockOwnerId-1",
|
||||
folderId = "mockId-1",
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = "mockName-1",
|
||||
notes = "mockNotes-1",
|
||||
type = CipherType.PASSPORT,
|
||||
login = null,
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
bankAccount = null,
|
||||
driversLicense = null,
|
||||
passport = PassportView(
|
||||
surname = "Wayne",
|
||||
givenName = "Bruce",
|
||||
dateOfBirth = "1939-05-27",
|
||||
birthPlace = "Gotham City",
|
||||
sex = "M",
|
||||
nationality = "American",
|
||||
passportNumber = "X12345678",
|
||||
passportType = "Regular",
|
||||
issuingCountry = "USA",
|
||||
issuingAuthority = "U.S. Department of State",
|
||||
issueDate = "2020-01-15",
|
||||
expirationDate = "2030-01-15",
|
||||
nationalIdentificationNumber = "987-65-4321",
|
||||
),
|
||||
favorite = false,
|
||||
reprompt = CipherRepromptType.NONE,
|
||||
organizationUseTotp = false,
|
||||
edit = true,
|
||||
viewPassword = true,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
fields = emptyList(),
|
||||
passwordHistory = null,
|
||||
permissions = null,
|
||||
creationDate = FIXED_CLOCK.instant(),
|
||||
deletedDate = null,
|
||||
revisionDate = FIXED_CLOCK.instant(),
|
||||
archivedDate = null,
|
||||
sshKey = null,
|
||||
attachmentDecryptionFailures = null,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCipherView should transform Card ItemType to CipherView`() {
|
||||
val viewState = VaultAddEditState.ViewState.Content(
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.bitwarden.network.model.AuthTokenData
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.model.NetworkCookie
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
import com.bitwarden.network.provider.PermissionProvider
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
@@ -84,13 +83,6 @@ object PlatformNetworkModule {
|
||||
|
||||
override fun acquireCookies(hostname: String): Unit = Unit
|
||||
},
|
||||
permissionProvider = object : PermissionProvider {
|
||||
override val errorMessageString: String get() = "Error"
|
||||
|
||||
override val hasLocalNetworkAccessPermission: Boolean get() = false
|
||||
|
||||
override fun acquireLocalNetworkAccessPermission(): Unit = Unit
|
||||
},
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -6,8 +6,8 @@ appVersionCode = "1"
|
||||
appVersionName = "2026.4.0"
|
||||
|
||||
# SDK Versions
|
||||
compileSdk = "37"
|
||||
targetSdk = "37"
|
||||
compileSdk = "36"
|
||||
targetSdk = "36"
|
||||
minSdk = "29"
|
||||
minSdkBwa = "28"
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.bitwarden.network.interceptor.AuthTokenManager
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.CookieInterceptor
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.interceptor.PermissionInterceptor
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
@@ -72,9 +71,6 @@ internal class BitwardenServiceClientImpl(
|
||||
cookieInterceptor = CookieInterceptor(
|
||||
cookieProvider = cookieProvider,
|
||||
),
|
||||
permissionInterceptor = PermissionInterceptor(
|
||||
permissionProvider = bitwardenServiceClientConfig.permissionProvider,
|
||||
),
|
||||
headersInterceptor = HeadersInterceptor(
|
||||
userAgent = bitwardenServiceClientConfig.clientData.userAgent,
|
||||
clientName = bitwardenServiceClientConfig.clientData.clientName,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.bitwarden.network.exception
|
||||
|
||||
import android.Manifest
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Thrown when a user attempts to make a network request to a device on the local network but does
|
||||
* not have the [Manifest.permission.ACCESS_LOCAL_NETWORK] permission.
|
||||
*/
|
||||
class LocalNetworkAccessException(message: String) : IOException(message)
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.bitwarden.network.interceptor
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
import com.bitwarden.network.provider.PermissionProvider
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* Interceptor responsible for determining if the destination of the network request is on the
|
||||
* local network or not.
|
||||
*/
|
||||
internal class PermissionInterceptor(
|
||||
private val permissionProvider: PermissionProvider,
|
||||
) : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (permissionProvider.hasLocalNetworkAccessPermission || !chain.isLocalRequest()) {
|
||||
return chain.proceed(request = chain.request())
|
||||
}
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
throw LocalNetworkAccessException(message = permissionProvider.errorMessageString)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
private fun Interceptor.Chain.isLocalRequest(): Boolean {
|
||||
val host = this.request().url.host
|
||||
val address = try {
|
||||
InetAddress.getByName(host)
|
||||
} catch (uhe: UnknownHostException) {
|
||||
// We just rethrow this exception, it was gonna happen anyway.
|
||||
throw uhe
|
||||
} catch (_: SecurityException) {
|
||||
// A security exception has occurred, lets be safe and assume it's a local request.
|
||||
return true
|
||||
}
|
||||
return address.isSiteLocalAddress
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
@@ -49,14 +48,12 @@ sealed class BitwardenError {
|
||||
*/
|
||||
fun Throwable.toBitwardenError(): BitwardenError {
|
||||
return when (this) {
|
||||
// The LocalNetworkAccessException and CookieRedirectException are subclasses of
|
||||
// IOException thrown when specific conditions are met. They must be checked before
|
||||
// IOException to avoid being classified as a generic Network error. We synthesize an
|
||||
// Http error with a JSON body so the exception's message propagates through the existing
|
||||
// CookieRedirectException is a subclass of IOException thrown when SSO cookies
|
||||
// expire in a load-balanced environment. It must be checked before IOException to
|
||||
// avoid being classified as a generic Network error. We synthesize an Http error
|
||||
// with a JSON body so the exception's message propagates through the existing
|
||||
// parseErrorBodyOrNull pipeline used by service-layer recoverCatching blocks.
|
||||
is LocalNetworkAccessException,
|
||||
is CookieRedirectException,
|
||||
-> {
|
||||
is CookieRedirectException -> {
|
||||
BitwardenError.Http(
|
||||
throwable = HttpException(
|
||||
Response.error<Any>(
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.bitwarden.network.interceptor.AuthTokenProvider
|
||||
import com.bitwarden.network.interceptor.BaseUrlsProvider
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
import com.bitwarden.network.provider.PermissionProvider
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import java.time.Clock
|
||||
|
||||
@@ -19,7 +18,6 @@ data class BitwardenServiceClientConfig(
|
||||
val authTokenProvider: AuthTokenProvider,
|
||||
val certificateProvider: CertificateProvider,
|
||||
val cookieProvider: CookieProvider,
|
||||
val permissionProvider: PermissionProvider,
|
||||
val clock: Clock,
|
||||
val enableHttpBodyLogging: Boolean = false,
|
||||
) {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.bitwarden.network.provider
|
||||
|
||||
import android.Manifest
|
||||
|
||||
/**
|
||||
* A provider for network-related permissions.
|
||||
*/
|
||||
interface PermissionProvider {
|
||||
/**
|
||||
* The translated human-readable string to be displayed when the local network access
|
||||
* permission is the reason for a request failure.
|
||||
*/
|
||||
val errorMessageString: String
|
||||
|
||||
/**
|
||||
* Indicates if the app does or does not have the [Manifest.permission.ACCESS_LOCAL_NETWORK]
|
||||
* permission.
|
||||
*/
|
||||
val hasLocalNetworkAccessPermission: Boolean
|
||||
|
||||
/**
|
||||
* Signals that local network access permission is required for the current environment.
|
||||
*/
|
||||
fun acquireLocalNetworkAccessPermission()
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import com.bitwarden.network.interceptor.BaseUrlInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.CookieInterceptor
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.interceptor.PermissionInterceptor
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import com.bitwarden.network.ssl.configureSsl
|
||||
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
|
||||
@@ -28,7 +27,6 @@ internal class RetrofitsImpl(
|
||||
cookieInterceptor: CookieInterceptor,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
json: Json,
|
||||
private val permissionInterceptor: PermissionInterceptor,
|
||||
private val certificateProvider: CertificateProvider,
|
||||
private val logHttpBody: Boolean = false,
|
||||
) : Retrofits {
|
||||
@@ -74,7 +72,6 @@ internal class RetrofitsImpl(
|
||||
baseClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(permissionInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
@@ -132,7 +129,6 @@ internal class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(permissionInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
@@ -147,7 +143,6 @@ internal class RetrofitsImpl(
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.addInterceptor(permissionInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package com.bitwarden.network.interceptor
|
||||
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
import com.bitwarden.network.provider.PermissionProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import okhttp3.Request
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class PermissionInterceptorTest {
|
||||
private val permissionProvider = mockk<PermissionProvider>()
|
||||
private val interceptor = PermissionInterceptor(
|
||||
permissionProvider = permissionProvider,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(InetAddress::class)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(InetAddress::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should proceed when local network access permission is granted`() {
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns true
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://192.168.1.1/api").build(),
|
||||
)
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals(200, response.code)
|
||||
verify(exactly = 1) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should proceed when request is not targeting the local network`() {
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
every { InetAddress.getByName("8.8.8.8") } returns mockk {
|
||||
every { isSiteLocalAddress } returns false
|
||||
}
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://8.8.8.8/api").build(),
|
||||
)
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals(200, response.code)
|
||||
verify(exactly = 1) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `intercept should acquire permission and throw when request targets local network without permission`() {
|
||||
val errorMessage = "Local network access required"
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
every { permissionProvider.errorMessageString } returns errorMessage
|
||||
every { permissionProvider.acquireLocalNetworkAccessPermission() } just runs
|
||||
every { InetAddress.getByName("192.168.1.1") } returns mockk {
|
||||
every { isSiteLocalAddress } returns true
|
||||
}
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://192.168.1.1/api").build(),
|
||||
)
|
||||
|
||||
val exception = assertThrows(LocalNetworkAccessException::class.java) {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
|
||||
assertEquals(errorMessage, exception.message)
|
||||
verify(exactly = 1) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should rethrow UnknownHostException when DNS resolution fails`() {
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
every { InetAddress.getByName(any()) } throws UnknownHostException("unknownhost")
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://unknownhost/api").build(),
|
||||
)
|
||||
|
||||
assertThrows(UnknownHostException::class.java) {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `intercept should treat request as local and throw when SecurityException occurs during DNS resolution`() {
|
||||
val errorMessage = "Local network access required"
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
every { permissionProvider.errorMessageString } returns errorMessage
|
||||
every { permissionProvider.acquireLocalNetworkAccessPermission() } just runs
|
||||
every { InetAddress.getByName(any()) } throws SecurityException("permission denied")
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://somehost/api").build(),
|
||||
)
|
||||
|
||||
val exception = assertThrows(LocalNetworkAccessException::class.java) {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
|
||||
assertEquals(errorMessage, exception.message)
|
||||
verify(exactly = 1) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.exception.LocalNetworkAccessException
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -36,29 +35,6 @@ class BitwardenErrorTest {
|
||||
assertTrue(body?.contains(message) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with LocalNetworkAccessException should return Http with status 400`() {
|
||||
val exception = LocalNetworkAccessException(message = "Fail!")
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
assertTrue(result is BitwardenError.Http)
|
||||
val httpError = result as BitwardenError.Http
|
||||
assertEquals(400, httpError.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with LocalNetworkAccessException should include message in body`() {
|
||||
val message = "Fail!"
|
||||
val exception = LocalNetworkAccessException(message = message)
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
val httpError = result as BitwardenError.Http
|
||||
val body = httpError.responseBodyString
|
||||
assertTrue(body?.contains(message) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with IOException should return Network`() {
|
||||
val exception = IOException("network failure")
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.bitwarden.network.interceptor.AuthTokenManager
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.CookieInterceptor
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.interceptor.PermissionInterceptor
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import io.mockk.MockKMatcherScope
|
||||
@@ -51,9 +50,6 @@ class RetrofitsTest {
|
||||
private val cookieInterceptor = mockk<CookieInterceptor> {
|
||||
mockIntercept { isCookieInterceptorCalled = true }
|
||||
}
|
||||
private val permissionInterceptor = mockk<PermissionInterceptor> {
|
||||
mockIntercept { isPermissionInterceptorCalled = true }
|
||||
}
|
||||
private val headersInterceptors = mockk<HeadersInterceptor> {
|
||||
mockIntercept { isHeadersInterceptorCalled = true }
|
||||
}
|
||||
@@ -69,7 +65,6 @@ class RetrofitsTest {
|
||||
authTokenManager = authTokenManager,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
cookieInterceptor = cookieInterceptor,
|
||||
permissionInterceptor = permissionInterceptor,
|
||||
headersInterceptor = headersInterceptors,
|
||||
certificateProvider = certificateProvider,
|
||||
json = json,
|
||||
@@ -78,7 +73,6 @@ class RetrofitsTest {
|
||||
private var isAuthInterceptorCalled = false
|
||||
private var isApiInterceptorCalled = false
|
||||
private var isCookieInterceptorCalled = false
|
||||
private var isPermissionInterceptorCalled = false
|
||||
private var isHeadersInterceptorCalled = false
|
||||
private var isIdentityInterceptorCalled = false
|
||||
private var isEventsInterceptorCalled = false
|
||||
@@ -182,7 +176,6 @@ class RetrofitsTest {
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
@@ -202,7 +195,6 @@ class RetrofitsTest {
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertTrue(isEventsInterceptorCalled)
|
||||
@@ -222,7 +214,6 @@ class RetrofitsTest {
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
@@ -242,7 +233,6 @@ class RetrofitsTest {
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertTrue(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
@@ -263,7 +253,6 @@ class RetrofitsTest {
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
@@ -284,7 +273,6 @@ class RetrofitsTest {
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isCookieInterceptorCalled)
|
||||
assertTrue(isPermissionInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
@@ -306,7 +294,6 @@ class RetrofitsTest {
|
||||
cookieInterceptor = cookieInterceptor,
|
||||
headersInterceptor = headersInterceptors,
|
||||
certificateProvider = certificateProvider,
|
||||
permissionInterceptor = permissionInterceptor,
|
||||
json = json,
|
||||
)
|
||||
|
||||
|
||||
@@ -1365,11 +1365,4 @@ Do you want to switch to this account?</string>
|
||||
<string name="birth_place">Birth place</string>
|
||||
<string name="national_identification_number">National identification number</string>
|
||||
<string name="copy_national_identification_number">Copy national identification number</string>
|
||||
<string name="access_your_local_network">Access your local network</string>
|
||||
<string name="local_network_access_required">Local network access required</string>
|
||||
<string name="ask_again_later">Ask again later</string>
|
||||
<string name="enable_local_network_access">Enable local network access</string>
|
||||
<string name="bitwarden_needs_local_network_access_to_sync_with_your_server">Bitwarden needs local network access to sync with your server. Without this permission, the app won’t be able to connect.</string>
|
||||
<string name="without_this_permission_bitwarden_wont_be_able_to_sync_with_your_server">Without this permission, Bitwarden won’t be able to connect and sync with your server. You can enable this in your device settings.</string>
|
||||
<string name="your_request_was_interrupted_because_the_app_needs_local_network_access">Your request was interrupted because the app needs local network access. You can enable this in your device settings.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user