From ddb167daf72a91f1ba1dddd0680a366d7b15ca4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Apr 2024 15:08:37 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/features/send/ui/SendFragment.kt | 40 ------------- .../tap/features/send/ui/SendViewModel.kt | 54 ++++++++++++++++++ .../DefaultQrScanningEventsRepository.kt | 9 +-- .../send/impl/presentation/SendFragment.kt | 31 ---------- .../presentation/viewmodel/SendViewModel.kt | 56 +++++++++++-------- 5 files changed, 88 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 23484d916d..fd6d7105d3 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message @@ -25,14 +24,11 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -63,7 +59,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.text.DecimalFormatSymbols import javax.inject.Inject @@ -87,9 +82,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver - private val cryptoCurrency: CryptoCurrency? - get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY) - val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) @Inject @@ -109,7 +101,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - subscribeToQrCodeScanner() subscribeToTransactionExtrasFields() subscribeToAddressField() subscribeToAmountField() @@ -200,22 +191,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(this) } - private fun CoroutineScope.subscribeToQrCodeScanner() { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .onEach { rawQr -> - cryptoCurrency?.let { cryptoCurrency -> - parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold( - ifLeft = { - onCodeScanned(QrResult(address = rawQr)) - Timber.w(it) - }, - ifRight = { onCodeScanned(it) }, - ) - } ?: onCodeScanned(QrResult(address = rawQr)) - }.launchIn(this) - } - private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) { // TODO: [REDACTED_TASK_KEY] etXlmMemo.inputtedTextAsFlow() @@ -282,21 +257,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(this@subscribeToTransactionExtrasFields) } - private fun onCodeScanned(parsedQr: QrResult) { - if (parsedQr.address.isEmpty()) return - - store.dispatch( - PasteAddress( - data = parsedQr.address, - sourceType = Token.Send.AddressEntered.SourceType.QRCode, - ), - ) - parsedQr.amount?.let { amount -> - store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false)) - } - store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused)) - } - private fun setupAmountLayout() { store.dispatch(SetMainCurrency(restoreMainCurrency())) store.dispatch(ReceiptAction.RefreshReceipt) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index de5e810b88..9f59533686 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -1,7 +1,11 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -9,12 +13,16 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.di.DelayedWork +import com.tangem.tap.features.send.redux.AddressActionUi import com.tangem.tap.features.send.redux.AmountAction import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -31,10 +39,19 @@ internal class SendViewModel @Inject constructor( private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { + init { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQRCodeScanned) + .launchIn(viewModelScope) + } + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] override fun onCreate(owner: LifecycleOwner) { @@ -79,6 +96,43 @@ internal class SendViewModel @Inject constructor( ) } + private fun onQRCodeScanned(qrScanResult: String) { + if (cryptoCurrency != null) { + parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold( + ifRight = { parsedCode -> + store.dispatch( + AddressActionUi.PasteAddress( + data = parsedCode.address, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + parsedCode.amount?.let { amount -> + store.dispatch(AmountAction.SetAmount(amount, isUserInput = false)) + } + // parsedCode.memo?.let { } + }, + ifLeft = { + store.dispatch( + AddressActionUi.PasteAddress( + data = qrScanResult, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + Timber.w(it) + }, + ) + } else { + store.dispatch( + AddressActionUi.PasteAddress( + data = qrScanResult, + sourceType = Token.Send.AddressEntered.SourceType.QRCode, + ), + ) + } + + store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true)) + } + companion object { private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L private const val TAG = "SendViewModel" diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index c4677126a6..ea61431f29 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -6,9 +6,7 @@ import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import com.tangem.domain.tokens.model.CryptoCurrency -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import kotlinx.coroutines.yield import java.math.BigDecimal import java.net.URLDecoder @@ -16,20 +14,15 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { private data class QrScanningEvent(val type: SourceType, val qrCode: String) - private val scannedEvents = MutableSharedFlow(replay = 1) + private val scannedEvents = MutableSharedFlow() override suspend fun emitResult(type: SourceType, qrCode: String) { scannedEvents.emit(QrScanningEvent(type, qrCode)) } - @OptIn(ExperimentalCoroutinesApi::class) override fun subscribeToScanningResults(type: SourceType) = scannedEvents .filter { it.type == type } .map { it.qrCode } - .onEach { - yield() // if we have more than one sub, we must allow them to collect emitted value - scannedEvents.resetReplayCache() - } override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { val withoutSchema = stripSchema(qrCode, cryptoCurrency) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 64e8f1e992..da17befee3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -4,27 +4,18 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.launch import java.lang.ref.WeakReference import javax.inject.Inject @@ -40,9 +31,6 @@ internal class SendFragment : ComposeFragment() { @Inject lateinit var router: SendRouter - @Inject - lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase - @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -65,7 +53,6 @@ internal class SendFragment : ComposeFragment() { analyticsEventsHandler = analyticsEventsHandler, ), ) - listenToQrCode() } @Composable @@ -83,25 +70,7 @@ internal class SendFragment : ComposeFragment() { super.onDestroy() } - private fun listenToQrCode() { - lifecycleScope.launch { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED) - .collect { - delay(QR_SCAN_DELAY) - - // Delayed launch is needed in order for the UI to be drawn and to process the sent events. - // If do not use the delay, then error field is not displayed when - // inserting an incorrect amount by shareUri - viewModel.onQrCodeScanned(it) - } - } - } - companion object { - private const val QR_SCAN_DELAY = 200L - /** Create send fragment instance */ fun create(): SendFragment = SendFragment() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index d1aed8da05..4fd4121ab7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -15,6 +15,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder @@ -92,6 +94,7 @@ internal class SendViewModel @Inject constructor( private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, @@ -189,11 +192,11 @@ internal class SendViewModel @Inject constructor( private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() - private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L init { + subscribeOnQRScannerResult() subscribeOnCurrencyStatusUpdates() subscribeOnBalanceHidden() getTapHelpPreviewAvailability() @@ -215,6 +218,13 @@ internal class SendViewModel @Inject constructor( this.stateRouter = stateRouter } + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(viewModelScope) + } + private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch(dispatchers.main) { getUserWalletUseCase(userWalletId).fold( @@ -536,6 +546,23 @@ internal class SendViewModel @Inject constructor( feeJobHolder.cancel() } } + + private fun onQrCodeScanned(address: String) { + parseQrCodeUseCase(address, cryptoCurrency).fold( + ifRight = { parsedCode -> + onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) + parsedCode.amount?.let { + onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals)) + } + parsedCode.memo?.let { onRecipientMemoValueChange(it) } + }, + ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) + Timber.w(it) + }, + ) + } + // endregion // region amount state clicks @@ -555,26 +582,9 @@ internal class SendViewModel @Inject constructor( override fun onAmountPasteTriggerDismiss() { uiState = amountStateFactory.getOnAmountPastedTriggerDismiss() } - // endregion +// endregion - // region recipient state clicks - fun onQrCodeScanned(address: String) { - viewModelScope.launch(dispatchers.main) { - parseQrCodeUseCase(address, cryptoCurrency).fold( - ifRight = { parsedCode -> - onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) - parsedCode.amount?.let { - onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals)) - } - parsedCode.memo?.let { onRecipientMemoValueChange(it) } - }, - ifLeft = { - onRecipientAddressValueChange(address, EnterAddressSource.QRCode) - Timber.w(it) - }, - ) - }.saveIn(qrScannerJobHolder) - } +// region recipient state clicks override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { viewModelScope.launch { @@ -633,7 +643,7 @@ internal class SendViewModel @Inject constructor( val isRecent = type == EnterAddressSource.RecentAddress if (isRecent && isValidAddress) onNextClick() } - // endregion +// endregion // region fee override fun feeReload() = loadFee() @@ -709,7 +719,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrency = cryptoCurrency, ) } - // endregion +// endregion // region send state clicks override fun onSendClick() { @@ -894,7 +904,7 @@ internal class SendViewModel @Inject constructor( } uiState = stateFactory.getHiddenTapHelpState() } - // endregion +// endregion private companion object { const val CHECK_FEE_UPDATE_DELAY = 60_000L