Address book fix on send screen (#1665)
* Address book fix on send screen * Code cleanup * Documentation update
This commit is contained in:
parent
57cbd3f5f2
commit
078f7b88df
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue