Updated on 2026-08-14
This commit is contained in:
parent
98d0d9c75e
commit
b1c19eff9e
21 changed files with 538 additions and 52 deletions
|
|
@ -31,4 +31,11 @@ sealed interface AddressBookSyncError {
|
|||
|
||||
/** Any other unexpected failure (encryption, missing data, unmapped HTTP code). */
|
||||
data object Unknown : AddressBookSyncError
|
||||
|
||||
/**
|
||||
* The stored book uses a contract version newer than this build supports
|
||||
* ([com.tangem.domain.addressbook.model.AddressBookBlob.isVersionCompatible]). Determined locally (no
|
||||
* network), the write is refused so a book the app cannot fully understand is not downgraded/overwritten.
|
||||
*/
|
||||
data object VersionMismatch : AddressBookSyncError
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.addressbook.model
|
||||
|
||||
import com.tangem.domain.addressbook.model.AddressBookBlob.Companion.CURRENT_VERSION
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class AddressBookBlob(
|
||||
@SerialName("version")
|
||||
val version: String = CURRENT_VERSION, // TODO Will come from BE in [REDACTED_TASK_KEY]
|
||||
val version: String = CURRENT_VERSION,
|
||||
@SerialName("walletId")
|
||||
val walletId: String,
|
||||
@SerialName("updatedAt")
|
||||
|
|
@ -39,7 +40,30 @@ data class AddressBookBlob(
|
|||
val authTag: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Whether this build can safely read and write the blob — i.e. its [version] is not newer than the
|
||||
* contract this app supports ([CURRENT_VERSION]). See [isVersionCompatible].
|
||||
*/
|
||||
val isVersionCompatible: Boolean get() = isVersionCompatible(version)
|
||||
|
||||
companion object {
|
||||
const val CURRENT_VERSION = "1.0"
|
||||
|
||||
/**
|
||||
* A blob is compatible when its contract version is **not higher** than [CURRENT_VERSION]. The version
|
||||
* is treated as a plain number (major/minor are not distinguished — any higher value is incompatible):
|
||||
* - lower (`0.9`) — the app understands a newer contract; reads work and a write upgrades the backend
|
||||
* copy to [CURRENT_VERSION].
|
||||
* - equal — same contract.
|
||||
* - higher (`1.1`, `2.0`) — the backend contract is newer than this build understands, so the book must
|
||||
* be treated as read-only-opaque (not read, not written).
|
||||
*
|
||||
* A version that cannot be parsed as a number is treated as incompatible — safer to refuse a blob we
|
||||
* cannot reason about.
|
||||
*/
|
||||
fun isVersionCompatible(version: String): Boolean {
|
||||
val parsed = version.trim().toDoubleOrNull() ?: return false
|
||||
return parsed <= CURRENT_VERSION.toDouble()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,14 @@ interface AddressBookRepository {
|
|||
|
||||
suspend fun getContact(userWalletId: UserWalletId, name: String): Contact?
|
||||
|
||||
/**
|
||||
* Whether the stored address book(s) can be used by this build — i.e. their contract version is not newer
|
||||
* than the one this app supports (see [com.tangem.domain.addressbook.model.AddressBookBlob.isVersionCompatible]).
|
||||
* @param userWalletId a specific wallet, or `null` to check every wallet — `null` is compatible only when
|
||||
* **all** currently stored books are compatible.
|
||||
*/
|
||||
fun isAddressBookCompatible(userWalletId: UserWalletId? = null): Flow<Boolean>
|
||||
|
||||
suspend fun saveContact(contact: Contact): Either<AddressBookSyncError, Unit>
|
||||
|
||||
suspend fun deleteContact(id: ContactId): Either<AddressBookSyncError, Unit>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.addressbook.usecase
|
||||
|
||||
import com.tangem.domain.addressbook.repository.AddressBookRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Emits whether the stored address book(s) can be used by this build — `false` when the backend contract
|
||||
* version is newer than the one this app supports, so consumers can degrade (hide the book, block editing).
|
||||
*
|
||||
* @param userWalletId a specific wallet, or `null` to check every wallet (compatible only when all stored
|
||||
* books are compatible). See [AddressBookRepository.isAddressBookCompatible].
|
||||
*/
|
||||
class IsAddressBookCompatibleUseCase(
|
||||
private val repository: AddressBookRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId? = null): Flow<Boolean> =
|
||||
repository.isAddressBookCompatible(userWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.domain.addressbook.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class AddressBookBlobVersionTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideVersions")
|
||||
fun `isVersionCompatible against CURRENT_VERSION`(model: VersionModel) {
|
||||
// CURRENT_VERSION is 1.0 — the cases below are written relative to it.
|
||||
assertThat(AddressBookBlob.isVersionCompatible(model.version)).isEqualTo(model.expectedCompatible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blob property mirrors the version function`() {
|
||||
val newer = AddressBookBlob(
|
||||
version = "2.0",
|
||||
walletId = "w",
|
||||
updatedAt = "t",
|
||||
nonce = "n",
|
||||
ciphertext = "c",
|
||||
authTag = "a",
|
||||
)
|
||||
|
||||
assertThat(newer.isVersionCompatible).isFalse()
|
||||
}
|
||||
|
||||
internal data class VersionModel(val version: String, val expectedCompatible: Boolean)
|
||||
|
||||
private fun provideVersions() = listOf(
|
||||
VersionModel(version = "1.0", expectedCompatible = true), // equal
|
||||
VersionModel(version = "0.9", expectedCompatible = true), // lower
|
||||
VersionModel(version = "0.5", expectedCompatible = true), // lower
|
||||
VersionModel(version = "1", expectedCompatible = true), // 1 == 1.0
|
||||
VersionModel(version = "1.1", expectedCompatible = false), // higher
|
||||
VersionModel(version = "2.0", expectedCompatible = false), // higher
|
||||
VersionModel(version = "1.10", expectedCompatible = false), // 1.10 == 1.1 as a number, higher than 1.0
|
||||
VersionModel(version = "", expectedCompatible = false), // not a number
|
||||
VersionModel(version = "1.0.0", expectedCompatible = false), // not a number
|
||||
VersionModel(version = "abc", expectedCompatible = false) // not a number
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue