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 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 - 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 ## [1.2.1 (760)] - 2024-10-22
### Changed ### 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 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 - 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 ## [1.2.1 (760)] - 2024-10-22
### Added ### 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.restoresuccess.viewmodel.RestoreSuccessViewModel
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel 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.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
@ -80,4 +81,5 @@ val viewModelModule =
) )
} }
viewModelOf(::IntegrationsViewModel) 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.contact.UpdateContactArgs
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
@ -25,8 +23,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds
class AddressBookViewModel( class AddressBookViewModel(
observeAddressBookContacts: ObserveAddressBookContactsUseCase, observeAddressBookContacts: ObserveAddressBookContactsUseCase,
@ -97,13 +93,8 @@ class AddressBookViewModel(
} }
AddressBookArgs.PICK_CONTACT -> { AddressBookArgs.PICK_CONTACT -> {
// receiver screen (send) does not have a VM by which to observe so we have to force observeContactPicked.onContactPicked(contact)
// non-cancellable coroutine due to back navigation backNavigationCommand.emit(Unit)
withContext(NonCancellable) {
backNavigationCommand.emit(Unit)
delay(.75.seconds) // wait for the receiver screen to display
observeContactPicked.onContactPicked(contact)
}
} }
} }
} }

View File

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