Updated on 2026-08-14
This commit is contained in:
parent
beeb77543b
commit
15daf08afe
4 changed files with 200 additions and 23 deletions
|
|
@ -100,8 +100,9 @@ internal class EditContactModel @Inject constructor(
|
|||
.stateIn(modelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
/**
|
||||
* The wallet the contact is saved to. For an existing contact it is fixed to the contact's wallet; for a new
|
||||
* contact it follows the selector pick and falls back to the app's currently selected wallet.
|
||||
* The wallet the contact is saved to. A user pick in the selector always wins (this is how an existing contact
|
||||
* is moved to another wallet). With no pick yet it defaults to the existing contact's own wallet, or — for a new
|
||||
* contact — the app's currently selected wallet.
|
||||
*/
|
||||
private val selectedWallet: StateFlow<UserWallet?> = combine(
|
||||
pickedWallet,
|
||||
|
|
@ -110,8 +111,8 @@ internal class EditContactModel @Inject constructor(
|
|||
userWalletsListRepository.userWallets,
|
||||
) { picked, contact, currentSelected, wallets ->
|
||||
when {
|
||||
contact != null -> wallets?.firstOrNull { it.walletId == contact.walletId }
|
||||
picked != null -> picked
|
||||
contact != null -> wallets?.firstOrNull { it.walletId == contact.walletId }
|
||||
else -> currentSelected
|
||||
}
|
||||
}.stateIn(modelScope, SharingStarted.Eagerly, null)
|
||||
|
|
@ -304,19 +305,26 @@ internal class EditContactModel @Inject constructor(
|
|||
val ui = stateController.uiState.value
|
||||
val addresses = ContactAddressEntriesConverter().convert(ui.addresses)
|
||||
val existing = loadedContact.value
|
||||
val isWalletChanged = existing != null && existing.walletId != userWallet.walletId
|
||||
|
||||
saveJob = modelScope.launch {
|
||||
val result = if (existing != null) {
|
||||
saveContactInteractor.updateContact(
|
||||
val result = when {
|
||||
existing == null -> saveContactInteractor.createContact(
|
||||
userWallet = userWallet,
|
||||
name = ui.name,
|
||||
iconColor = ui.colors.selected.name,
|
||||
addresses = addresses,
|
||||
)
|
||||
isWalletChanged -> saveContactInteractor.moveContact(
|
||||
targetWallet = userWallet,
|
||||
contact = existing,
|
||||
name = ui.name,
|
||||
iconColor = ui.colors.selected.name,
|
||||
addresses = addresses,
|
||||
)
|
||||
} else {
|
||||
saveContactInteractor.createContact(
|
||||
else -> saveContactInteractor.updateContact(
|
||||
userWallet = userWallet,
|
||||
contact = existing,
|
||||
name = ui.name,
|
||||
iconColor = ui.colors.selected.name,
|
||||
addresses = addresses,
|
||||
|
|
@ -426,13 +434,13 @@ internal class EditContactModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isWalletChangeable(wallets: List<UserWallet>?): Boolean {
|
||||
val unlockedWalletsCount = wallets.orEmpty().count { !it.isLocked }
|
||||
return params.contactId == null && unlockedWalletsCount > 1
|
||||
return wallets.orEmpty().count { !it.isLocked } > 1
|
||||
}
|
||||
|
||||
private suspend fun validateName(name: String, walletId: UserWalletId): ContactNameValidationError? {
|
||||
if (name.isBlank()) return null
|
||||
if (name == loadedContact.value?.name?.value) return null
|
||||
val loaded = loadedContact.value
|
||||
if (loaded != null && name == loaded.name.value && walletId == loaded.walletId) return null
|
||||
val error = contactNameValidator.validate(walletId, name).leftOrNull() ?: return null
|
||||
if (error is ContactNameValidationError.Format && error.error is ContactName.Error.Empty) return null
|
||||
return error
|
||||
|
|
@ -538,8 +546,9 @@ internal class EditContactModel @Inject constructor(
|
|||
|
||||
/** Dirty when the current editor differs from its baseline — the loaded contact, or the empty new contact. */
|
||||
private fun isDirty(): Boolean {
|
||||
val baseline = loadedContact.value?.toSnapshot() ?: newContactBaseline
|
||||
return currentSnapshot() != baseline
|
||||
val loaded = loadedContact.value ?: return currentSnapshot() != newContactBaseline
|
||||
val isWalletChanged = selectedWallet.value?.walletId?.let { it != loaded.walletId } == true
|
||||
return isWalletChanged || currentSnapshot() != loaded.toSnapshot()
|
||||
}
|
||||
|
||||
private fun currentSnapshot(): EditSnapshot {
|
||||
|
|
|
|||
|
|
@ -658,6 +658,69 @@ internal class EditContactModelTest {
|
|||
coVerify(exactly = 0) { saveContactInteractor.createContact(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contact AND multiple unlocked wallets WHEN created THEN wallet block changeable`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
val walletB = createWallet(id = "bb", name = "Wallet B")
|
||||
setupWallets(wallets = listOf(walletA, walletB), selected = walletA)
|
||||
every { getContactByIdUseCase(ContactId("c-1")) } returns
|
||||
MutableStateFlow(existingContact(walletId = "aa", name = "Alice", address = "0xABC"))
|
||||
|
||||
// Act
|
||||
val model = createModel(testScope = this, params = createParams(contactId = ContactId("c-1")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — an existing contact can now be moved, so its wallet block is changeable.
|
||||
assertThat(model.state.value.walletBlock.isChangeable).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contact AND wallet changed WHEN save clicked THEN moveContact called`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
val walletB = createWallet(id = "bb", name = "Wallet B")
|
||||
setupWallets(wallets = listOf(walletA, walletB), selected = walletA)
|
||||
val contact = existingContact(walletId = "aa", name = "Alice", address = "0xABC")
|
||||
every { getContactByIdUseCase(ContactId("c-1")) } returns MutableStateFlow(contact)
|
||||
val moved = mockk<Contact> { every { id } returns ContactId(value = "moved-1") }
|
||||
coEvery { saveContactInteractor.moveContact(any(), any(), any(), any(), any()) } returns moved.right()
|
||||
val model = createModel(testScope = this, params = createParams(contactId = ContactId("c-1")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act — pick wallet B in the selector, then save.
|
||||
selectedWalletData.tryEmit(walletB to mockk())
|
||||
advanceUntilIdle()
|
||||
model.state.value.saveButton.onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert — the contact is moved to wallet B; plain update/create are not used.
|
||||
coVerify(exactly = 1) { saveContactInteractor.moveContact(walletB, contact, "Alice", any(), any()) }
|
||||
coVerify(exactly = 0) { saveContactInteractor.updateContact(any(), any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { saveContactInteractor.createContact(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contact AND wallet unchanged WHEN save clicked THEN updateContact used not move`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
val walletB = createWallet(id = "bb", name = "Wallet B")
|
||||
setupWallets(wallets = listOf(walletA, walletB), selected = walletA)
|
||||
val contact = existingContact(walletId = "aa", name = "Alice", address = "0xABC")
|
||||
every { getContactByIdUseCase(ContactId("c-1")) } returns MutableStateFlow(contact)
|
||||
coEvery { saveContactInteractor.updateContact(any(), any(), any(), any(), any()) } returns contact.right()
|
||||
val model = createModel(testScope = this, params = createParams(contactId = ContactId("c-1")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act — no wallet pick, so it stays in its own wallet.
|
||||
model.state.value.saveButton.onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { saveContactInteractor.updateContact(walletA, contact, "Alice", any(), any()) }
|
||||
coVerify(exactly = 0) { saveContactInteractor.moveContact(any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new contact saved WHEN success THEN contact-added snackbar shown`() = runTest {
|
||||
// Arrange
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue