From 078f7b88df6a622801a18529fd133809eb72d40b Mon Sep 17 00:00:00 2001 From: Milan Date: Fri, 8 Nov 2024 12:03:51 +0100 Subject: [PATCH] Address book fix on send screen (#1665) * Address book fix on send screen * Code cleanup * Documentation update --- CHANGELOG.md | 3 + docs/whatsNew/WHATS_NEW_EN.md | 3 + .../electriccoin/zcash/di/ViewModelModule.kt | 2 + .../viewmodel/AddressBookViewModel.kt | 13 +- .../zcash/ui/screen/send/AndroidSend.kt | 101 +++------------ .../zcash/ui/screen/send/SendViewModel.kt | 115 ++++++++++++++++++ 6 files changed, 141 insertions(+), 96 deletions(-) create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/SendViewModel.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 859cd6a1..043e4da2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index 7dc1fafe..168ef4c4 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -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 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index a6aab523..20efe5ac 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -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) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/addressbook/viewmodel/AddressBookViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/addressbook/viewmodel/AddressBookViewModel.kt index c681d3d7..b79ef0f1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/addressbook/viewmodel/AddressBookViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/addressbook/viewmodel/AddressBookViewModel.kt @@ -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) } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt index 8542c220..1b837ee8 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt @@ -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() - val observeContactPicked = koinInject() + val viewModel = koinViewModel() + + 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 = 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 ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/SendViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/SendViewModel.kt new file mode 100644 index 00000000..6003e5d8 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/SendViewModel.kt @@ -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() + + @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 } + } +}