Address book fix on send screen (#1665)

* Address book fix on send screen

* Code cleanup

* Documentation update
This commit is contained in:
Milan 2024-11-08 12:03:51 +01:00 committed by GitHub
parent 57cbd3f5f2
commit 078f7b88df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 96 deletions

View File

@ -10,6 +10,9 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- The device authentication feature on the Zashi app launch has been added
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field
## [1.2.1 (760)] - 2024-10-22
### Changed

View File

@ -13,6 +13,9 @@ directly impact users rather than highlighting other key architectural updates.*
- The device authentication feature on the Zashi app launch has been added
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field
## [1.2.1 (760)] - 2024-10-22
### Added

View File

@ -21,6 +21,7 @@ import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
import co.electriccoin.zcash.ui.screen.send.SendViewModel
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@ -80,4 +81,5 @@ val viewModelModule =
)
}
viewModelOf(::IntegrationsViewModel)
viewModelOf(::SendViewModel)
}

View File

@ -16,8 +16,6 @@ import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
import co.electriccoin.zcash.ui.screen.contact.UpdateContactArgs
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
@ -25,8 +23,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds
class AddressBookViewModel(
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
@ -97,13 +93,8 @@ class AddressBookViewModel(
}
AddressBookArgs.PICK_CONTACT -> {
// receiver screen (send) does not have a VM by which to observe so we have to force
// non-cancellable coroutine due to back navigation
withContext(NonCancellable) {
backNavigationCommand.emit(Unit)
delay(.75.seconds) // wait for the receiver screen to display
observeContactPicked.onContactPicked(contact)
}
observeContactPicked.onContactPicked(contact)
backNavigationCommand.emit(Unit)
}
}
}

View File

@ -6,11 +6,8 @@ import android.content.pm.PackageManager
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@ -32,31 +29,23 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.model.AddressBookContact
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.AmountState
import co.electriccoin.zcash.ui.screen.send.model.MemoState
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import co.electriccoin.zcash.ui.screen.send.model.SendAddressBookState
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.koin.androidx.compose.koinViewModel
import org.zecdev.zip321.ZIP321
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@Composable
@Suppress("LongParameterList")
@ -140,8 +129,15 @@ internal fun WrapSend(
val navController = LocalNavController.current
val observeContactByAddress = koinInject<ObserveContactByAddressUseCase>()
val observeContactPicked = koinInject<ObserveContactPickedUseCase>()
val viewModel = koinViewModel<SendViewModel>()
LaunchedEffect(Unit) {
viewModel.navigateCommand.collect {
navController.navigate(it)
}
}
val sendAddressBookState by viewModel.sendAddressBookState.collectAsStateWithLifecycle()
val context = LocalContext.current
@ -150,13 +146,10 @@ internal fun WrapSend(
val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) }
// Address computation:
val (recipientAddressState, setRecipientAddressState) =
rememberSaveable(stateSaver = RecipientAddressState.Saver) {
mutableStateOf(RecipientAddressState.new(zecSend?.destination?.address ?: "", null))
}
val recipientAddressState by viewModel.recipientAddressState.collectAsStateWithLifecycle()
if (sendArguments?.recipientAddress != null) {
setRecipientAddressState(
viewModel.onRecipientAddressChanged(
RecipientAddressState.new(
sendArguments.recipientAddress.address,
sendArguments.recipientAddress.type
@ -183,68 +176,6 @@ internal fun WrapSend(
}
}
val existingContact: MutableState<AddressBookContact?> = remember { mutableStateOf(null) }
var isHintVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
observeContactPicked().collect {
setRecipientAddressState(it)
}
}
LaunchedEffect(recipientAddressState.address) {
observeContactByAddress(recipientAddressState.address).collect {
existingContact.value = it
}
}
LaunchedEffect(existingContact, recipientAddressState.type) {
val exists = existingContact.value != null
val isValid = recipientAddressState.type?.isNotValid == false
if (!exists && isValid) {
isHintVisible = true
delay(3.seconds)
isHintVisible = false
} else {
isHintVisible = false
}
}
val sendAddressBookState =
remember(existingContact.value, recipientAddressState, isHintVisible) {
derivedStateOf {
val exists = existingContact.value != null
val isValid = recipientAddressState.type?.isNotValid == false
val mode =
if (isValid) {
if (exists) {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK
} else {
SendAddressBookState.Mode.ADD_TO_ADDRESS_BOOK
}
} else {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK
}
SendAddressBookState(
mode = mode,
isHintVisible = isHintVisible,
onButtonClick = {
when (mode) {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK -> {
navController.navigate(AddressBookArgs(AddressBookArgs.PICK_CONTACT))
}
SendAddressBookState.Mode.ADD_TO_ADDRESS_BOOK -> {
navController.navigate(AddContactArgs(recipientAddressState.address))
}
}
}
)
}
}
// Amount computation:
val (amountState, setAmountState) =
rememberSaveable(stateSaver = AmountState.Saver) {
@ -302,7 +233,7 @@ internal fun WrapSend(
if (sendArguments?.clearForm == true) {
setSendStage(SendStage.Form)
setZecSend(null)
setRecipientAddressState(RecipientAddressState.new("", null))
viewModel.onRecipientAddressChanged(RecipientAddressState.new("", null))
setAmountState(
AmountState.newFromZec(
context = context,
@ -359,7 +290,7 @@ internal fun WrapSend(
recipientAddressState = recipientAddressState,
onRecipientAddressChange = {
scope.launch {
setRecipientAddressState(
viewModel.onRecipientAddressChanged(
RecipientAddressState.new(
address = it,
// TODO [#342]: Verify Addresses without Synchronizer
@ -379,7 +310,7 @@ internal fun WrapSend(
topAppBarSubTitleState = topAppBarSubTitleState,
walletSnapshot = walletSnapshot,
exchangeRateState = exchangeRateState,
sendAddressBookState = sendAddressBookState.value
sendAddressBookState = sendAddressBookState
)
}
}

View File

@ -0,0 +1,115 @@
package co.electriccoin.zcash.ui.screen.send
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
import co.electriccoin.zcash.ui.screen.send.model.RecipientAddressState
import co.electriccoin.zcash.ui.screen.send.model.SendAddressBookState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
class SendViewModel(
private val observeContactByAddress: ObserveContactByAddressUseCase,
private val observeContactPicked: ObserveContactPickedUseCase,
) : ViewModel() {
val recipientAddressState = MutableStateFlow(RecipientAddressState.new("", null))
val navigateCommand = MutableSharedFlow<String>()
@OptIn(ExperimentalCoroutinesApi::class)
val sendAddressBookState =
recipientAddressState.flatMapLatest { recipientAddressState ->
observeContactByAddress(recipientAddressState.address).flatMapLatest { contact ->
flow {
val exists = contact != null
val isValid = recipientAddressState.type?.isNotValid == false
val mode =
if (isValid) {
if (exists) {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK
} else {
SendAddressBookState.Mode.ADD_TO_ADDRESS_BOOK
}
} else {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK
}
val isHintVisible = !exists && isValid
emit(
SendAddressBookState(
mode = mode,
isHintVisible = isHintVisible,
onButtonClick = { onAddressBookButtonClicked(mode, recipientAddressState) }
)
)
if (isHintVisible) {
delay(3.seconds)
emit(
SendAddressBookState(
mode = mode,
isHintVisible = false,
onButtonClick = { onAddressBookButtonClicked(mode, recipientAddressState) }
)
)
}
}
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
SendAddressBookState(
mode = SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK,
isHintVisible = false,
onButtonClick = {
onAddressBookButtonClicked(
mode = SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK,
recipient = recipientAddressState.value
)
}
)
)
init {
viewModelScope.launch {
observeContactPicked().collect {
onRecipientAddressChanged(it)
}
}
}
private fun onAddressBookButtonClicked(
mode: SendAddressBookState.Mode,
recipient: RecipientAddressState
) {
when (mode) {
SendAddressBookState.Mode.PICK_FROM_ADDRESS_BOOK ->
viewModelScope.launch {
navigateCommand.emit(AddressBookArgs(AddressBookArgs.PICK_CONTACT))
}
SendAddressBookState.Mode.ADD_TO_ADDRESS_BOOK ->
viewModelScope.launch {
navigateCommand.emit(AddContactArgs(recipient.address))
}
}
}
fun onRecipientAddressChanged(state: RecipientAddressState) {
recipientAddressState.update { state }
}
}