Updated on 2026-08-14
This commit is contained in:
commit
b3dfa28319
6 changed files with 149 additions and 8 deletions
|
|
@ -9,10 +9,15 @@ sealed interface AddressBookOpenMode {
|
|||
@Serializable
|
||||
data object Default : AddressBookOpenMode
|
||||
|
||||
/**
|
||||
* Opened to create a contact for an already-known [address] on [networkId]. [memo] carries the destination tag /
|
||||
* memo that went with it, when the network supports transaction extras and one was entered.
|
||||
*/
|
||||
@Serializable
|
||||
data class WithContactCreation(
|
||||
val address: String,
|
||||
val networkId: String,
|
||||
val memo: String? = null,
|
||||
) : AddressBookOpenMode
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -74,10 +74,18 @@ internal class AddressBookChildFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/** Builds the address attached up-front in WithContactCreation mode, when both the address and network are known. */
|
||||
/**
|
||||
* Builds the address attached up-front in WithContactCreation mode, when both the address and network are known.
|
||||
* The memo rides along when one was provided; `ContactAddressEntriesConverter` drops it on save if the network has
|
||||
* no transaction extras.
|
||||
*/
|
||||
private fun buildPredefinedAddress(route: AddressBookRoute.EditContact): ValidatedAddress? {
|
||||
val address = route.predefinedAddress ?: return null
|
||||
val networkId = route.predefinedNetworkId ?: return null
|
||||
return ValidatedAddress(address = address, networkIds = persistentListOf(networkId))
|
||||
return ValidatedAddress(
|
||||
address = address,
|
||||
networkIds = persistentListOf(networkId),
|
||||
memo = route.predefinedMemo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -139,6 +139,7 @@ internal class DefaultAddressBookComponent @AssistedInject constructor(
|
|||
AddressBookRoute.EditContact(
|
||||
predefinedAddress = mode.address,
|
||||
predefinedNetworkId = mode.networkId,
|
||||
predefinedMemo = mode.memo,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,15 +16,17 @@ internal sealed class AddressBookRoute {
|
|||
/**
|
||||
* if [contactId] is not null we should fetch existing contact.
|
||||
*
|
||||
* [predefinedAddress] and [predefinedNetworkId] are set only when the feature is opened in
|
||||
* [predefinedAddress], [predefinedNetworkId] and [predefinedMemo] are set only when the feature is opened in
|
||||
* [com.tangem.common.routing.entity.AddressBookOpenMode.WithContactCreation] mode — the address and its
|
||||
* network are already known, so the new contact is opened with that address already attached.
|
||||
* network are already known, so the new contact is opened with that address already attached. [predefinedMemo]
|
||||
* stays null when the network has no transaction extras or no memo was entered.
|
||||
*/
|
||||
@Serializable
|
||||
data class EditContact(
|
||||
val contactId: String? = null,
|
||||
val predefinedAddress: String? = null,
|
||||
val predefinedNetworkId: String? = null,
|
||||
val predefinedMemo: String? = null,
|
||||
) : AddressBookRoute()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -183,12 +183,16 @@ internal class SendDestinationModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/** Opens the contact editor pre-filled with the sent address/network to save the recipient (success screen). */
|
||||
/** Opens the contact editor pre-filled with the sent address/network/memo to save the recipient (success screen). */
|
||||
fun onAddContactClick() {
|
||||
val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.actualAddress ?: return
|
||||
val content = uiState.value as? DestinationUM.Content ?: return
|
||||
router.push(
|
||||
AppRoute.AddressBook(
|
||||
AddressBookOpenMode.WithContactCreation(address = address, networkId = cryptoCurrency.network.rawId),
|
||||
AddressBookOpenMode.WithContactCreation(
|
||||
address = content.addressTextField.actualAddress,
|
||||
networkId = cryptoCurrency.network.rawId,
|
||||
memo = content.memoTextField?.value?.takeIf(String::isNotBlank),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.entity.AddressBookOpenMode
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
|
|
@ -105,7 +107,14 @@ internal class SendDestinationModelTest {
|
|||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
// PER_CLASS parameterized nested classes reuse one instance — reset verified mocks between rows.
|
||||
clearMocks(callback, validateWalletAddressUseCase, answers = false, recordedCalls = true, childMocks = false)
|
||||
clearMocks(
|
||||
callback,
|
||||
validateWalletAddressUseCase,
|
||||
router,
|
||||
answers = false,
|
||||
recordedCalls = true,
|
||||
childMocks = false,
|
||||
)
|
||||
coEvery { getNetworkAddressesUseCase.invokeSync(any(), any<Network.RawID>()) } returns emptyList()
|
||||
every { getWalletsUseCase() } returns flowOf(emptyList())
|
||||
every { multiAccountStatusListSupplier() } returns flowOf(emptyList())
|
||||
|
|
@ -536,6 +545,74 @@ internal class SendDestinationModelTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OnAddContactClick {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN onAddContactClick THEN address book opened with the sent address and memo`(
|
||||
model: AddContactRouteModel,
|
||||
) = runTest {
|
||||
// Arrange
|
||||
val sut = buildModel(
|
||||
initialState = contentState(
|
||||
address = "0xDave",
|
||||
blockchainAddress = model.blockchainAddress,
|
||||
memo = model.memoFieldValue,
|
||||
),
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
sut.onAddContactClick()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) {
|
||||
router.push(
|
||||
AppRoute.AddressBook(
|
||||
AddressBookOpenMode.WithContactCreation(
|
||||
address = model.expectedAddress,
|
||||
networkId = networkRawId,
|
||||
memo = model.expectedMemo,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// memo entered -> carried over to the new contact
|
||||
AddContactRouteModel(
|
||||
memoFieldValue = "12345",
|
||||
blockchainAddress = null,
|
||||
expectedAddress = "0xDave",
|
||||
expectedMemo = "12345",
|
||||
),
|
||||
// memo field shown but left blank -> nothing to carry
|
||||
AddContactRouteModel(
|
||||
memoFieldValue = " ",
|
||||
blockchainAddress = null,
|
||||
expectedAddress = "0xDave",
|
||||
expectedMemo = null,
|
||||
),
|
||||
// network without transaction extras -> no memo field at all
|
||||
AddContactRouteModel(
|
||||
memoFieldValue = null,
|
||||
blockchainAddress = null,
|
||||
expectedAddress = "0xDave",
|
||||
expectedMemo = null,
|
||||
),
|
||||
// ENS-resolved recipient -> the canonical address is saved, not the typed name
|
||||
AddContactRouteModel(
|
||||
memoFieldValue = "42",
|
||||
blockchainAddress = "0xCanonical",
|
||||
expectedAddress = "0xCanonical",
|
||||
expectedMemo = "42",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SyncAddressBooks {
|
||||
|
||||
|
|
@ -741,8 +818,52 @@ internal class SendDestinationModelTest {
|
|||
private fun content(model: SendDestinationModel): DestinationUM.Content =
|
||||
model.uiState.value as DestinationUM.Content
|
||||
|
||||
/**
|
||||
* A ready-made [DestinationUM.Content] with `isInitialized = true`, so `SendDestinationInitialStateTransformer`
|
||||
* leaves it alone and the test fully controls the address / memo fields. A null [memo] means the network has no
|
||||
* transaction extras and therefore no memo field.
|
||||
*/
|
||||
private fun contentState(
|
||||
address: String,
|
||||
blockchainAddress: String? = null,
|
||||
memo: String? = null,
|
||||
): DestinationUM.Content = DestinationUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
isInitialized = true,
|
||||
addressTextField = DestinationTextFieldUM.RecipientAddress(
|
||||
value = address,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Text),
|
||||
placeholder = stringReference(""),
|
||||
label = stringReference(""),
|
||||
isValuePasted = false,
|
||||
blockchainAddress = blockchainAddress,
|
||||
),
|
||||
memoTextField = memo?.let {
|
||||
DestinationTextFieldUM.RecipientMemo(
|
||||
value = it,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done, keyboardType = KeyboardType.Text),
|
||||
placeholder = stringReference(""),
|
||||
label = stringReference(""),
|
||||
disabledText = stringReference(""),
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
)
|
||||
},
|
||||
recent = persistentListOf(),
|
||||
wallets = persistentListOf(),
|
||||
networkName = "Ethereum",
|
||||
isRecentHidden = false,
|
||||
)
|
||||
|
||||
data class AutoNextModel(val addressValidation: AddressValidationResult, val expectedNextClicks: Int)
|
||||
|
||||
data class AddContactRouteModel(
|
||||
val memoFieldValue: String?,
|
||||
val blockchainAddress: String?,
|
||||
val expectedAddress: String,
|
||||
val expectedMemo: String?,
|
||||
)
|
||||
|
||||
data class AddContactModel(
|
||||
val isAddContactAvailable: Boolean,
|
||||
val savedAddresses: List<String>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue