mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 05:20:24 -05:00
Compare commits
19 Commits
android-co
...
v2024.11.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9673f5bd7 | ||
|
|
ef01f271bc | ||
|
|
966a521e0b | ||
|
|
4fb67825d3 | ||
|
|
c0c97af177 | ||
|
|
22efdc23a2 | ||
|
|
c977dfc877 | ||
|
|
afa1b598ad | ||
|
|
3c77933b3d | ||
|
|
b5752c10ed | ||
|
|
ec85e7af61 | ||
|
|
816b9769a1 | ||
|
|
25097cbae1 | ||
|
|
5a4b8d64ab | ||
|
|
5523d99400 | ||
|
|
9f8d21cb95 | ||
|
|
75fc9fe210 | ||
|
|
42671aadfb | ||
|
|
d71389ab02 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -382,7 +382,9 @@ jobs:
|
||||
|
||||
- name: Publish Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
||||
run: bundle exec fastlane publishBetaToPlayStore
|
||||
run: |
|
||||
bundle exec fastlane publishProdToPlayStore
|
||||
bundle exec fastlane publishBetaToPlayStore
|
||||
|
||||
publish_fdroid:
|
||||
name: Publish F-Droid artifacts
|
||||
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -6,6 +6,10 @@
|
||||
# we keep it here.
|
||||
-keep class com.bitwarden.** { *; }
|
||||
|
||||
# The Android Verifier component must be kept because it looks like dead code. Proguard is unable to
|
||||
# see any JNI usage, so our rules must manually opt into keeping it.
|
||||
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||
|
||||
################################################################################
|
||||
# Bitwarden Models
|
||||
################################################################################
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f28200334a5c94feed1d9712e04ff01b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherType",
|
||||
"columnName": "cipher_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cipherJson",
|
||||
"columnName": "cipher_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ciphers_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "collections",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "organizationId",
|
||||
"columnName": "organization_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "shouldHidePasswords",
|
||||
"columnName": "should_hide_passwords",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "externalId",
|
||||
"columnName": "external_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collections_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "domains",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domainsJson",
|
||||
"columnName": "domains_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "folders",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "revisionDate",
|
||||
"columnName": "revision_date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_folders_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "sends",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendType",
|
||||
"columnName": "send_type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sendJson",
|
||||
"columnName": "send_json",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_sends_user_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f28200334a5c94feed1d9712e04ff01b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -190,12 +190,14 @@ class MainViewModel @Inject constructor(
|
||||
private fun handleAccessibilitySelectionReceive(
|
||||
action: MainAction.Internal.AccessibilitySelectionReceive,
|
||||
) {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
private fun handleAutofillSelectionReceive(
|
||||
action: MainAction.Internal.AutofillSelectionReceive,
|
||||
) {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountReque
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.POST
|
||||
@@ -18,43 +19,43 @@ interface AuthenticatedAccountsApi {
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
@POST("/accounts/convert-to-key-connector")
|
||||
suspend fun convertToKeyConnector(): Result<Unit>
|
||||
suspend fun convertToKeyConnector(): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Creates the keys for the current account.
|
||||
*/
|
||||
@POST("/accounts/keys")
|
||||
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result<Unit>
|
||||
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Deletes the current account.
|
||||
*/
|
||||
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
|
||||
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
|
||||
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/request-otp")
|
||||
suspend fun requestOtp(): Result<Unit>
|
||||
suspend fun requestOtp(): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/verify-otp")
|
||||
suspend fun verifyOtp(
|
||||
@Body body: VerifyOtpRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Resets the temporary password.
|
||||
*/
|
||||
@HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true)
|
||||
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Resets the password.
|
||||
*/
|
||||
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
|
||||
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Sets the password.
|
||||
*/
|
||||
@POST("/accounts/set-password")
|
||||
suspend fun setPassword(@Body body: SetPasswordRequestJson): Result<Unit>
|
||||
suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -22,7 +23,7 @@ interface AuthenticatedAuthRequestsApi {
|
||||
suspend fun createAdminAuthRequest(
|
||||
@Header("Device-Identifier") deviceIdentifier: String,
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Updates an authentication request.
|
||||
@@ -31,13 +32,13 @@ interface AuthenticatedAuthRequestsApi {
|
||||
suspend fun updateAuthRequest(
|
||||
@Path("id") userId: String,
|
||||
@Body body: AuthRequestUpdateRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Gets a list of auth requests for this device.
|
||||
*/
|
||||
@GET("/auth-requests")
|
||||
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
|
||||
suspend fun getAuthRequests(): NetworkResult<AuthRequestsResponseJson>
|
||||
|
||||
/**
|
||||
* Retrieves an existing authentication request by ID.
|
||||
@@ -45,5 +46,5 @@ interface AuthenticatedAuthRequestsApi {
|
||||
@GET("/auth-requests/{requestId}")
|
||||
suspend fun getAuthRequest(
|
||||
@Path("requestId") requestId: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
@@ -16,5 +17,5 @@ interface AuthenticatedDevicesApi {
|
||||
suspend fun updateTrustedDeviceKeys(
|
||||
@Path(value = "appId") appId: String,
|
||||
@Body request: TrustedDeviceKeysRequestJson,
|
||||
): Result<TrustedDeviceKeysResponseJson>
|
||||
): NetworkResult<TrustedDeviceKeysResponseJson>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
@@ -15,5 +16,5 @@ interface AuthenticatedKeyConnectorApi {
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.PUT
|
||||
@@ -20,7 +21,7 @@ interface AuthenticatedOrganizationApi {
|
||||
@Path("orgId") organizationId: String,
|
||||
@Path("userId") userId: String,
|
||||
@Body body: OrganizationResetPasswordEnrollRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Checks whether this organization auto enrolls users in password reset.
|
||||
@@ -28,7 +29,7 @@ interface AuthenticatedOrganizationApi {
|
||||
@GET("/organizations/{identifier}/auto-enroll-status")
|
||||
suspend fun getOrganizationAutoEnrollResponse(
|
||||
@Path("identifier") organizationIdentifier: String,
|
||||
): Result<OrganizationAutoEnrollStatusResponseJson>
|
||||
): NetworkResult<OrganizationAutoEnrollStatusResponseJson>
|
||||
|
||||
/**
|
||||
* Gets the public and private keys for this organization.
|
||||
@@ -36,5 +37,5 @@ interface AuthenticatedOrganizationApi {
|
||||
@GET("/organizations/{id}/keys")
|
||||
suspend fun getOrganizationKeys(
|
||||
@Path("id") organizationId: String,
|
||||
): Result<OrganizationKeysResponseJson>
|
||||
): NetworkResult<OrganizationKeysResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
@@ -14,5 +15,5 @@ interface HaveIBeenPwnedApi {
|
||||
suspend fun fetchBreachedPasswords(
|
||||
@Path("hashPrefix")
|
||||
hashPrefix: String,
|
||||
): Result<ResponseBody>
|
||||
): NetworkResult<ResponseBody>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
@@ -15,16 +16,16 @@ interface UnauthenticatedAccountsApi {
|
||||
@POST("/accounts/password-hint")
|
||||
suspend fun passwordHintRequest(
|
||||
@Body body: PasswordHintRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/two-factor/send-email-login")
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
@Body body: ResendEmailRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/set-key-connector-key")
|
||||
suspend fun setKeyConnectorKey(
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -21,7 +22,7 @@ interface UnauthenticatedAuthRequestsApi {
|
||||
suspend fun createAuthRequest(
|
||||
@Header("Device-Identifier") deviceIdentifier: String,
|
||||
@Body body: AuthRequestRequestJson,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
|
||||
/**
|
||||
* Queries for updates to a given auth request.
|
||||
@@ -30,5 +31,5 @@ interface UnauthenticatedAuthRequestsApi {
|
||||
suspend fun getAuthRequestUpdate(
|
||||
@Path("requestId") requestId: String,
|
||||
@Query("code") accessCode: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest>
|
||||
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
|
||||
@@ -11,5 +12,5 @@ interface UnauthenticatedDevicesApi {
|
||||
suspend fun getIsKnownDevice(
|
||||
@Header(value = "X-Request-Email") emailAddress: String,
|
||||
@Header(value = "X-Device-Identifier") deviceId: String,
|
||||
): Result<Boolean>
|
||||
): NetworkResult<Boolean>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
@@ -46,12 +47,12 @@ interface UnauthenticatedIdentityApi {
|
||||
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
|
||||
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
|
||||
@Field(value = "authRequest") authRequestId: String?,
|
||||
): Result<GetTokenResponseJson.Success>
|
||||
): NetworkResult<GetTokenResponseJson.Success>
|
||||
|
||||
@GET("/sso/prevalidate")
|
||||
suspend fun prevalidateSso(
|
||||
@Query("domainHint") organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson>
|
||||
): NetworkResult<PrevalidateSsoResponseJson>
|
||||
|
||||
/**
|
||||
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
|
||||
@@ -66,23 +67,25 @@ interface UnauthenticatedIdentityApi {
|
||||
): Call<RefreshTokenResponseJson>
|
||||
|
||||
@POST("/accounts/prelogin")
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult<PreLoginResponseJson>
|
||||
|
||||
@POST("/accounts/register")
|
||||
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
|
||||
suspend fun register(
|
||||
@Body body: RegisterRequestJson,
|
||||
): NetworkResult<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/finish")
|
||||
suspend fun registerFinish(
|
||||
@Body body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson.Success>
|
||||
): NetworkResult<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/send-verification-email")
|
||||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): Result<JsonPrimitive?>
|
||||
): NetworkResult<JsonPrimitive?>
|
||||
|
||||
@POST("/accounts/register/verification-email-clicked")
|
||||
suspend fun verifyEmailToken(
|
||||
@Body body: VerifyEmailTokenRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
@@ -20,11 +21,11 @@ interface UnauthenticatedKeyConnectorApi {
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
@GET
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
): NetworkResult<KeyConnectorMasterKeyResponseJson>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
@@ -15,5 +16,5 @@ interface UnauthenticatedOrganizationApi {
|
||||
@POST("/organizations/domain/sso/details")
|
||||
suspend fun getClaimedDomainOrganizationDetails(
|
||||
@Body body: OrganizationDomainSsoDetailsRequestJson,
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson>
|
||||
): NetworkResult<OrganizationDomainSsoDetailsResponseJson>
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJs
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
@@ -37,18 +38,22 @@ class AccountsServiceImpl(
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
override suspend fun convertToKeyConnector(): Result<Unit> =
|
||||
authenticatedAccountsApi.convertToKeyConnector()
|
||||
authenticatedAccountsApi
|
||||
.convertToKeyConnector()
|
||||
.toResult()
|
||||
|
||||
override suspend fun createAccountKeys(
|
||||
publicKey: String,
|
||||
encryptedPrivateKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedAccountsApi.createAccountKeys(
|
||||
body = CreateAccountKeysRequest(
|
||||
publicKey = publicKey,
|
||||
encryptedPrivateKey = encryptedPrivateKey,
|
||||
),
|
||||
)
|
||||
authenticatedAccountsApi
|
||||
.createAccountKeys(
|
||||
body = CreateAccountKeysRequest(
|
||||
publicKey = publicKey,
|
||||
encryptedPrivateKey = encryptedPrivateKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun deleteAccount(
|
||||
masterPasswordHash: String?,
|
||||
@@ -61,9 +66,8 @@ class AccountsServiceImpl(
|
||||
oneTimePassword = oneTimePassword,
|
||||
),
|
||||
)
|
||||
.map {
|
||||
DeleteAccountResponseJson.Success
|
||||
}
|
||||
.toResult()
|
||||
.map { DeleteAccountResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
@@ -75,20 +79,25 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun requestOneTimePasscode(): Result<Unit> =
|
||||
authenticatedAccountsApi.requestOtp()
|
||||
authenticatedAccountsApi
|
||||
.requestOtp()
|
||||
.toResult()
|
||||
|
||||
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
|
||||
authenticatedAccountsApi.verifyOtp(
|
||||
VerifyOtpRequestJson(
|
||||
oneTimePasscode = passcode,
|
||||
),
|
||||
)
|
||||
authenticatedAccountsApi
|
||||
.verifyOtp(
|
||||
VerifyOtpRequestJson(
|
||||
oneTimePasscode = passcode,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun requestPasswordHint(
|
||||
email: String,
|
||||
): Result<PasswordHintResponseJson> =
|
||||
unauthenticatedAccountsApi
|
||||
.passwordHintRequest(PasswordHintRequestJson(email))
|
||||
.toResult()
|
||||
.map { PasswordHintResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -101,54 +110,70 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
|
||||
unauthenticatedAccountsApi
|
||||
.resendVerificationCodeEmail(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
|
||||
return if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi.resetTempPassword(body = body)
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||
if (body.currentPasswordHash == null) {
|
||||
authenticatedAccountsApi
|
||||
.resetTempPassword(body = body)
|
||||
.toResult()
|
||||
} else {
|
||||
authenticatedAccountsApi.resetPassword(body = body)
|
||||
authenticatedAccountsApi
|
||||
.resetPassword(body = body)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
|
||||
body = body,
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
): Result<Unit> =
|
||||
unauthenticatedAccountsApi
|
||||
.setKeyConnectorKey(
|
||||
body = body,
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun setPassword(
|
||||
body: SetPasswordRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
|
||||
): Result<Unit> = authenticatedAccountsApi
|
||||
.setPassword(body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
unauthenticatedKeyConnectorApi
|
||||
.getMasterKeyFromKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
authenticatedKeyConnectorApi
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
unauthenticatedKeyConnectorApi
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class AuthRequestsServiceImpl(
|
||||
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
|
||||
) : AuthRequestsService {
|
||||
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
|
||||
authenticatedAuthRequestsApi.getAuthRequests()
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequests()
|
||||
.toResult()
|
||||
|
||||
override suspend fun getAuthRequest(
|
||||
requestId: String,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId)
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequest(requestId = requestId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun updateAuthRequest(
|
||||
requestId: String,
|
||||
@@ -22,13 +27,15 @@ class AuthRequestsServiceImpl(
|
||||
deviceId: String,
|
||||
isApproved: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
authenticatedAuthRequestsApi.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
authenticatedAuthRequestsApi
|
||||
.updateAuthRequest(
|
||||
userId = requestId,
|
||||
body = AuthRequestUpdateRequestJson(
|
||||
key = key,
|
||||
masterPasswordHash = masterPasswordHash,
|
||||
deviceId = deviceId,
|
||||
isApproved = isApproved,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevic
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class DevicesServiceImpl(
|
||||
private val authenticatedDevicesApi: AuthenticatedDevicesApi,
|
||||
@@ -13,22 +14,26 @@ class DevicesServiceImpl(
|
||||
override suspend fun getIsKnownDevice(
|
||||
emailAddress: String,
|
||||
deviceId: String,
|
||||
): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice(
|
||||
emailAddress = emailAddress.base64UrlEncode(),
|
||||
deviceId = deviceId,
|
||||
)
|
||||
): Result<Boolean> = unauthenticatedDevicesApi
|
||||
.getIsKnownDevice(
|
||||
emailAddress = emailAddress.base64UrlEncode(),
|
||||
deviceId = deviceId,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun trustDevice(
|
||||
appId: String,
|
||||
encryptedUserKey: String,
|
||||
encryptedDevicePublicKey: String,
|
||||
encryptedDevicePrivateKey: String,
|
||||
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys(
|
||||
appId = appId,
|
||||
request = TrustedDeviceKeysRequestJson(
|
||||
encryptedUserKey = encryptedUserKey,
|
||||
encryptedDevicePublicKey = encryptedDevicePublicKey,
|
||||
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
|
||||
),
|
||||
)
|
||||
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi
|
||||
.updateTrustedDeviceKeys(
|
||||
appId = appId,
|
||||
request = TrustedDeviceKeysRequestJson(
|
||||
encryptedUserKey = encryptedUserKey,
|
||||
encryptedDevicePublicKey = encryptedDevicePublicKey,
|
||||
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import java.security.MessageDigest
|
||||
|
||||
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
|
||||
@@ -17,6 +18,7 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
|
||||
|
||||
return api
|
||||
.fetchBreachedPasswords(hashPrefix = hashPrefix)
|
||||
.toResult()
|
||||
.mapCatching { responseBody ->
|
||||
responseBody.string()
|
||||
// First split the response by newline: each hashed password is on a new line.
|
||||
|
||||
@@ -16,8 +16,9 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRe
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -28,12 +29,15 @@ class IdentityServiceImpl(
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
|
||||
unauthenticatedIdentityApi
|
||||
.preLogin(PreLoginRequestJson(email = email))
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
unauthenticatedIdentityApi
|
||||
.register(body)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
@@ -75,6 +79,7 @@ class IdentityServiceImpl(
|
||||
captchaResponse = captchaToken,
|
||||
authRequestId = authModel.authRequestId,
|
||||
)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
@@ -95,6 +100,7 @@ class IdentityServiceImpl(
|
||||
.prevalidateSso(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
@@ -104,7 +110,8 @@ class IdentityServiceImpl(
|
||||
grantType = "refresh_token",
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
.executeForResult()
|
||||
.executeForNetworkResult()
|
||||
.toResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
@@ -112,6 +119,7 @@ class IdentityServiceImpl(
|
||||
): Result<RegisterResponseJson> =
|
||||
unauthenticatedIdentityApi
|
||||
.registerFinish(body)
|
||||
.toResult()
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
@@ -127,6 +135,7 @@ class IdentityServiceImpl(
|
||||
): Result<String?> {
|
||||
return unauthenticatedIdentityApi
|
||||
.sendVerificationEmail(body = body)
|
||||
.toResult()
|
||||
.map { it?.content }
|
||||
}
|
||||
|
||||
@@ -136,9 +145,8 @@ class IdentityServiceImpl(
|
||||
.verifyEmailToken(
|
||||
body = body,
|
||||
)
|
||||
.map {
|
||||
VerifyEmailTokenResponseJson.Valid
|
||||
}
|
||||
.toResult()
|
||||
.map { VerifyEmailTokenResponseJson.Valid }
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthR
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
||||
/**
|
||||
@@ -24,17 +25,19 @@ class NewAuthRequestServiceImpl(
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
when (authRequestType) {
|
||||
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
|
||||
unauthenticatedAuthRequestsApi.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
unauthenticatedAuthRequestsApi
|
||||
.createAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.UNLOCK -> {
|
||||
@@ -43,17 +46,19 @@ class NewAuthRequestServiceImpl(
|
||||
}
|
||||
|
||||
AuthRequestTypeJson.ADMIN_APPROVAL -> {
|
||||
authenticatedAuthRequestsApi.createAdminAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
authenticatedAuthRequestsApi
|
||||
.createAdminAuthRequest(
|
||||
deviceIdentifier = deviceId,
|
||||
body = AuthRequestRequestJson(
|
||||
email = email,
|
||||
publicKey = publicKey,
|
||||
deviceId = deviceId,
|
||||
accessCode = accessCode,
|
||||
fingerprint = fingerprint,
|
||||
type = authRequestType,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +68,15 @@ class NewAuthRequestServiceImpl(
|
||||
isSso: Boolean,
|
||||
): Result<AuthRequestsResponseJson.AuthRequest> =
|
||||
if (isSso) {
|
||||
authenticatedAuthRequestsApi.getAuthRequest(requestId)
|
||||
authenticatedAuthRequestsApi
|
||||
.getAuthRequest(requestId = requestId)
|
||||
.toResult()
|
||||
} else {
|
||||
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
unauthenticatedAuthRequestsApi
|
||||
.getAuthRequestUpdate(
|
||||
requestId = requestId,
|
||||
accessCode = accessCode,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomain
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
/**
|
||||
* Default implementation of [OrganizationService].
|
||||
@@ -29,6 +30,7 @@ class OrganizationServiceImpl(
|
||||
resetPasswordKey = resetPasswordKey,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
@@ -38,6 +40,7 @@ class OrganizationServiceImpl(
|
||||
email = email,
|
||||
),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationAutoEnrollStatus(
|
||||
organizationIdentifier: String,
|
||||
@@ -45,6 +48,7 @@ class OrganizationServiceImpl(
|
||||
.getOrganizationAutoEnrollResponse(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getOrganizationKeys(
|
||||
organizationId: String,
|
||||
@@ -52,4 +56,5 @@ class OrganizationServiceImpl(
|
||||
.getOrganizationKeys(
|
||||
organizationId = organizationId,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class BitwardenAccessibilityService : AccessibilityService() {
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
if (rootInActiveWindow?.packageName != event.packageName) return
|
||||
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = rootInActiveWindow)
|
||||
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source)
|
||||
}
|
||||
|
||||
override fun onInterrupt() = Unit
|
||||
|
||||
@@ -26,18 +26,18 @@ class AccessibilityCompletionManagerImpl(
|
||||
.intent
|
||||
?.getAutofillSelectionDataOrNull()
|
||||
?: run {
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
return
|
||||
}
|
||||
if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) {
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
return
|
||||
}
|
||||
val uri = autofillSelectionData
|
||||
.uri
|
||||
?.toUriOrNull()
|
||||
?: run {
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class AccessibilityCompletionManagerImpl(
|
||||
)
|
||||
mainScope.launch {
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
activity.finish()
|
||||
}
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
|
||||
@@ -15,5 +16,5 @@ interface DigitalAssetLinkApi {
|
||||
@GET
|
||||
suspend fun getDigitalAssetLinks(
|
||||
@Url url: String,
|
||||
): Result<List<DigitalAssetLinkResponseJson>>
|
||||
): NetworkResult<List<DigitalAssetLinkResponseJson>>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api.DigitalAssetLinkApi
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
/**
|
||||
* Primary implementation of [DigitalAssetLinkService].
|
||||
@@ -18,4 +19,5 @@ class DigitalAssetLinkServiceImpl(
|
||||
.getDigitalAssetLinks(
|
||||
url = "$scheme$relyingParty/.well-known/assetlinks.json",
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ fun createAutofillSelectionIntent(
|
||||
.apply {
|
||||
// This helps prevent a crash when using the accessibility framework
|
||||
if (framework == AutofillSelectionData.Framework.ACCESSIBILITY) {
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
putExtra(
|
||||
AUTOFILL_BUNDLE_KEY,
|
||||
|
||||
@@ -74,6 +74,11 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
var lastDatabaseSchemeChangeInstant: Instant?
|
||||
|
||||
/**
|
||||
* Emits updates that track [lastDatabaseSchemeChangeInstant].
|
||||
*/
|
||||
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
|
||||
@@ -71,6 +71,8 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -154,7 +156,14 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
|
||||
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
||||
set(value) {
|
||||
putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
||||
mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
get() = mutableLastDatabaseSchemeChangeInstantFlow
|
||||
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.http.GET
|
||||
|
||||
/**
|
||||
@@ -9,5 +10,5 @@ import retrofit2.http.GET
|
||||
interface ConfigApi {
|
||||
|
||||
@GET("config")
|
||||
suspend fun getConfig(): Result<ConfigResponseJson>
|
||||
suspend fun getConfig(): NetworkResult<ConfigResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
@@ -9,5 +10,7 @@ import retrofit2.http.POST
|
||||
*/
|
||||
interface EventApi {
|
||||
@POST("/collect")
|
||||
suspend fun collectOrganizationEvents(@Body events: List<OrganizationEventJson>): Result<Unit>
|
||||
suspend fun collectOrganizationEvents(
|
||||
@Body events: List<OrganizationEventJson>,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.PUT
|
||||
@@ -13,5 +14,5 @@ interface PushApi {
|
||||
suspend fun putDeviceToken(
|
||||
@Path("appId") appId: String,
|
||||
@Body body: PushTokenRequest,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import okhttp3.Request
|
||||
import okio.IOException
|
||||
import okio.Timeout
|
||||
@@ -18,33 +17,36 @@ import java.lang.reflect.Type
|
||||
private const val NO_CONTENT_RESPONSE_CODE: Int = 204
|
||||
|
||||
/**
|
||||
* A [Call] for wrapping a network request into a [Result].
|
||||
* A [Call] for wrapping a network request into a [NetworkResult].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ResultCall<T>(
|
||||
class NetworkResultCall<T>(
|
||||
private val backingCall: Call<T>,
|
||||
private val successType: Type,
|
||||
) : Call<Result<T>> {
|
||||
) : Call<NetworkResult<T>> {
|
||||
override fun cancel(): Unit = backingCall.cancel()
|
||||
|
||||
override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)
|
||||
override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(backingCall, successType)
|
||||
|
||||
override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
|
||||
override fun enqueue(callback: Callback<NetworkResult<T>>): Unit = backingCall.enqueue(
|
||||
object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
|
||||
callback.onResponse(
|
||||
this@NetworkResultCall,
|
||||
Response.success(response.toNetworkResult()),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
|
||||
callback.onResponse(this@NetworkResultCall, Response.success(t.toFailure()))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun execute(): Response<Result<T>> =
|
||||
override fun execute(): Response<NetworkResult<T>> =
|
||||
try {
|
||||
Response.success(backingCall.execute().toResult())
|
||||
Response.success(backingCall.execute().toNetworkResult())
|
||||
} catch (ioException: IOException) {
|
||||
Response.success(ioException.toFailure())
|
||||
} catch (runtimeException: RuntimeException) {
|
||||
@@ -60,16 +62,18 @@ class ResultCall<T>(
|
||||
override fun timeout(): Timeout = backingCall.timeout()
|
||||
|
||||
/**
|
||||
* Synchronously send the request and return its response as a [Result].
|
||||
* Synchronously send the request and return its response as a [NetworkResult].
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
fun executeForResult(): NetworkResult<T> = requireNotNull(execute().body())
|
||||
|
||||
private fun Throwable.toFailure(): Result<T> =
|
||||
this
|
||||
.also { Timber.w(it, "Network Error: ${backingCall.request().url}") }
|
||||
.asFailure()
|
||||
private fun Throwable.toFailure(): NetworkResult<T> {
|
||||
// We rebuild the URL without query params, we do not want to log those
|
||||
val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" }
|
||||
Timber.w(this, "Network Error: $url")
|
||||
return NetworkResult.Failure(this)
|
||||
}
|
||||
|
||||
private fun Response<T>.toResult(): Result<T> =
|
||||
private fun Response<T>.toNetworkResult(): NetworkResult<T> =
|
||||
if (!this.isSuccessful) {
|
||||
HttpException(this).toFailure()
|
||||
} else {
|
||||
@@ -77,11 +81,11 @@ class ResultCall<T>(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when {
|
||||
// We got a nonnull T as the body, just return it.
|
||||
body != null -> body.asSuccess()
|
||||
body != null -> NetworkResult.Success(body)
|
||||
// We expected the body to be null since the successType is Unit, just return Unit.
|
||||
successType == Unit::class.java -> (Unit as T).asSuccess()
|
||||
successType == Unit::class.java -> NetworkResult.Success(Unit as T)
|
||||
// We allow null for 204's, just return null.
|
||||
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
|
||||
this.code() == NO_CONTENT_RESPONSE_CODE -> NetworkResult.Success(null as T)
|
||||
// All other null bodies result in an error.
|
||||
else -> IllegalStateException("Unexpected null body!").toFailure()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A [CallAdapter] for wrapping network requests into [NetworkResult].
|
||||
*/
|
||||
class NetworkResultCallAdapter<T>(
|
||||
private val successType: Type,
|
||||
) : CallAdapter<T, Call<NetworkResult<T>>> {
|
||||
|
||||
override fun responseType(): Type = successType
|
||||
override fun adapt(call: Call<T>): Call<NetworkResult<T>> = NetworkResultCall(call, successType)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
@@ -7,9 +8,9 @@ import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A [CallAdapter.Factory] for wrapping network requests into [kotlin.Result].
|
||||
* A [CallAdapter.Factory] for wrapping network requests into [NetworkResult].
|
||||
*/
|
||||
class ResultCallAdapterFactory : CallAdapter.Factory() {
|
||||
class NetworkResultCallAdapterFactory : CallAdapter.Factory() {
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
@@ -18,13 +19,13 @@ class ResultCallAdapterFactory : CallAdapter.Factory() {
|
||||
check(returnType is ParameterizedType) { "$returnType must be parameterized" }
|
||||
val containerType = getParameterUpperBound(0, returnType)
|
||||
|
||||
if (getRawType(containerType) != Result::class.java) return null
|
||||
if (getRawType(containerType) != NetworkResult::class.java) return null
|
||||
check(containerType is ParameterizedType) { "$containerType must be parameterized" }
|
||||
|
||||
val requestType = getParameterUpperBound(0, containerType)
|
||||
|
||||
return if (getRawType(returnType) == Call::class.java) {
|
||||
ResultCallAdapter<Any>(successType = requestType)
|
||||
NetworkResultCallAdapter<Any>(successType = requestType)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* A [CallAdapter] for wrapping network requests into [kotlin.Result].
|
||||
*/
|
||||
class ResultCallAdapter<T>(
|
||||
private val successType: Type,
|
||||
) : CallAdapter<T, Call<Result<T>>> {
|
||||
|
||||
override fun responseType(): Type = successType
|
||||
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call, successType)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* A wrapper class for a network result for type [T]. If the network request is successful, the
|
||||
* response will be a [Success] containing the data. If the network request is a failure, the
|
||||
* response will be a [Failure] containing the [Throwable].
|
||||
*/
|
||||
@Keep
|
||||
sealed class NetworkResult<out T> {
|
||||
/**
|
||||
* A successful network result with the relevant [T] data.
|
||||
*/
|
||||
data class Success<T>(val value: T) : NetworkResult<T>()
|
||||
|
||||
/**
|
||||
* A failed network result with the relevant [throwable] error.
|
||||
*/
|
||||
data class Failure(val throwable: Throwable) : NetworkResult<Nothing>()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
@@ -105,7 +105,7 @@ class RetrofitsImpl(
|
||||
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
|
||||
Retrofit.Builder()
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.client(baseOkHttpClient)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* Base [KSerializer] for mapping an [Enum] with possible values given by [values] to/from integer
|
||||
* values, which should be specified using [SerialName].
|
||||
* values, which should be specified using [SerialName]. If a [default] value is provided, it will
|
||||
* be used when an unknown value is provided.
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
private val values: Array<T>,
|
||||
private val default: T? = null,
|
||||
) : KSerializer<T> {
|
||||
|
||||
override val descriptor: SerialDescriptor
|
||||
@@ -25,7 +27,9 @@ abstract class BaseEnumeratedIntSerializer<T : Enum<T>>(
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
val decodedValue = decoder.decodeInt().toString()
|
||||
return values.first { it.serialNameAnnotation?.value == decodedValue }
|
||||
return values.firstOrNull { it.serialNameAnnotation?.value == decodedValue }
|
||||
?: default
|
||||
?: throw IllegalArgumentException("Unknown value $decodedValue")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class ConfigServiceImpl(private val configApi: ConfigApi) : ConfigService {
|
||||
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig()
|
||||
override suspend fun getConfig(): Result<ConfigResponseJson> = configApi.getConfig().toResult()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.api.EventApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEventJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
/**
|
||||
* The default implementation of the [EventService].
|
||||
@@ -11,5 +12,5 @@ class EventServiceImpl(
|
||||
) : EventService {
|
||||
override suspend fun sendOrganizationEvents(
|
||||
events: List<OrganizationEventJson>,
|
||||
): Result<Unit> = eventApi.collectOrganizationEvents(events = events)
|
||||
): Result<Unit> = eventApi.collectOrganizationEvents(events = events).toResult()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.api.PushApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
|
||||
class PushServiceImpl(
|
||||
private val pushApi: PushApi,
|
||||
@@ -15,4 +16,5 @@ class PushServiceImpl(
|
||||
appId = appId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCall
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCall
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import retrofit2.Call
|
||||
|
||||
/**
|
||||
* Synchronously executes the [Call] and returns the [Result].
|
||||
* Synchronously executes the [Call] and returns the [NetworkResult].
|
||||
*/
|
||||
inline fun <reified T : Any> Call<T>.executeForResult(): Result<T> =
|
||||
inline fun <reified T : Any> Call<T>.executeForNetworkResult(): NetworkResult<T> =
|
||||
this
|
||||
.toResultCall()
|
||||
.toNetworkResultCall()
|
||||
.executeForResult()
|
||||
|
||||
/**
|
||||
* Wraps the existing [Call] in a [ResultCall].
|
||||
* Wraps the existing [Call] in a [NetworkResultCall].
|
||||
*/
|
||||
inline fun <reified T : Any> Call<T>.toResultCall(): ResultCall<T> =
|
||||
ResultCall(
|
||||
inline fun <reified T : Any> Call<T>.toNetworkResultCall(): NetworkResultCall<T> =
|
||||
NetworkResultCall(
|
||||
backingCall = this,
|
||||
successType = T::class.java,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
|
||||
/**
|
||||
* Converts the [NetworkResult] to a [Result].
|
||||
*/
|
||||
fun <T> NetworkResult<T>.toResult(): Result<T> =
|
||||
when (this) {
|
||||
is NetworkResult.Failure -> this.throwable.asFailure()
|
||||
is NetworkResult.Success -> this.value.asSuccess()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@@ -14,4 +15,9 @@ interface DatabaseSchemeManager {
|
||||
* that a scheme change to any database will update this value and trigger a sync.
|
||||
*/
|
||||
var lastDatabaseSchemeChangeInstant: Instant?
|
||||
|
||||
/**
|
||||
* A flow of the last database schema change instant.
|
||||
*/
|
||||
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@@ -8,10 +12,23 @@ import java.time.Instant
|
||||
*/
|
||||
class DatabaseSchemeManagerImpl(
|
||||
val settingsDiskSource: SettingsDiskSource,
|
||||
val dispatcherManager: DispatcherManager,
|
||||
) : DatabaseSchemeManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
|
||||
set(value) {
|
||||
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
|
||||
}
|
||||
|
||||
override val lastDatabaseSchemeChangeInstantFlow =
|
||||
settingsDiskSource
|
||||
.lastDatabaseSchemeChangeInstantFlow
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource.lastDatabaseSchemeChangeInstant,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -304,7 +304,9 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun provideDatabaseSchemeManager(
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -31,3 +33,34 @@ fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndLoggedIn(
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId?.let(observer) ?: flow { awaitCancellation() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the [observer] callback whenever the user is logged in, the active user changes, the
|
||||
* vault for the user changes, and there are subscribers to the [MutableStateFlow]. The flow from
|
||||
* all previous calls to the `observer` is canceled whenever the `observer` is re-invoked, there
|
||||
* is no active user (logged-out), there are no subscribers to the [MutableStateFlow], or the vault
|
||||
* is not unlocked.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow: Flow<UserStateJson?>,
|
||||
vaultUnlockFlow: Flow<List<VaultUnlockData>>,
|
||||
observer: (activeUserId: String) -> Flow<R>,
|
||||
): Flow<R> =
|
||||
combine(
|
||||
this.subscriptionCount.map { it > 0 }.distinctUntilChanged(),
|
||||
userStateFlow.map { it?.activeUserId }.distinctUntilChanged(),
|
||||
userStateFlow.map { it?.activeUserId }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
vaultUnlockFlow
|
||||
.map { it.any { it.userId == activeUserId } }
|
||||
.distinctUntilChanged()
|
||||
},
|
||||
) { isSubscribed, activeUserId, isUnlocked ->
|
||||
activeUserId.takeIf { isSubscribed && isUnlocked }
|
||||
}
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId?.let(observer) ?: flow { awaitCancellation() }
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class BitwardenAutofillTileService : TileService() {
|
||||
}
|
||||
accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptParseUri
|
||||
val intent = Intent(applicationContext, AccessibilityActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
|
||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -190,7 +191,7 @@ class GeneratorRepositoryImpl(
|
||||
|
||||
override suspend fun generateForwardedServiceUsername(
|
||||
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
||||
): GeneratedForwardedServiceUsernameResult =
|
||||
): GeneratedForwardedServiceUsernameResult = withContext(scope.coroutineContext) {
|
||||
generatorSdkSource.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
||||
.fold(
|
||||
onSuccess = { generatedEmail ->
|
||||
@@ -200,6 +201,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
||||
val userId = authDiskSource.userState?.activeUserId
|
||||
|
||||
@@ -37,7 +37,7 @@ interface VaultDiskSource {
|
||||
/**
|
||||
* Retrieves all domains from the data source for a given [userId].
|
||||
*/
|
||||
fun getDomains(userId: String): Flow<SyncResponseJson.Domains>
|
||||
fun getDomains(userId: String): Flow<SyncResponseJson.Domains?>
|
||||
|
||||
/**
|
||||
* Deletes a folder from the data source for the given [userId] and [folderId].
|
||||
|
||||
@@ -119,12 +119,12 @@ class VaultDiskSourceImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains> =
|
||||
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains?> =
|
||||
domainsDao
|
||||
.getDomains(userId)
|
||||
.map { entity ->
|
||||
withContext(dispatcherManager.default) {
|
||||
json.decodeFromString<SyncResponseJson.Domains>(entity.domainsJson)
|
||||
entity?.domainsJson?.let { json.decodeFromString<SyncResponseJson.Domains>(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ class VaultDiskSourceImpl(
|
||||
domainsDao.insertDomains(
|
||||
domains = DomainsEntity(
|
||||
userId = userId,
|
||||
domainsJson = json.encodeToString(vault.domains),
|
||||
domainsJson = vault.domains?.let { json.encodeToString(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ interface DomainsDao {
|
||||
@Query("SELECT * FROM domains WHERE user_id = :userId")
|
||||
fun getDomains(
|
||||
userId: String,
|
||||
): Flow<DomainsEntity>
|
||||
): Flow<DomainsEntity?>
|
||||
|
||||
/**
|
||||
* Inserts domains into the database.
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
||||
FolderEntity::class,
|
||||
SendEntity::class,
|
||||
],
|
||||
version = 3,
|
||||
version = 5,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
||||
|
||||
@@ -14,5 +14,5 @@ data class DomainsEntity(
|
||||
val userId: String,
|
||||
|
||||
@ColumnInfo(name = "domains_json")
|
||||
val domainsJson: String,
|
||||
val domainsJson: String?,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
@@ -21,5 +22,5 @@ interface AzureApi {
|
||||
@Header("x-ms-date") date: String,
|
||||
@Header("x-ms-version") version: String?,
|
||||
@Body body: RequestBody,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
|
||||
@@ -26,7 +27,7 @@ interface CiphersApi {
|
||||
* Create a cipher.
|
||||
*/
|
||||
@POST("ciphers")
|
||||
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
|
||||
suspend fun createCipher(@Body body: CipherJsonRequest): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Create a cipher that belongs to an organization.
|
||||
@@ -34,7 +35,7 @@ interface CiphersApi {
|
||||
@POST("ciphers/create")
|
||||
suspend fun createCipherInOrganization(
|
||||
@Body body: CreateCipherInOrganizationJsonRequest,
|
||||
): Result<SyncResponseJson.Cipher>
|
||||
): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Associates an attachment with a cipher.
|
||||
@@ -43,7 +44,7 @@ interface CiphersApi {
|
||||
suspend fun createAttachment(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Body body: AttachmentJsonRequest,
|
||||
): Result<AttachmentJsonResponse>
|
||||
): NetworkResult<AttachmentJsonResponse>
|
||||
|
||||
/**
|
||||
* Uploads the attachment associated with a cipher.
|
||||
@@ -53,7 +54,7 @@ interface CiphersApi {
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Path("attachmentId") attachmentId: String,
|
||||
@Body body: MultipartBody,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Updates a cipher.
|
||||
@@ -62,7 +63,7 @@ interface CiphersApi {
|
||||
suspend fun updateCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Body body: CipherJsonRequest,
|
||||
): Result<SyncResponseJson.Cipher>
|
||||
): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Shares a cipher.
|
||||
@@ -71,7 +72,7 @@ interface CiphersApi {
|
||||
suspend fun shareCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Body body: ShareCipherJsonRequest,
|
||||
): Result<SyncResponseJson.Cipher>
|
||||
): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Shares an attachment.
|
||||
@@ -82,7 +83,7 @@ interface CiphersApi {
|
||||
@Path("attachmentId") attachmentId: String,
|
||||
@Query("organizationId") organizationId: String?,
|
||||
@Body body: MultipartBody,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Updates a cipher's collections.
|
||||
@@ -91,7 +92,7 @@ interface CiphersApi {
|
||||
suspend fun updateCipherCollections(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Body body: UpdateCipherCollectionsJsonRequest,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Hard deletes a cipher.
|
||||
@@ -99,7 +100,7 @@ interface CiphersApi {
|
||||
@DELETE("ciphers/{cipherId}")
|
||||
suspend fun hardDeleteCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Soft deletes a cipher.
|
||||
@@ -107,7 +108,7 @@ interface CiphersApi {
|
||||
@PUT("ciphers/{cipherId}/delete")
|
||||
suspend fun softDeleteCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Deletes an attachment from a cipher.
|
||||
@@ -116,7 +117,7 @@ interface CiphersApi {
|
||||
suspend fun deleteCipherAttachment(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Path("attachmentId") attachmentId: String,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Restores a cipher.
|
||||
@@ -124,7 +125,7 @@ interface CiphersApi {
|
||||
@PUT("ciphers/{cipherId}/restore")
|
||||
suspend fun restoreCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
): Result<SyncResponseJson.Cipher>
|
||||
): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Gets a cipher.
|
||||
@@ -132,7 +133,7 @@ interface CiphersApi {
|
||||
@GET("ciphers/{cipherId}")
|
||||
suspend fun getCipher(
|
||||
@Path("cipherId") cipherId: String,
|
||||
): Result<SyncResponseJson.Cipher>
|
||||
): NetworkResult<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Gets a cipher attachment.
|
||||
@@ -141,11 +142,11 @@ interface CiphersApi {
|
||||
suspend fun getCipherAttachment(
|
||||
@Path("cipherId") cipherId: String,
|
||||
@Path("attachmentId") attachmentId: String,
|
||||
): Result<SyncResponseJson.Cipher.Attachment>
|
||||
): NetworkResult<SyncResponseJson.Cipher.Attachment>
|
||||
|
||||
/**
|
||||
* Indicates if the active user has unassigned ciphers.
|
||||
*/
|
||||
@GET("ciphers/has-unassigned-ciphers")
|
||||
suspend fun hasUnassignedCiphers(): Result<Boolean>
|
||||
suspend fun hasUnassignedCiphers(): NetworkResult<Boolean>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Streaming
|
||||
@@ -16,5 +17,5 @@ interface DownloadApi {
|
||||
@Streaming
|
||||
suspend fun getDataStream(
|
||||
@Url url: String,
|
||||
): Result<ResponseBody>
|
||||
): NetworkResult<ResponseBody>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import retrofit2.http.Body
|
||||
@@ -18,7 +19,7 @@ interface FoldersApi {
|
||||
* Create a folder.
|
||||
*/
|
||||
@POST("folders")
|
||||
suspend fun createFolder(@Body body: FolderJsonRequest): Result<SyncResponseJson.Folder>
|
||||
suspend fun createFolder(@Body body: FolderJsonRequest): NetworkResult<SyncResponseJson.Folder>
|
||||
|
||||
/**
|
||||
* Gets a folder.
|
||||
@@ -26,7 +27,7 @@ interface FoldersApi {
|
||||
@GET("folders/{folderId}")
|
||||
suspend fun getFolder(
|
||||
@Path("folderId") folderId: String,
|
||||
): Result<SyncResponseJson.Folder>
|
||||
): NetworkResult<SyncResponseJson.Folder>
|
||||
|
||||
/**
|
||||
* Updates a folder.
|
||||
@@ -35,11 +36,11 @@ interface FoldersApi {
|
||||
suspend fun updateFolder(
|
||||
@Path("folderId") folderId: String,
|
||||
@Body body: FolderJsonRequest,
|
||||
): Result<SyncResponseJson.Folder>
|
||||
): NetworkResult<SyncResponseJson.Folder>
|
||||
|
||||
/**
|
||||
* Deletes a folder.
|
||||
*/
|
||||
@DELETE("folders/{folderId}")
|
||||
suspend fun deleteFolder(@Path("folderId") folderId: String): Result<Unit>
|
||||
suspend fun deleteFolder(@Path("folderId") folderId: String): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
@@ -22,13 +23,15 @@ interface SendsApi {
|
||||
* Create a text send.
|
||||
*/
|
||||
@POST("sends")
|
||||
suspend fun createTextSend(@Body body: SendJsonRequest): Result<SyncResponseJson.Send>
|
||||
suspend fun createTextSend(@Body body: SendJsonRequest): NetworkResult<SyncResponseJson.Send>
|
||||
|
||||
/**
|
||||
* Create a file send.
|
||||
*/
|
||||
@POST("sends/file/v2")
|
||||
suspend fun createFileSend(@Body body: SendJsonRequest): Result<CreateFileSendResponseJson>
|
||||
suspend fun createFileSend(
|
||||
@Body body: SendJsonRequest,
|
||||
): NetworkResult<CreateFileSendResponseJson>
|
||||
|
||||
/**
|
||||
* Updates a send.
|
||||
@@ -37,7 +40,7 @@ interface SendsApi {
|
||||
suspend fun updateSend(
|
||||
@Path("sendId") sendId: String,
|
||||
@Body body: SendJsonRequest,
|
||||
): Result<SyncResponseJson.Send>
|
||||
): NetworkResult<SyncResponseJson.Send>
|
||||
|
||||
/**
|
||||
* Uploads the file associated with a send.
|
||||
@@ -47,23 +50,25 @@ interface SendsApi {
|
||||
@Path("sendId") sendId: String,
|
||||
@Path("fileId") fileId: String,
|
||||
@Body body: MultipartBody,
|
||||
): Result<Unit>
|
||||
): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Deletes a send.
|
||||
*/
|
||||
@DELETE("sends/{sendId}")
|
||||
suspend fun deleteSend(@Path("sendId") sendId: String): Result<Unit>
|
||||
suspend fun deleteSend(@Path("sendId") sendId: String): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Deletes a send.
|
||||
*/
|
||||
@PUT("sends/{sendId}/remove-password")
|
||||
suspend fun removeSendPassword(@Path("sendId") sendId: String): Result<SyncResponseJson.Send>
|
||||
suspend fun removeSendPassword(
|
||||
@Path("sendId") sendId: String,
|
||||
): NetworkResult<SyncResponseJson.Send>
|
||||
|
||||
/**
|
||||
* Gets a send.
|
||||
*/
|
||||
@GET("sends/{sendId}")
|
||||
suspend fun getSend(@Path("sendId") sendId: String): Result<SyncResponseJson.Send>
|
||||
suspend fun getSend(@Path("sendId") sendId: String): NetworkResult<SyncResponseJson.Send>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import retrofit2.http.GET
|
||||
|
||||
@@ -13,8 +14,8 @@ interface SyncApi {
|
||||
* @return A [SyncResponseJson] containing the vault response model.
|
||||
*/
|
||||
@GET("sync")
|
||||
suspend fun sync(): Result<SyncResponseJson>
|
||||
suspend fun sync(): NetworkResult<SyncResponseJson>
|
||||
|
||||
@GET("/accounts/revision-date")
|
||||
suspend fun getAccountRevisionDateMillis(): Result<Long>
|
||||
suspend fun getAccountRevisionDateMillis(): NetworkResult<Long>
|
||||
}
|
||||
|
||||
@@ -81,8 +81,19 @@ enum class PolicyTypeJson {
|
||||
*/
|
||||
@SerialName("11")
|
||||
ACTIVATE_AUTOFILL,
|
||||
|
||||
/**
|
||||
* Represents an unknown policy type.
|
||||
*
|
||||
* This is used for forward compatibility to handle new policy types that the client doesn't yet
|
||||
* understand.
|
||||
*/
|
||||
@SerialName("-1")
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class PolicyTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<PolicyTypeJson>(PolicyTypeJson.entries.toTypedArray())
|
||||
private class PolicyTypeSerializer : BaseEnumeratedIntSerializer<PolicyTypeJson>(
|
||||
values = PolicyTypeJson.entries.toTypedArray(),
|
||||
default = PolicyTypeJson.UNKNOWN,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@@ -21,6 +23,7 @@ private const val DEFAULT_FIDO_2_KEY_CURVE = "P-256"
|
||||
* @property domains A domains object associated with the vault data.
|
||||
* @property sends A list of send objects associated with the vault data (nullable).
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class SyncResponseJson(
|
||||
@SerialName("folders")
|
||||
@@ -30,6 +33,7 @@ data class SyncResponseJson(
|
||||
val collections: List<Collection>?,
|
||||
|
||||
@SerialName("profile")
|
||||
@JsonNames("Profile")
|
||||
val profile: Profile,
|
||||
|
||||
@SerialName("ciphers")
|
||||
@@ -39,7 +43,8 @@ data class SyncResponseJson(
|
||||
val policies: List<Policy>?,
|
||||
|
||||
@SerialName("domains")
|
||||
val domains: Domains,
|
||||
@JsonNames("Domains")
|
||||
val domains: Domains?,
|
||||
|
||||
@SerialName("sends")
|
||||
val sends: List<Send>?,
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.core.net.toUri
|
||||
import com.bitwarden.vault.Attachment
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
|
||||
@@ -34,20 +35,26 @@ class CiphersServiceImpl(
|
||||
private val clock: Clock,
|
||||
) : CiphersService {
|
||||
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
|
||||
ciphersApi.createCipher(body = body)
|
||||
ciphersApi
|
||||
.createCipher(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun createCipherInOrganization(
|
||||
body: CreateCipherInOrganizationJsonRequest,
|
||||
): Result<SyncResponseJson.Cipher> = ciphersApi.createCipherInOrganization(body = body)
|
||||
): Result<SyncResponseJson.Cipher> = ciphersApi
|
||||
.createCipherInOrganization(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun createAttachment(
|
||||
cipherId: String,
|
||||
body: AttachmentJsonRequest,
|
||||
): Result<AttachmentJsonResponse> =
|
||||
ciphersApi.createAttachment(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
ciphersApi
|
||||
.createAttachment(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun uploadAttachment(
|
||||
attachmentJsonResponse: AttachmentJsonResponse,
|
||||
@@ -82,6 +89,7 @@ class CiphersServiceImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
.toResult()
|
||||
.map { cipher }
|
||||
}
|
||||
|
||||
@@ -94,6 +102,7 @@ class CiphersServiceImpl(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
.map { UpdateCipherResponseJson.Success(cipher = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -115,77 +124,97 @@ class CiphersServiceImpl(
|
||||
?: return IllegalStateException("Attachment must have ID").asFailure()
|
||||
val attachmentKey = attachment.key
|
||||
?: return IllegalStateException("Attachment must have Key").asFailure()
|
||||
return ciphersApi.shareAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
organizationId = organizationId,
|
||||
body = this
|
||||
.createMultipartBodyBuilder(
|
||||
encryptedFile = encryptedFile,
|
||||
filename = attachment.fileName,
|
||||
)
|
||||
.addPart(
|
||||
part = MultipartBody.Part.createFormData(
|
||||
name = "key",
|
||||
value = attachmentKey,
|
||||
),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
return ciphersApi
|
||||
.shareAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
organizationId = organizationId,
|
||||
body = this
|
||||
.createMultipartBodyBuilder(
|
||||
encryptedFile = encryptedFile,
|
||||
filename = attachment.fileName,
|
||||
)
|
||||
.addPart(
|
||||
part = MultipartBody.Part.createFormData(
|
||||
name = "key",
|
||||
value = attachmentKey,
|
||||
),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
override suspend fun shareCipher(
|
||||
cipherId: String,
|
||||
body: ShareCipherJsonRequest,
|
||||
): Result<SyncResponseJson.Cipher> =
|
||||
ciphersApi.shareCipher(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
ciphersApi
|
||||
.shareCipher(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun updateCipherCollections(
|
||||
cipherId: String,
|
||||
body: UpdateCipherCollectionsJsonRequest,
|
||||
): Result<Unit> =
|
||||
ciphersApi.updateCipherCollections(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
ciphersApi
|
||||
.updateCipherCollections(
|
||||
cipherId = cipherId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun hardDeleteCipher(cipherId: String): Result<Unit> =
|
||||
ciphersApi.hardDeleteCipher(cipherId = cipherId)
|
||||
ciphersApi
|
||||
.hardDeleteCipher(cipherId = cipherId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun softDeleteCipher(cipherId: String): Result<Unit> =
|
||||
ciphersApi.softDeleteCipher(cipherId = cipherId)
|
||||
ciphersApi
|
||||
.softDeleteCipher(cipherId = cipherId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun deleteCipherAttachment(
|
||||
cipherId: String,
|
||||
attachmentId: String,
|
||||
): Result<Unit> =
|
||||
ciphersApi.deleteCipherAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
)
|
||||
ciphersApi
|
||||
.deleteCipherAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun restoreCipher(cipherId: String): Result<SyncResponseJson.Cipher> =
|
||||
ciphersApi.restoreCipher(cipherId = cipherId)
|
||||
ciphersApi
|
||||
.restoreCipher(cipherId = cipherId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getCipher(
|
||||
cipherId: String,
|
||||
): Result<SyncResponseJson.Cipher> =
|
||||
ciphersApi.getCipher(cipherId = cipherId)
|
||||
ciphersApi
|
||||
.getCipher(cipherId = cipherId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getCipherAttachment(
|
||||
cipherId: String,
|
||||
attachmentId: String,
|
||||
): Result<SyncResponseJson.Cipher.Attachment> =
|
||||
ciphersApi.getCipherAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
)
|
||||
ciphersApi
|
||||
.getCipherAttachment(
|
||||
cipherId = cipherId,
|
||||
attachmentId = attachmentId,
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun hasUnassignedCiphers(): Result<Boolean> =
|
||||
ciphersApi.hasUnassignedCiphers()
|
||||
ciphersApi
|
||||
.hasUnassignedCiphers()
|
||||
.toResult()
|
||||
|
||||
private fun createMultipartBodyBuilder(
|
||||
encryptedFile: File,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.DownloadApi
|
||||
import okhttp3.ResponseBody
|
||||
|
||||
@@ -12,5 +13,7 @@ class DownloadServiceImpl(
|
||||
override suspend fun getDataStream(
|
||||
url: String,
|
||||
): Result<ResponseBody> =
|
||||
downloadApi.getDataStream(url = url)
|
||||
downloadApi
|
||||
.getDataStream(url = url)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.FoldersApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.FolderJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
@@ -13,7 +14,9 @@ class FolderServiceImpl(
|
||||
private val json: Json,
|
||||
) : FolderService {
|
||||
override suspend fun createFolder(body: FolderJsonRequest): Result<SyncResponseJson.Folder> =
|
||||
foldersApi.createFolder(body = body)
|
||||
foldersApi
|
||||
.createFolder(body = body)
|
||||
.toResult()
|
||||
|
||||
override suspend fun updateFolder(
|
||||
folderId: String,
|
||||
@@ -24,6 +27,7 @@ class FolderServiceImpl(
|
||||
folderId = folderId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
.map { UpdateFolderResponseJson.Success(folder = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -36,10 +40,13 @@ class FolderServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun deleteFolder(folderId: String): Result<Unit> =
|
||||
foldersApi.deleteFolder(folderId = folderId)
|
||||
foldersApi
|
||||
.deleteFolder(folderId = folderId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getFolder(
|
||||
folderId: String,
|
||||
): Result<SyncResponseJson.Folder> = foldersApi
|
||||
.getFolder(folderId = folderId)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.SendsApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
|
||||
@@ -34,7 +35,9 @@ class SendsServiceImpl(
|
||||
override suspend fun createTextSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<CreateSendJsonResponse> =
|
||||
sendsApi.createTextSend(body = body)
|
||||
sendsApi
|
||||
.createTextSend(body = body)
|
||||
.toResult()
|
||||
.map { CreateSendJsonResponse.Success(send = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable.toBitwardenError()
|
||||
@@ -48,7 +51,9 @@ class SendsServiceImpl(
|
||||
override suspend fun createFileSend(
|
||||
body: SendJsonRequest,
|
||||
): Result<CreateFileSendResponse> =
|
||||
sendsApi.createFileSend(body = body)
|
||||
sendsApi
|
||||
.createFileSend(body = body)
|
||||
.toResult()
|
||||
.map { CreateFileSendResponse.Success(it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable.toBitwardenError()
|
||||
@@ -68,6 +73,7 @@ class SendsServiceImpl(
|
||||
sendId = sendId,
|
||||
body = body,
|
||||
)
|
||||
.toResult()
|
||||
.map { UpdateSendResponseJson.Success(send = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -118,16 +124,20 @@ class SendsServiceImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
.toResult()
|
||||
.onFailure { sendsApi.deleteSend(send.id) }
|
||||
.map { send }
|
||||
}
|
||||
|
||||
override suspend fun deleteSend(sendId: String): Result<Unit> =
|
||||
sendsApi.deleteSend(sendId = sendId)
|
||||
sendsApi
|
||||
.deleteSend(sendId = sendId)
|
||||
.toResult()
|
||||
|
||||
override suspend fun removeSendPassword(sendId: String): Result<UpdateSendResponseJson> =
|
||||
sendsApi
|
||||
.removeSendPassword(sendId = sendId)
|
||||
.toResult()
|
||||
.map { UpdateSendResponseJson.Success(send = it) }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
@@ -142,5 +152,7 @@ class SendsServiceImpl(
|
||||
override suspend fun getSend(
|
||||
sendId: String,
|
||||
): Result<SyncResponseJson.Send> =
|
||||
sendsApi.getSend(sendId = sendId)
|
||||
sendsApi
|
||||
.getSend(sendId = sendId)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.api.SyncApi
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
|
||||
class SyncServiceImpl(
|
||||
private val syncApi: SyncApi,
|
||||
) : SyncService {
|
||||
override suspend fun sync(): Result<SyncResponseJson> = syncApi.sync()
|
||||
override suspend fun sync(): Result<SyncResponseJson> = syncApi
|
||||
.sync()
|
||||
.toResult()
|
||||
|
||||
override suspend fun getAccountRevisionDateMillis(): Result<Long> =
|
||||
syncApi.getAccountRevisionDateMillis()
|
||||
syncApi
|
||||
.getAccountRevisionDateMillis()
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.map
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.mapNullable
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
@@ -98,6 +99,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
@@ -222,7 +224,13 @@ class VaultRepositoryImpl(
|
||||
// Cancel any ongoing sync request and clear the vault data in memory every time
|
||||
// the user switches or the vault is locked for the active user.
|
||||
merge(
|
||||
authDiskSource.userSwitchingChangesFlow,
|
||||
authDiskSource
|
||||
.userSwitchingChangesFlow
|
||||
.onEach {
|
||||
// DomainState is not part of the locked data but should still be cleared
|
||||
// when the user changes
|
||||
mutableDomainsStateFlow.update { DataState.Loading }
|
||||
},
|
||||
vaultLockManager
|
||||
.vaultUnlockDataStateFlow
|
||||
.filter { vaultUnlockDataList ->
|
||||
@@ -238,7 +246,10 @@ class VaultRepositoryImpl(
|
||||
|
||||
// Setup ciphers MutableStateFlow
|
||||
mutableCiphersStateFlow
|
||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
) { activeUserId ->
|
||||
observeVaultDiskCiphers(activeUserId)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
@@ -250,19 +261,28 @@ class VaultRepositoryImpl(
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup folders MutableStateFlow
|
||||
mutableFoldersStateFlow
|
||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
) { activeUserId ->
|
||||
observeVaultDiskFolders(activeUserId)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup collections MutableStateFlow
|
||||
mutableCollectionsStateFlow
|
||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
) { activeUserId ->
|
||||
observeVaultDiskCollections(activeUserId)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
// Setup sends MutableStateFlow
|
||||
mutableSendDataStateFlow
|
||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = authDiskSource.userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||
) { activeUserId ->
|
||||
observeVaultDiskSends(activeUserId)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
@@ -301,11 +321,16 @@ class VaultRepositoryImpl(
|
||||
.syncFolderUpsertFlow
|
||||
.onEach(::syncFolderIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
|
||||
databaseSchemeManager
|
||||
.lastDatabaseSchemeChangeInstantFlow
|
||||
.filterNotNull()
|
||||
.onEach { sync() }
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
private fun clearUnlockedData() {
|
||||
mutableCiphersStateFlow.update { DataState.Loading }
|
||||
mutableDomainsStateFlow.update { DataState.Loading }
|
||||
mutableFoldersStateFlow.update { DataState.Loading }
|
||||
mutableCollectionsStateFlow.update { DataState.Loading }
|
||||
mutableSendDataStateFlow.update { DataState.Loading }
|
||||
|
||||
@@ -6,14 +6,14 @@ import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
/**
|
||||
* Map the API [Domains] model to the internal [DomainsData] model.
|
||||
*/
|
||||
fun Domains.toDomainsData(): DomainsData {
|
||||
fun Domains?.toDomainsData(): DomainsData {
|
||||
val globalEquivalentDomains = this
|
||||
.globalEquivalentDomains
|
||||
?.globalEquivalentDomains
|
||||
?.map { it.toInternalModel() }
|
||||
.orEmpty()
|
||||
|
||||
return DomainsData(
|
||||
equivalentDomains = this.equivalentDomains.orEmpty(),
|
||||
equivalentDomains = this?.equivalentDomains.orEmpty(),
|
||||
globalEquivalentDomains = globalEquivalentDomains,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,31 +157,8 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
CreateAuthRequestResult.Declined -> {
|
||||
when (state.loginWithDeviceType) {
|
||||
LoginWithDeviceType.OTHER_DEVICE,
|
||||
LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
-> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
loginWithDeviceType = it.loginWithDeviceType,
|
||||
fingerprintPhrase = "",
|
||||
isResendNotificationLoading = false,
|
||||
),
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = null,
|
||||
message = R.string.this_request_is_no_longer_valid.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> {
|
||||
// Do nothing, the user should not be informed of this state
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do nothing, the user should not be informed of this state
|
||||
CreateAuthRequestResult.Declined -> Unit
|
||||
|
||||
CreateAuthRequestResult.Expired -> {
|
||||
mutableStateFlow.update {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -19,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A reusable modal bottom sheet that applies provides a bottom sheet layout with the
|
||||
@@ -28,11 +30,12 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
* @param sheetTitle The title to display in the [BitwardenTopAppBar]
|
||||
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed
|
||||
* when the "close" icon is clicked, caller must handle any desired animation or hiding of the
|
||||
* bottom sheet.
|
||||
* bottom sheet. This will be invoked _after_ the sheet has been animated away.
|
||||
* @param showBottomSheet Whether or not to show the bottom sheet, by default this is true assuming
|
||||
* the showing/hiding will be handled by the caller.
|
||||
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding
|
||||
* from the containing [BitwardenScaffold].
|
||||
* from the containing [BitwardenScaffold] and a `onDismiss` lambda to be used for manual dismissal
|
||||
* that will include the dismissal animation.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -42,7 +45,10 @@ fun BitwardenModalBottomSheet(
|
||||
modifier: Modifier = Modifier,
|
||||
showBottomSheet: Boolean = true,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
sheetContent: @Composable (PaddingValues) -> Unit,
|
||||
sheetContent: @Composable (
|
||||
paddingValues: PaddingValues,
|
||||
animatedOnDismiss: () -> Unit,
|
||||
) -> Unit,
|
||||
) {
|
||||
if (!showBottomSheet) return
|
||||
ModalBottomSheet(
|
||||
@@ -56,13 +62,14 @@ fun BitwardenModalBottomSheet(
|
||||
shape = BitwardenTheme.shapes.bottomSheet,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val animatedOnDismiss = sheetState.createAnimatedDismissAction(onDismiss = onDismiss)
|
||||
BitwardenScaffold(
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = sheetTitle,
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
|
||||
onNavigationIconClick = onDismiss,
|
||||
onNavigationIconClick = animatedOnDismiss,
|
||||
navigationIconContentDescription = stringResource(R.string.close),
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -73,7 +80,18 @@ fun BitwardenModalBottomSheet(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.fillMaxSize(),
|
||||
) { paddingValues ->
|
||||
sheetContent(paddingValues)
|
||||
sheetContent(paddingValues, animatedOnDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SheetState.createAnimatedDismissAction(onDismiss: () -> Unit): () -> Unit {
|
||||
val scope = rememberCoroutineScope()
|
||||
return {
|
||||
scope
|
||||
.launch { this@createAnimatedDismissAction.hide() }
|
||||
.invokeOnCompletion { onDismiss() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ fun BitwardenSegmentedButton(
|
||||
options: ImmutableList<SegmentedButtonState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (options.isEmpty()) return
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(color = BitwardenTheme.colorScheme.background.secondary)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -14,6 +17,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -21,6 +25,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -40,11 +45,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.platform.util.isFdroid
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButtonWithIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
@@ -52,6 +62,8 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
@@ -62,6 +74,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
@Composable
|
||||
fun PendingRequestsScreen(
|
||||
viewModel: PendingRequestsViewModel = hiltViewModel(),
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLoginApproval: (fingerprint: String) -> Unit,
|
||||
) {
|
||||
@@ -98,6 +111,29 @@ fun PendingRequestsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val hideBottomSheet = state.hideBottomSheet ||
|
||||
isFdroid ||
|
||||
isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) ||
|
||||
permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS) ||
|
||||
permissionsManager.shouldShowRequestPermissionRationale(
|
||||
permission = Manifest.permission.POST_NOTIFICATIONS,
|
||||
)
|
||||
BitwardenModalBottomSheet(
|
||||
showBottomSheet = !hideBottomSheet,
|
||||
sheetTitle = stringResource(R.string.enable_notifications),
|
||||
onDismiss = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PendingRequestsAction.HideBottomSheet) }
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
) { paddingValues, animatedOnDismiss ->
|
||||
PendingRequestsBottomSheetContent(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
permissionsManager = permissionsManager,
|
||||
onDismiss = animatedOnDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
@@ -338,3 +374,68 @@ private fun PendingRequestsEmpty(
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingRequestsBottomSheetContent(
|
||||
permissionsManager: PermissionsManager,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val notificationPermissionLauncher = permissionsManager.getLauncher {
|
||||
onDismiss()
|
||||
}
|
||||
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.img_2fa),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.size(size = 132.dp)
|
||||
.align(alignment = Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_quickly_and_easily_across_devices),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.bitwarden_can_notify_you_each_time_you_receive_a_new_login_request_from_another_device,
|
||||
),
|
||||
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 = R.string.enable_notifications),
|
||||
onClick = {
|
||||
@SuppressLint("InlinedApi")
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(id = R.string.skip_for_now),
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ private const val KEY_STATE = "state"
|
||||
/**
|
||||
* View model for the pending login requests screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class PendingRequestsViewModel @Inject constructor(
|
||||
private val clock: Clock,
|
||||
@@ -39,6 +40,7 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
viewState = PendingRequestsState.ViewState.Loading,
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
isRefreshing = false,
|
||||
hideBottomSheet = false,
|
||||
),
|
||||
) {
|
||||
private var authJob: Job = Job().apply { complete() }
|
||||
@@ -56,6 +58,7 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
when (action) {
|
||||
PendingRequestsAction.CloseClick -> handleCloseClicked()
|
||||
PendingRequestsAction.DeclineAllRequestsConfirm -> handleDeclineAllRequestsConfirmed()
|
||||
PendingRequestsAction.HideBottomSheet -> handleHideBottomSheet()
|
||||
PendingRequestsAction.LifecycleResume -> handleOnLifecycleResumed()
|
||||
PendingRequestsAction.RefreshPull -> handleRefreshPull()
|
||||
is PendingRequestsAction.PendingRequestRowClick -> {
|
||||
@@ -89,6 +92,10 @@ class PendingRequestsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHideBottomSheet() {
|
||||
mutableStateFlow.update { it.copy(hideBottomSheet = true) }
|
||||
}
|
||||
|
||||
private fun handleOnLifecycleResumed() {
|
||||
updateAuthRequestList()
|
||||
}
|
||||
@@ -193,6 +200,7 @@ data class PendingRequestsState(
|
||||
val viewState: ViewState,
|
||||
private val isPullToRefreshSettingEnabled: Boolean,
|
||||
val isRefreshing: Boolean,
|
||||
val hideBottomSheet: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Indicates that the pull-to-refresh should be enabled in the UI.
|
||||
@@ -297,6 +305,11 @@ sealed class PendingRequestsAction {
|
||||
*/
|
||||
data object DeclineAllRequestsConfirm : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The user has dismissed the bottom sheet.
|
||||
*/
|
||||
data object HideBottomSheet : PendingRequestsAction()
|
||||
|
||||
/**
|
||||
* The screen has been re-opened and should be updated.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -27,6 +28,7 @@ import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
@@ -82,7 +84,7 @@ class IntentManagerImpl(
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ class IntentManagerImpl(
|
||||
}
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -132,12 +134,28 @@ class IntentManagerImpl(
|
||||
}
|
||||
|
||||
override fun launchUri(uri: Uri) {
|
||||
val newUri = if (uri.scheme == null) {
|
||||
uri.buildUpon().scheme("https").build()
|
||||
if (uri.scheme.equals(other = "androidapp", ignoreCase = true)) {
|
||||
val packageName = uri.toString().removePrefix(prefix = "androidapp://")
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU)) {
|
||||
startActivity(createPlayStoreIntent(packageName))
|
||||
} else {
|
||||
try {
|
||||
context
|
||||
.packageManager
|
||||
.getLaunchIntentSenderForPackage(packageName)
|
||||
.sendIntent(context, Activity.RESULT_OK, null, null, null)
|
||||
} catch (_: IntentSender.SendIntentException) {
|
||||
startActivity(createPlayStoreIntent(packageName))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uri.normalizeScheme()
|
||||
val newUri = if (uri.scheme == null) {
|
||||
uri.buildUpon().scheme("https").build()
|
||||
} else {
|
||||
uri.normalizeScheme()
|
||||
}
|
||||
startActivity(Intent(Intent.ACTION_VIEW, newUri))
|
||||
}
|
||||
startActivity(Intent(Intent.ACTION_VIEW, newUri))
|
||||
}
|
||||
|
||||
override fun shareText(text: String) {
|
||||
@@ -301,6 +319,15 @@ class IntentManagerImpl(
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun createPlayStoreIntent(packageName: String): Intent {
|
||||
val playStoreUri = "https://play.google.com/store/apps/details"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("id", packageName)
|
||||
.build()
|
||||
return Intent(Intent.ACTION_VIEW, playStoreUri)
|
||||
}
|
||||
|
||||
private fun getCameraFileData(): IntentManager.FileData {
|
||||
val tmpDir = File(context.filesDir, TEMP_CAMERA_IMAGE_DIR)
|
||||
val file = File(tmpDir, TEMP_CAMERA_IMAGE_NAME)
|
||||
|
||||
@@ -202,16 +202,14 @@ fun GeneratorScreen(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
if (state.generatorMode == GeneratorMode.Default) {
|
||||
MainStateOptionsItem(
|
||||
selectedType = state.selectedType,
|
||||
passcodePolicyOverride = state.passcodePolicyOverride,
|
||||
possibleMainStates = state.typeOptions.toImmutableList(),
|
||||
onMainStateOptionClicked = onMainStateOptionClicked,
|
||||
modifier = Modifier
|
||||
.scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior),
|
||||
)
|
||||
}
|
||||
MainStateOptionsItem(
|
||||
selectedType = state.selectedType,
|
||||
passcodePolicyOverride = state.passcodePolicyOverride,
|
||||
possibleMainStates = state.typeOptions.toImmutableList(),
|
||||
onMainStateOptionClicked = onMainStateOptionClicked,
|
||||
modifier = Modifier
|
||||
.scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior),
|
||||
)
|
||||
ScrollContent(
|
||||
state = state,
|
||||
onRegenerateClick = onRegenerateClick,
|
||||
|
||||
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.filter
|
||||
import kotlin.math.max
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
@@ -1726,10 +1727,17 @@ data class GeneratorState(
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Provides a list of available main types for the generator.
|
||||
* Provides a list of available main types for the generator based on the [GeneratorMode].
|
||||
*/
|
||||
val typeOptions: List<MainTypeOption>
|
||||
get() = MainTypeOption.entries.toList()
|
||||
get() = when (generatorMode) {
|
||||
GeneratorMode.Default -> MainTypeOption.entries.toList()
|
||||
GeneratorMode.Modal.Password -> MainTypeOption
|
||||
.entries
|
||||
.filter { it != MainTypeOption.USERNAME }
|
||||
|
||||
is GeneratorMode.Modal.Username -> emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the main type options for the generator, such as PASSWORD PASSPHRASE, and
|
||||
|
||||
@@ -23,7 +23,6 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
@@ -64,7 +63,6 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHan
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
|
||||
|
||||
@@ -99,27 +97,15 @@ fun ImportLoginsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
val hideSheetAndExecuteCompleteImportLogins: () -> Unit = {
|
||||
// This pattern mirrors the onDismissRequest handling in the material ModalBottomSheet
|
||||
scope
|
||||
.launch {
|
||||
sheetState.hide()
|
||||
}
|
||||
.invokeOnCompletion {
|
||||
handler.onSuccessfulSyncAcknowledged()
|
||||
}
|
||||
}
|
||||
BitwardenModalBottomSheet(
|
||||
showBottomSheet = state.showBottomSheet,
|
||||
sheetTitle = stringResource(R.string.bitwarden_tools),
|
||||
onDismiss = hideSheetAndExecuteCompleteImportLogins,
|
||||
onDismiss = handler.onSuccessfulSyncAcknowledged,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
) { paddingValues ->
|
||||
) { paddingValues, animatedOnDismiss ->
|
||||
ImportLoginsSuccessBottomSheetContent(
|
||||
onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins,
|
||||
onCompleteImportLogins = animatedOnDismiss,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.scaleIn
|
||||
@@ -15,7 +13,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -58,11 +55,9 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPull
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
@@ -86,7 +81,6 @@ fun VaultScreen(
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
@@ -128,10 +122,6 @@ fun VaultScreen(
|
||||
}
|
||||
}
|
||||
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
|
||||
VaultScreenPushNotifications(
|
||||
hideNotificationsDialog = state.hideNotificationsDialog,
|
||||
permissionsManager = permissionsManager,
|
||||
)
|
||||
VaultScreenScaffold(
|
||||
state = state,
|
||||
pullToRefreshState = pullToRefreshState,
|
||||
@@ -140,28 +130,6 @@ fun VaultScreen(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the notifications permission request.
|
||||
*/
|
||||
@Composable
|
||||
private fun VaultScreenPushNotifications(
|
||||
hideNotificationsDialog: Boolean,
|
||||
permissionsManager: PermissionsManager,
|
||||
) {
|
||||
if (hideNotificationsDialog) return
|
||||
val launcher = permissionsManager.getLauncher {
|
||||
// We do not actually care what the response is, we just need
|
||||
// to give the user a chance to give us the permission.
|
||||
}
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
@SuppressLint("InlinedApi")
|
||||
// We check the version code as part of the 'hideNotificationsDialog' property.
|
||||
if (!permissionsManager.checkPermission(Manifest.permission.POST_NOTIFICATIONS)) {
|
||||
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scaffold for the [VaultScreen]
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -18,8 +17,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.platform.util.isFdroid
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
@@ -94,7 +91,6 @@ class VaultViewModel @Inject constructor(
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
|
||||
hasMasterPassword = userState.activeAccount.hasMasterPassword,
|
||||
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
|
||||
isRefreshing = false,
|
||||
showImportActionCard = false,
|
||||
)
|
||||
@@ -665,7 +661,6 @@ data class VaultState(
|
||||
private val isPullToRefreshSettingEnabled: Boolean,
|
||||
val baseIconUrl: String,
|
||||
val isIconLoadingDisabled: Boolean,
|
||||
val hideNotificationsDialog: Boolean,
|
||||
val isRefreshing: Boolean,
|
||||
val showImportActionCard: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
73
app/src/main/res/drawable-night/img_2fa.xml
Normal file
73
app/src/main/res/drawable-night/img_2fa.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="201dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="201">
|
||||
<path
|
||||
android:fillColor="#AAC3EF"
|
||||
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H125C131.9,25.67 137.5,31.26 137.5,38.17V117.33C137.5,124.24 131.9,129.83 125,129.83H12.5C5.6,129.83 0,124.24 0,117.33V38.17Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M125,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V117.33C4.17,121.94 7.9,125.67 12.5,125.67H125C129.6,125.67 133.33,121.94 133.33,117.33V38.17C133.33,33.56 129.6,29.83 125,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V117.33C0,124.24 5.6,129.83 12.5,129.83H125C131.9,129.83 137.5,124.24 137.5,117.33V38.17C137.5,31.26 131.9,25.67 125,25.67H12.5Z" />
|
||||
<path
|
||||
android:fillColor="#79A1E9"
|
||||
android:pathData="M47.92,75.67C47.92,72.21 50.71,69.42 54.17,69.42H83.33C86.78,69.42 89.58,72.21 89.58,75.67V96.5C89.58,99.95 86.78,102.75 83.33,102.75H54.17C50.71,102.75 47.92,99.95 47.92,96.5V75.67Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M83.33,73.58H54.17C53.02,73.58 52.08,74.52 52.08,75.67V96.5C52.08,97.65 53.02,98.58 54.17,98.58H83.33C84.48,98.58 85.42,97.65 85.42,96.5V75.67C85.42,74.52 84.48,73.58 83.33,73.58ZM54.17,69.42C50.71,69.42 47.92,72.21 47.92,75.67V96.5C47.92,99.95 50.71,102.75 54.17,102.75H83.33C86.78,102.75 89.58,99.95 89.58,96.5V75.67C89.58,72.21 86.78,69.42 83.33,69.42H54.17Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M66.67,81.92C66.67,80.77 67.6,79.83 68.75,79.83C69.9,79.83 70.83,80.77 70.83,81.92V90.25C70.83,91.4 69.9,92.33 68.75,92.33C67.6,92.33 66.67,91.4 66.67,90.25V81.92Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M58.33,67.33C58.33,61.58 63,56.92 68.75,56.92C74.5,56.92 79.17,61.58 79.17,67.33V69.42H75V67.33C75,63.88 72.2,61.08 68.75,61.08C65.3,61.08 62.5,63.88 62.5,67.33V69.42H58.33V67.33Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M135.42,50.67H2.08V46.5H135.42V50.67Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M129.17,38.17C129.17,40.47 127.3,42.33 125,42.33C122.7,42.33 120.83,40.47 120.83,38.17C120.83,35.87 122.7,34 125,34C127.3,34 129.17,35.87 129.17,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M116.67,38.17C116.67,40.47 114.8,42.33 112.5,42.33C110.2,42.33 108.33,40.47 108.33,38.17C108.33,35.87 110.2,34 112.5,34C114.8,34 116.67,35.87 116.67,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M104.17,38.17C104.17,40.47 102.3,42.33 100,42.33C97.7,42.33 95.83,40.47 95.83,38.17C95.83,35.87 97.7,34 100,34C102.3,34 104.17,35.87 104.17,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#F3F6F9"
|
||||
android:pathData="M170.83,88.17C170.83,104.28 157.77,117.33 141.67,117.33C125.56,117.33 112.5,104.28 112.5,88.17C112.5,72.06 125.56,59 141.67,59C157.77,59 170.83,72.06 170.83,88.17Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M141.67,113.17C155.47,113.17 166.67,101.97 166.67,88.17C166.67,74.36 155.47,63.17 141.67,63.17C127.86,63.17 116.67,74.36 116.67,88.17C116.67,101.97 127.86,113.17 141.67,113.17ZM141.67,117.33C157.77,117.33 170.83,104.28 170.83,88.17C170.83,72.06 157.77,59 141.67,59C125.56,59 112.5,72.06 112.5,88.17C112.5,104.28 125.56,117.33 141.67,117.33Z" />
|
||||
<path
|
||||
android:fillColor="#F3F6F9"
|
||||
android:pathData="M195.64,150.62C198.52,157.57 200,165.02 200,172.54C200,174.27 198.6,175.67 196.87,175.67H88.54C86.82,175.67 85.42,174.27 85.42,172.54C85.42,165.02 86.9,157.57 89.78,150.62C92.66,143.67 96.88,137.35 102.2,132.03C107.52,126.71 113.83,122.49 120.78,119.61C127.73,116.73 135.18,115.25 142.71,115.25C150.23,115.25 157.68,116.73 164.63,119.61C171.58,122.49 177.9,126.71 183.22,132.03C188.54,137.35 192.76,143.67 195.64,150.62Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M195.82,171.5C195.69,164.88 194.33,158.34 191.79,152.21C189.12,145.77 185.21,139.91 180.27,134.98C175.34,130.04 169.48,126.13 163.04,123.46C156.59,120.79 149.68,119.42 142.71,119.42C135.73,119.42 128.82,120.79 122.38,123.46C115.93,126.13 110.08,130.04 105.14,134.98C100.21,139.91 96.3,145.77 93.63,152.21C91.09,158.34 89.72,164.88 89.59,171.5H195.82ZM200,172.54C200,165.02 198.52,157.57 195.64,150.62C192.76,143.67 188.54,137.35 183.22,132.03C177.9,126.71 171.58,122.49 164.63,119.61C157.68,116.73 150.23,115.25 142.71,115.25C135.18,115.25 127.73,116.73 120.78,119.61C113.83,122.49 107.52,126.71 102.2,132.03C96.88,137.35 92.66,143.67 89.78,150.62C86.9,157.57 85.42,165.02 85.42,172.54C85.42,174.27 86.82,175.67 88.54,175.67H196.87C198.6,175.67 200,174.27 200,172.54Z" />
|
||||
<path
|
||||
android:fillColor="#FFBF00"
|
||||
android:pathData="M22.92,127.75C22.92,118.54 30.38,111.08 39.58,111.08H95.83C105.04,111.08 112.5,118.54 112.5,127.75C112.5,136.95 105.04,144.42 95.83,144.42H39.58C30.38,144.42 22.92,136.95 22.92,127.75Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M95.83,115.25H39.58C32.68,115.25 27.08,120.85 27.08,127.75C27.08,134.65 32.68,140.25 39.58,140.25H95.83C102.74,140.25 108.33,134.65 108.33,127.75C108.33,120.85 102.74,115.25 95.83,115.25ZM39.58,111.08C30.38,111.08 22.92,118.54 22.92,127.75C22.92,136.95 30.38,144.42 39.58,144.42H95.83C105.04,144.42 112.5,136.95 112.5,127.75C112.5,118.54 105.04,111.08 95.83,111.08H39.58Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M41.68,120.61C42.83,120.61 43.76,121.54 43.76,122.69V125.61L46.49,124.71C47.59,124.36 48.76,124.95 49.12,126.05C49.48,127.14 48.88,128.32 47.79,128.67L45.02,129.58L46.76,132.01C47.42,132.95 47.21,134.25 46.27,134.92C45.33,135.59 44.03,135.37 43.36,134.43L41.68,132.07L39.99,134.43C39.32,135.37 38.02,135.59 37.08,134.92C36.15,134.25 35.93,132.95 36.6,132.01L38.33,129.58L35.56,128.67C34.47,128.32 33.87,127.14 34.23,126.05C34.59,124.95 35.77,124.36 36.86,124.71L39.59,125.61V122.69C39.59,121.54 40.53,120.61 41.68,120.61ZM60.43,120.61C61.58,120.61 62.51,121.54 62.51,122.69V125.61L65.24,124.71C66.34,124.36 67.51,124.95 67.87,126.05C68.23,127.14 67.63,128.32 66.54,128.67L63.77,129.58L65.51,132.01C66.17,132.95 65.96,134.25 65.02,134.92C64.08,135.59 62.78,135.37 62.11,134.43L60.43,132.07L58.74,134.43C58.07,135.37 56.77,135.59 55.83,134.92C54.9,134.25 54.68,132.95 55.35,132.01L57.08,129.58L54.31,128.67C53.22,128.32 52.62,127.14 52.98,126.05C53.34,124.95 54.52,124.36 55.61,124.71L58.34,125.61V122.69C58.34,121.54 59.28,120.61 60.43,120.61Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M72.92,131.92C72.92,130.77 73.85,129.83 75,129.83L83.33,129.83C84.48,129.83 85.42,130.77 85.42,131.92C85.42,133.07 84.48,134 83.33,134L75,134C73.85,134 72.92,133.07 72.92,131.92Z" />
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M89.58,131.92C89.58,130.77 90.52,129.83 91.67,129.83L100,129.83C101.15,129.83 102.08,130.77 102.08,131.92C102.08,133.07 101.15,134 100,134L91.67,134C90.52,134 89.58,133.07 89.58,131.92Z" />
|
||||
</vector>
|
||||
73
app/src/main/res/drawable/img_2fa.xml
Normal file
73
app/src/main/res/drawable/img_2fa.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="201dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="201">
|
||||
<path
|
||||
android:fillColor="#DBE5F6"
|
||||
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H125C131.9,25.67 137.5,31.26 137.5,38.17V117.33C137.5,124.24 131.9,129.83 125,129.83H12.5C5.6,129.83 0,124.24 0,117.33V38.17Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M125,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V117.33C4.17,121.94 7.9,125.67 12.5,125.67H125C129.6,125.67 133.33,121.94 133.33,117.33V38.17C133.33,33.56 129.6,29.83 125,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V117.33C0,124.24 5.6,129.83 12.5,129.83H125C131.9,129.83 137.5,124.24 137.5,117.33V38.17C137.5,31.26 131.9,25.67 125,25.67H12.5Z" />
|
||||
<path
|
||||
android:fillColor="#AAC3EF"
|
||||
android:pathData="M47.92,75.67C47.92,72.21 50.71,69.42 54.17,69.42H83.33C86.78,69.42 89.58,72.21 89.58,75.67V96.5C89.58,99.95 86.78,102.75 83.33,102.75H54.17C50.71,102.75 47.92,99.95 47.92,96.5V75.67Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M83.33,73.58H54.17C53.02,73.58 52.08,74.52 52.08,75.67V96.5C52.08,97.65 53.02,98.58 54.17,98.58H83.33C84.48,98.58 85.42,97.65 85.42,96.5V75.67C85.42,74.52 84.48,73.58 83.33,73.58ZM54.17,69.42C50.71,69.42 47.92,72.21 47.92,75.67V96.5C47.92,99.95 50.71,102.75 54.17,102.75H83.33C86.78,102.75 89.58,99.95 89.58,96.5V75.67C89.58,72.21 86.78,69.42 83.33,69.42H54.17Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M66.67,81.92C66.67,80.77 67.6,79.83 68.75,79.83C69.9,79.83 70.83,80.77 70.83,81.92V90.25C70.83,91.4 69.9,92.33 68.75,92.33C67.6,92.33 66.67,91.4 66.67,90.25V81.92Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M58.33,67.33C58.33,61.58 63,56.92 68.75,56.92C74.5,56.92 79.17,61.58 79.17,67.33V69.42H75V67.33C75,63.88 72.2,61.08 68.75,61.08C65.3,61.08 62.5,63.88 62.5,67.33V69.42H58.33V67.33Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M135.42,50.67H2.08V46.5H135.42V50.67Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M129.17,38.17C129.17,40.47 127.3,42.33 125,42.33C122.7,42.33 120.83,40.47 120.83,38.17C120.83,35.87 122.7,34 125,34C127.3,34 129.17,35.87 129.17,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M116.67,38.17C116.67,40.47 114.8,42.33 112.5,42.33C110.2,42.33 108.33,40.47 108.33,38.17C108.33,35.87 110.2,34 112.5,34C114.8,34 116.67,35.87 116.67,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M104.17,38.17C104.17,40.47 102.3,42.33 100,42.33C97.7,42.33 95.83,40.47 95.83,38.17C95.83,35.87 97.7,34 100,34C102.3,34 104.17,35.87 104.17,38.17Z" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M170.83,88.17C170.83,104.28 157.77,117.33 141.67,117.33C125.56,117.33 112.5,104.28 112.5,88.17C112.5,72.06 125.56,59 141.67,59C157.77,59 170.83,72.06 170.83,88.17Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M141.67,113.17C155.47,113.17 166.67,101.97 166.67,88.17C166.67,74.36 155.47,63.17 141.67,63.17C127.86,63.17 116.67,74.36 116.67,88.17C116.67,101.97 127.86,113.17 141.67,113.17ZM141.67,117.33C157.77,117.33 170.83,104.28 170.83,88.17C170.83,72.06 157.77,59 141.67,59C125.56,59 112.5,72.06 112.5,88.17C112.5,104.28 125.56,117.33 141.67,117.33Z" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M195.64,150.62C198.52,157.57 200,165.02 200,172.54C200,174.27 198.6,175.67 196.87,175.67H88.54C86.82,175.67 85.42,174.27 85.42,172.54C85.42,165.02 86.9,157.57 89.78,150.62C92.66,143.67 96.88,137.35 102.2,132.03C107.52,126.71 113.83,122.49 120.78,119.61C127.73,116.73 135.18,115.25 142.71,115.25C150.23,115.25 157.68,116.73 164.63,119.61C171.58,122.49 177.9,126.71 183.22,132.03C188.54,137.35 192.76,143.67 195.64,150.62Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M195.82,171.5C195.69,164.88 194.33,158.34 191.79,152.21C189.12,145.77 185.21,139.91 180.27,134.98C175.34,130.04 169.48,126.13 163.04,123.46C156.59,120.79 149.68,119.42 142.71,119.42C135.73,119.42 128.82,120.79 122.38,123.46C115.93,126.13 110.08,130.04 105.14,134.98C100.21,139.91 96.3,145.77 93.63,152.21C91.09,158.34 89.72,164.88 89.59,171.5H195.82ZM200,172.54C200,165.02 198.52,157.57 195.64,150.62C192.76,143.67 188.54,137.35 183.22,132.03C177.9,126.71 171.58,122.49 164.63,119.61C157.68,116.73 150.23,115.25 142.71,115.25C135.18,115.25 127.73,116.73 120.78,119.61C113.83,122.49 107.52,126.71 102.2,132.03C96.88,137.35 92.66,143.67 89.78,150.62C86.9,157.57 85.42,165.02 85.42,172.54C85.42,174.27 86.82,175.67 88.54,175.67H196.87C198.6,175.67 200,174.27 200,172.54Z" />
|
||||
<path
|
||||
android:fillColor="#FFBF00"
|
||||
android:pathData="M22.92,127.75C22.92,118.54 30.38,111.08 39.58,111.08H95.83C105.04,111.08 112.5,118.54 112.5,127.75C112.5,136.95 105.04,144.42 95.83,144.42H39.58C30.38,144.42 22.92,136.95 22.92,127.75Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M95.83,115.25H39.58C32.68,115.25 27.08,120.85 27.08,127.75C27.08,134.65 32.68,140.25 39.58,140.25H95.83C102.74,140.25 108.33,134.65 108.33,127.75C108.33,120.85 102.74,115.25 95.83,115.25ZM39.58,111.08C30.38,111.08 22.92,118.54 22.92,127.75C22.92,136.95 30.38,144.42 39.58,144.42H95.83C105.04,144.42 112.5,136.95 112.5,127.75C112.5,118.54 105.04,111.08 95.83,111.08H39.58Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M41.67,120.61C42.83,120.61 43.76,121.54 43.76,122.69V125.61L46.49,124.71C47.58,124.36 48.76,124.95 49.12,126.05C49.48,127.14 48.88,128.32 47.79,128.67L45.02,129.58L46.75,132.01C47.42,132.95 47.2,134.25 46.27,134.92C45.33,135.59 44.03,135.37 43.36,134.43L41.67,132.07L39.99,134.43C39.32,135.37 38.02,135.59 37.08,134.92C36.14,134.25 35.93,132.95 36.6,132.01L38.33,129.58L35.56,128.67C34.47,128.32 33.87,127.14 34.23,126.05C34.59,124.95 35.76,124.36 36.86,124.71L39.59,125.61V122.69C39.59,121.54 40.52,120.61 41.67,120.61ZM60.42,120.61C61.58,120.61 62.51,121.54 62.51,122.69V125.61L65.24,124.71C66.33,124.36 67.51,124.95 67.87,126.05C68.23,127.14 67.63,128.32 66.54,128.67L63.77,129.58L65.5,132.01C66.17,132.95 65.95,134.25 65.02,134.92C64.08,135.59 62.78,135.37 62.11,134.43L60.42,132.07L58.74,134.43C58.07,135.37 56.77,135.59 55.83,134.92C54.89,134.25 54.68,132.95 55.35,132.01L57.08,129.58L54.31,128.67C53.22,128.32 52.62,127.14 52.98,126.05C53.34,124.95 54.51,124.36 55.61,124.71L58.34,125.61V122.69C58.34,121.54 59.27,120.61 60.42,120.61Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M72.92,131.92C72.92,130.77 73.85,129.83 75,129.83L83.33,129.83C84.48,129.83 85.42,130.77 85.42,131.92C85.42,133.07 84.48,134 83.33,134L75,134C73.85,134 72.92,133.07 72.92,131.92Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M89.58,131.92C89.58,130.77 90.52,129.83 91.67,129.83L100,129.83C101.15,129.83 102.08,130.77 102.08,131.92C102.08,133.07 101.15,134 100,134L91.67,134C90.52,134 89.58,133.07 89.58,131.92Z" />
|
||||
</vector>
|
||||
@@ -1072,4 +1072,8 @@ Do you want to switch to this account?</string>
|
||||
<string name="manage_your_logins_from_anywhere_with_bitwarden_tools">Manage your logins from anywhere with Bitwarden tools for web and desktop.</string>
|
||||
<string name="bitwarden_tools">Bitwarden Tools</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="enable_notifications">Enable notifications</string>
|
||||
<string name="log_in_quickly_and_easily_across_devices">Log in quickly and easily across devices</string>
|
||||
<string name="bitwarden_can_notify_you_each_time_you_receive_a_new_login_request_from_another_device">Bitwarden can notify you each time you receive a new login request from another device.</string>
|
||||
<string name="skip_for_now">Skip for now</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||
android:canRetrieveWindowContent="true"
|
||||
|
||||
@@ -30,7 +30,7 @@ class LogsManagerImpl(
|
||||
}
|
||||
if (value) {
|
||||
Timber.plant(nonfatalErrorTree)
|
||||
} else {
|
||||
} else if (Timber.forest().contains(nonfatalErrorTree)) {
|
||||
Timber.uproot(nonfatalErrorTree)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test
|
||||
class AccessibilityCompletionManagerTest {
|
||||
|
||||
private val activity: Activity = mockk {
|
||||
every { finish() } just runs
|
||||
every { finishAndRemoveTask() } just runs
|
||||
}
|
||||
private val accessibilityAutofillManager: AccessibilityAutofillManager = mockk()
|
||||
private val totpManager: AutofillTotpManager = mockk()
|
||||
@@ -68,7 +68,7 @@ class AccessibilityCompletionManagerTest {
|
||||
|
||||
verify(exactly = 1) {
|
||||
activity.intent
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class AccessibilityCompletionManagerTest {
|
||||
verify(exactly = 1) {
|
||||
activity.intent
|
||||
mockIntent.getAutofillSelectionDataOrNull()
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class AccessibilityCompletionManagerTest {
|
||||
verify(exactly = 1) {
|
||||
activity.intent
|
||||
mockIntent.getAutofillSelectionDataOrNull()
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class AccessibilityCompletionManagerTest {
|
||||
verify(exactly = 1) {
|
||||
activity.intent
|
||||
mockIntent.getAutofillSelectionDataOrNull()
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class AccessibilityCompletionManagerTest {
|
||||
verify(exactly = 1) {
|
||||
activity.intent
|
||||
mockIntent.getAutofillSelectionDataOrNull()
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ class AccessibilityCompletionManagerTest {
|
||||
cipherView = cipherView,
|
||||
uri = uri,
|
||||
)
|
||||
activity.finish()
|
||||
activity.finishAndRemoveTask()
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.base
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.NetworkResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -24,7 +24,7 @@ abstract class BaseServiceTest {
|
||||
|
||||
protected val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(url.toString())
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableLastDatabaseSchemeChangeInstant =
|
||||
bufferedMutableSharedFlow<Instant?>()
|
||||
|
||||
private var storedAppTheme: AppTheme = AppTheme.DEFAULT
|
||||
private val storedLastSyncTime = mutableMapOf<String, Instant?>()
|
||||
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
||||
@@ -137,6 +140,11 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||
get() = storedLastDatabaseSchemeChangeInstant
|
||||
set(value) { storedLastDatabaseSchemeChangeInstant = value }
|
||||
|
||||
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
get() = mutableLastDatabaseSchemeChangeInstant.onSubscription {
|
||||
emit(lastDatabaseSchemeChangeInstant)
|
||||
}
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
userId: String,
|
||||
systemBioIntegrityState: String,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
@@ -10,14 +11,14 @@ import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import retrofit2.http.GET
|
||||
|
||||
class ResultCallAdapterTest {
|
||||
class NetworkResultCallAdapterTest {
|
||||
|
||||
private val server: MockWebServer = MockWebServer().apply { start() }
|
||||
private val testService: FakeService =
|
||||
Retrofit.Builder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
// add the adapter being tested
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.build()
|
||||
.create()
|
||||
|
||||
@@ -30,14 +31,14 @@ class ResultCallAdapterTest {
|
||||
fun `when server returns error response code result should be failure`() = runBlocking {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
val result = testService.requestWithUnitData()
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result is NetworkResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when server returns successful response result should be success`() = runBlocking {
|
||||
server.enqueue(MockResponse())
|
||||
val result = testService.requestWithUnitData()
|
||||
assertTrue(result.isSuccess)
|
||||
assertTrue(result is NetworkResult.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,5 +47,5 @@ class ResultCallAdapterTest {
|
||||
*/
|
||||
private interface FakeService {
|
||||
@GET("/fake")
|
||||
suspend fun requestWithUnitData(): Result<Unit>
|
||||
suspend fun requestWithUnitData(): NetworkResult<Unit>
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.Refres
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.HeadersInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
@@ -39,7 +40,7 @@ class RetrofitsTest {
|
||||
}
|
||||
}
|
||||
private val headersInterceptors = mockk<HeadersInterceptor> {
|
||||
mockIntercept { isheadersInterceptorCalled = true }
|
||||
mockIntercept { isHeadersInterceptorCalled = true }
|
||||
}
|
||||
private val refreshAuthenticator = mockk<RefreshAuthenticator> {
|
||||
mockAuthenticate { isRefreshAuthenticatorCalled = true }
|
||||
@@ -57,7 +58,7 @@ class RetrofitsTest {
|
||||
|
||||
private var isAuthInterceptorCalled = false
|
||||
private var isApiInterceptorCalled = false
|
||||
private var isheadersInterceptorCalled = false
|
||||
private var isHeadersInterceptorCalled = false
|
||||
private var isIdentityInterceptorCalled = false
|
||||
private var isEventsInterceptorCalled = false
|
||||
private var isRefreshAuthenticatorCalled = false
|
||||
@@ -158,7 +159,7 @@ class RetrofitsTest {
|
||||
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -176,7 +177,7 @@ class RetrofitsTest {
|
||||
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertTrue(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -194,7 +195,7 @@ class RetrofitsTest {
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -212,7 +213,7 @@ class RetrofitsTest {
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertTrue(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -231,7 +232,7 @@ class RetrofitsTest {
|
||||
|
||||
assertTrue(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -250,7 +251,7 @@ class RetrofitsTest {
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
assertTrue(isHeadersInterceptorCalled)
|
||||
assertFalse(isIdentityInterceptorCalled)
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
@@ -264,7 +265,7 @@ class RetrofitsTest {
|
||||
|
||||
interface TestApi {
|
||||
@GET("/test")
|
||||
suspend fun test(): Result<JsonObject>
|
||||
suspend fun test(): NetworkResult<JsonObject>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,18 @@ class BaseEnumeratedIntSerializerTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `properly returns default value when unknown value is provided`() {
|
||||
assertEquals(
|
||||
TestEnum.UNKNOWN,
|
||||
json.decodeFromString<TestEnum>(
|
||||
"""
|
||||
-1
|
||||
""",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable(TestEnumSerializer::class)
|
||||
@@ -44,7 +56,12 @@ private enum class TestEnum {
|
||||
|
||||
@SerialName("2")
|
||||
CASE_2,
|
||||
|
||||
@SerialName("-1")
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
private class TestEnumSerializer :
|
||||
BaseEnumeratedIntSerializer<TestEnum>(values = TestEnum.entries.toTypedArray())
|
||||
private class TestEnumSerializer : BaseEnumeratedIntSerializer<TestEnum>(
|
||||
values = TestEnum.entries.toTypedArray(),
|
||||
default = TestEnum.UNKNOWN,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -11,62 +12,71 @@ import org.junit.jupiter.api.Test
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
|
||||
class CallExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when execute throws IOException`() {
|
||||
val request = mockk<Request> {
|
||||
every { url } returns mockk()
|
||||
}
|
||||
fun `executeForNetworkResult returns failure when execute throws IOException`() {
|
||||
val request = createMockkRequest()
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { request() } returns request
|
||||
every { execute() } throws IOException("Fail")
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
val result = call.executeForNetworkResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result is NetworkResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when execute throws RuntimeException`() {
|
||||
val request = mockk<Request> {
|
||||
every { url } returns mockk()
|
||||
}
|
||||
fun `executeForNetworkResult returns failure when execute throws RuntimeException`() {
|
||||
val request = createMockkRequest()
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { request() } returns request
|
||||
every { execute() } throws RuntimeException("Fail")
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
val result = call.executeForNetworkResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result is NetworkResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when response is failure`() {
|
||||
val request = mockk<Request> {
|
||||
every { url } returns mockk()
|
||||
}
|
||||
fun `executeForNetworkResult returns failure when response is failure`() {
|
||||
val request = createMockkRequest()
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { request() } returns request
|
||||
every { execute() } returns Response.error(400, "".toResponseBody())
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
val result = call.executeForNetworkResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result is NetworkResult.Failure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns success when response is failure`() {
|
||||
fun `executeForNetworkResult returns success when response is failure`() {
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { execute() } returns Response.success(Unit)
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
val result = call.executeForNetworkResult()
|
||||
|
||||
assertEquals(Unit.asSuccess(), result)
|
||||
assertEquals(NetworkResult.Success(Unit), result)
|
||||
}
|
||||
|
||||
private fun createMockkRequest(): Request {
|
||||
val mockkUrl = mockk<URL> {
|
||||
every { protocol } returns "http"
|
||||
every { authority } returns "bitwarden.com"
|
||||
every { path } returns "/example/path"
|
||||
}
|
||||
val mockkHttpUrl = mockk<HttpUrl> {
|
||||
every { toUrl() } returns mockkUrl
|
||||
}
|
||||
return mockk<Request> {
|
||||
every { url } returns mockkHttpUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class NetworkResultExtensionsTest {
|
||||
@Test
|
||||
fun `NetworkResult toResult with success should return successful result`() {
|
||||
val value = "test"
|
||||
val original = NetworkResult.Success(value)
|
||||
|
||||
val result = original.toResult()
|
||||
|
||||
assertEquals(value.asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NetworkResult toResult with failure should return failure result`() {
|
||||
val exception = Exception("Failed")
|
||||
val original = NetworkResult.Failure(exception)
|
||||
|
||||
val result = original.toResult()
|
||||
|
||||
assertEquals(exception.asFailure(), result)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,37 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class DatabaseSchemeManagerTest {
|
||||
|
||||
private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow<Instant?>(null)
|
||||
private val mockSettingsDiskSource: SettingsDiskSource = mockk {
|
||||
every { lastDatabaseSchemeChangeInstant } returns null
|
||||
every { lastDatabaseSchemeChangeInstant = any() } just runs
|
||||
every {
|
||||
lastDatabaseSchemeChangeInstant
|
||||
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
||||
every { lastDatabaseSchemeChangeInstant = any() } answers {
|
||||
mutableLastDatabaseSchemeChangeInstantFlow.value = firstArg()
|
||||
}
|
||||
every {
|
||||
lastDatabaseSchemeChangeInstantFlow
|
||||
} returns mutableLastDatabaseSchemeChangeInstantFlow
|
||||
}
|
||||
private val dispatcherManager = FakeDispatcherManager()
|
||||
private val databaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||
settingsDiskSource = mockSettingsDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@@ -30,6 +43,23 @@ class DatabaseSchemeManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLastDatabaseSchemeChangeInstant does emit value`() = runTest {
|
||||
databaseSchemeManager.lastDatabaseSchemeChangeInstantFlow.test {
|
||||
// Assert the value is initialized to null
|
||||
assertEquals(
|
||||
null,
|
||||
awaitItem(),
|
||||
)
|
||||
// Assert the new value is emitted
|
||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant()
|
||||
assertEquals(
|
||||
FIXED_CLOCK.instant(),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getLastDatabaseSchemeChangeInstant retrieves stored value from settingsDiskSource`() {
|
||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -50,6 +51,74 @@ class StateFlowExtensionsTest {
|
||||
assertEquals(0, awaitItem())
|
||||
assertEquals(1, awaitItem())
|
||||
|
||||
job.cancel()
|
||||
// Job is canceled, we should have no more subscribers
|
||||
assertEquals(0, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `observeWhenSubscribedAndUnlocked should observe the given flow depending on the state of the source user and vault unlock flow`() =
|
||||
runTest {
|
||||
val userStateFlow = MutableStateFlow<UserStateJson?>(null)
|
||||
val vaultUnlockFlow = MutableStateFlow<List<VaultUnlockData>>(emptyList())
|
||||
val observerStateFlow = MutableStateFlow("")
|
||||
val sourceMutableStateFlow = MutableStateFlow(Unit)
|
||||
|
||||
assertEquals(0, observerStateFlow.subscriptionCount.value)
|
||||
sourceMutableStateFlow
|
||||
.observeWhenSubscribedAndUnlocked(
|
||||
userStateFlow = userStateFlow,
|
||||
vaultUnlockFlow = vaultUnlockFlow,
|
||||
observer = { observerStateFlow },
|
||||
)
|
||||
.launchIn(backgroundScope)
|
||||
|
||||
observerStateFlow.subscriptionCount.test {
|
||||
// No subscriber to start
|
||||
assertEquals(0, awaitItem())
|
||||
|
||||
userStateFlow.value = mockk<UserStateJson> {
|
||||
every { activeUserId } returns "user_id_1234"
|
||||
}
|
||||
// Still none, since no one has subscribed to the testMutableStateFlow
|
||||
expectNoEvents()
|
||||
|
||||
vaultUnlockFlow.value = listOf(
|
||||
VaultUnlockData(
|
||||
userId = "user_id_1234",
|
||||
status = VaultUnlockData.Status.UNLOCKED,
|
||||
),
|
||||
)
|
||||
|
||||
// Still none, since no one has subscribed to the testMutableStateFlow
|
||||
expectNoEvents()
|
||||
|
||||
val job = sourceMutableStateFlow.launchIn(backgroundScope)
|
||||
// Now we subscribe to the observer flow since have a active user and a listener
|
||||
assertEquals(1, awaitItem())
|
||||
|
||||
userStateFlow.value = mockk<UserStateJson> {
|
||||
every { activeUserId } returns "user_id_4321"
|
||||
}
|
||||
// The user changed, so we clear the previous observer but then resubscribe
|
||||
// with the new user ID
|
||||
assertEquals(0, awaitItem())
|
||||
assertEquals(1, awaitItem())
|
||||
|
||||
vaultUnlockFlow.value = listOf(
|
||||
VaultUnlockData(
|
||||
userId = "user_id_4321",
|
||||
status = VaultUnlockData.Status.UNLOCKED,
|
||||
),
|
||||
)
|
||||
|
||||
// The VaultUnlockData changed, so we clear the previous observer but then resubscribe
|
||||
// with the new data
|
||||
assertEquals(0, awaitItem())
|
||||
assertEquals(1, awaitItem())
|
||||
|
||||
job.cancel()
|
||||
// Job is canceled, we should have no more subscribers
|
||||
assertEquals(0, awaitItem())
|
||||
|
||||
@@ -248,10 +248,13 @@ class VaultDiskSourceTest {
|
||||
// We cannot compare the JSON strings directly because of formatting differences
|
||||
// So we split that off into its own assertion.
|
||||
assertEquals(
|
||||
DOMAINS_ENTITY.copy(domainsJson = ""),
|
||||
storedDomainsEntity.copy(domainsJson = ""),
|
||||
DOMAINS_ENTITY.copy(domainsJson = null),
|
||||
storedDomainsEntity.copy(domainsJson = null),
|
||||
)
|
||||
assertJsonEquals(
|
||||
requireNotNull(DOMAINS_ENTITY.domainsJson),
|
||||
requireNotNull(storedDomainsEntity.domainsJson),
|
||||
)
|
||||
assertJsonEquals(DOMAINS_ENTITY.domainsJson, storedDomainsEntity.domainsJson)
|
||||
|
||||
// Verify the folders dao is updated
|
||||
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
|
||||
class FakeDomainsDao : DomainsDao {
|
||||
var storedDomains: DomainsEntity? = null
|
||||
@@ -18,9 +17,9 @@ class FakeDomainsDao : DomainsDao {
|
||||
deleteDomainsCalled = true
|
||||
}
|
||||
|
||||
override fun getDomains(userId: String): Flow<DomainsEntity> {
|
||||
override fun getDomains(userId: String): Flow<DomainsEntity?> {
|
||||
getDomainsCalled = true
|
||||
return mutableDomainsFlow.filterNotNull()
|
||||
return mutableDomainsFlow
|
||||
}
|
||||
|
||||
override suspend fun insertDomains(domains: DomainsEntity) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user