Updated on 2026-08-14
This commit is contained in:
parent
c5c28eca82
commit
ddb167daf7
5 changed files with 88 additions and 102 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<QrScanningEvent>(replay = 1)
|
||||
private val scannedEvents = MutableSharedFlow<QrScanningEvent>()
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue