Updated on 2026-08-14
This commit is contained in:
parent
13fcfb8728
commit
b723940318
16 changed files with 182 additions and 22 deletions
|
|
@ -113,4 +113,16 @@ internal object WalletsDomainModule {
|
|||
): ValidateWalletMemoUseCase {
|
||||
return ValidateWalletMemoUseCase(walletAddressServiceRepository = walletAddressServiceRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesParseSharedAddressUseCase(
|
||||
walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ParseSharedAddressUseCase {
|
||||
return ParseSharedAddressUseCase(
|
||||
walletAddressServiceRepository = walletAddressServiceRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.data.wallets
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.blockchains.near.NearWalletManager
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.errors.ParsedQrCodeErrors
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
import java.math.BigInteger
|
||||
|
||||
|
|
@ -42,6 +45,39 @@ class DefaultWalletAddressServiceRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val addressSchemeSplit = when (blockchain) {
|
||||
Blockchain.BitcoinCash, Blockchain.Kaspa -> listOf(input)
|
||||
else -> input.split(":")
|
||||
}
|
||||
|
||||
val noSchemeAddress = when (addressSchemeSplit.size) {
|
||||
1 -> { // no scheme
|
||||
input
|
||||
}
|
||||
2 -> { // scheme
|
||||
if (blockchain.validateShareScheme(addressSchemeSplit[0])) {
|
||||
addressSchemeSplit[1]
|
||||
} else {
|
||||
// to preserve old logic
|
||||
return ParsedQrCode(address = input)
|
||||
}
|
||||
}
|
||||
else -> { // invalid URI
|
||||
throw ParsedQrCodeErrors.InvalidUriError
|
||||
}
|
||||
}
|
||||
|
||||
val uri = Uri.parse(noSchemeAddress)
|
||||
val address = uri.host ?: noSchemeAddress
|
||||
val amount = uri.getQueryParameter("amount")?.toBigDecimalOrNull()
|
||||
return ParsedQrCode(
|
||||
address = address,
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Blockchain.isNear(): Boolean {
|
||||
return this == Blockchain.Near || this == Blockchain.NearTestnet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ParsedQrCode(
|
||||
val address: String,
|
||||
val memo: String? = null,
|
||||
val amount: BigDecimal? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.wallets.models.errors
|
||||
|
||||
sealed class ParsedQrCodeErrors : Throwable() {
|
||||
|
||||
object InvalidUriError : ParsedQrCodeErrors()
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.wallets.repository
|
||||
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
|
|
@ -11,4 +12,6 @@ interface WalletAddressServiceRepository {
|
|||
suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
|
||||
fun validateMemo(network: Network, memo: String): Boolean
|
||||
|
||||
suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.wallets.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.ParsedQrCode
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ParseSharedAddressUseCase(
|
||||
private val walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(input: String, network: Network): Either<Throwable, ParsedQrCode> {
|
||||
return withContext(dispatchers.io) {
|
||||
Either.catch {
|
||||
walletAddressServiceRepository.parseSharedAddress(input, network)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,9 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
private val viewModel by viewModels<QrScanningViewModel>()
|
||||
|
||||
private var cameraExecutor: ExecutorService by Delegates.notNull()
|
||||
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(viewModel::onQrScanned)
|
||||
}
|
||||
|
||||
private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (!it) parentFragmentManager.popBackStack()
|
||||
|
|
@ -66,7 +69,7 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
}
|
||||
QrScanningContent(
|
||||
executor = { cameraExecutor },
|
||||
analyzer = { MLKitBarcodeAnalyzer(viewModel::onQrScanned) },
|
||||
analyzer = { analyzer },
|
||||
uiState = viewModel.uiState,
|
||||
)
|
||||
setFitSystemWindows(fit = false)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import javax.annotation.concurrent.Immutable
|
|||
class MLKitBarcodeAnalyzer(private val onScanned: (String) -> Unit) : ImageAnalysis.Analyzer {
|
||||
|
||||
private var isScanning: Boolean = false
|
||||
private val scanner = BarcodeScanning.getClient()
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null && !isScanning) {
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
val scanner = BarcodeScanning.getClient()
|
||||
|
||||
isScanning = true
|
||||
scanner.process(image)
|
||||
|
|
|
|||
|
|
@ -38,11 +38,16 @@ internal class QrScanningViewModel @Inject constructor(
|
|||
var uiState: QrScanningState by mutableStateOf(factory.getInitialState(source, network))
|
||||
private set
|
||||
|
||||
private var isScanned = false
|
||||
|
||||
override fun onBackClick() = router.popBackStack()
|
||||
|
||||
override fun onQrScanned(qrCode: String) {
|
||||
if (qrCode.isNotBlank()) {
|
||||
router.popBackStack()
|
||||
if (!isScanned) {
|
||||
router.popBackStack()
|
||||
isScanned = true
|
||||
}
|
||||
viewModelScope.launch(dispatcher.main) {
|
||||
emitQrScannedEventUseCase.invoke(source, qrCode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,11 +58,12 @@ dependencies {
|
|||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.qrScanning.api)
|
||||
implementation(projects.features.qrScanning.impl)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.navigation.NavigationAction
|
|||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
||||
|
|
@ -31,4 +33,16 @@ internal class DefaultSendRouter(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun openQrCodeScanner(network: String) {
|
||||
reduxNavController.navigate(
|
||||
action = NavigationAction.NavigateTo(
|
||||
screen = AppScreen.QrScanning,
|
||||
bundle = bundleOf(
|
||||
QrScanningRouter.SOURCE_KEY to SourceType.SEND,
|
||||
QrScanningRouter.NETWORK_KEY to network,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,4 +11,7 @@ interface InnerSendRouter : SendRouter {
|
|||
|
||||
/** Open token details screen by [userWalletId] and [currency] */
|
||||
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
/** Open QR code scanner screen */
|
||||
fun openQrCodeScanner(network: String)
|
||||
}
|
||||
|
|
@ -4,16 +4,25 @@ 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.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
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.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.usecase.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
|
||||
|
||||
|
|
@ -29,6 +38,9 @@ internal class SendFragment : ComposeFragment() {
|
|||
@Inject
|
||||
lateinit var router: SendRouter
|
||||
|
||||
@Inject
|
||||
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
private val innerSendRouter: InnerSendRouter
|
||||
get() = requireNotNull(router as? InnerSendRouter) {
|
||||
|
|
@ -44,6 +56,7 @@ internal class SendFragment : ComposeFragment() {
|
|||
fragmentManager = WeakReference(parentFragmentManager),
|
||||
),
|
||||
)
|
||||
listenToQrCode()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -60,7 +73,24 @@ 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 etAmount error field is not displayed when
|
||||
// inserting an incorrect amount by shareUri
|
||||
viewModel.onRecipientAddressScanned(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QR_SCAN_DELAY = 200L
|
||||
|
||||
/** Create send fragment instance */
|
||||
fun create(): SendFragment = SendFragment()
|
||||
|
|
|
|||
|
|
@ -94,11 +94,14 @@ internal class SendStateFactory(
|
|||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState = currentStateProvider().copy(
|
||||
amountState = amountStateConverter.convert(Unit),
|
||||
recipientState = recipientStateConverter.convert(Unit),
|
||||
feeState = feeStateConverter.convert(Unit),
|
||||
)
|
||||
fun getReadyState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
amountState = state.amountState ?: amountStateConverter.convert(Unit),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(Unit),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region amount state clicks
|
||||
|
|
@ -160,9 +163,8 @@ internal class SendStateFactory(
|
|||
isValidating = false,
|
||||
addressTextField = recipientState.addressTextField.copy(
|
||||
error = when {
|
||||
!isValidAddress || isAddressInWallet -> resourceReference(
|
||||
R.string.send_recipient_address_error,
|
||||
)
|
||||
!isValidAddress -> resourceReference(R.string.send_recipient_address_error)
|
||||
isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet)
|
||||
else -> null
|
||||
},
|
||||
isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.features.send.impl.presentation.state.SendUiState
|
|||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
internal class SendAmountFieldChangeConverter(
|
||||
|
|
@ -106,8 +105,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
var trimmedValue = this
|
||||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
|
||||
return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar)
|
||||
return trimmedValue.replace(TRIM_REGEX.toRegex(), ".")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,15 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotificationFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.getFee
|
||||
|
|
@ -67,6 +68,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
|
|
@ -116,6 +118,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private var feeJobHolder = JobHolder()
|
||||
private var addressValidationJobHolder = JobHolder()
|
||||
private var sendNotificationsJobHolder = JobHolder()
|
||||
private var qrScannerJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
|
|
@ -326,9 +329,7 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onNextClick() = stateRouter.onNextClick()
|
||||
override fun onPrevClick() = stateRouter.onPrevClick()
|
||||
|
||||
override fun onQrCodeScanClick() {
|
||||
// TODO Add QR code scanning
|
||||
}
|
||||
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
|
|
@ -355,6 +356,21 @@ internal class SendViewModel @Inject constructor(
|
|||
// endregion
|
||||
|
||||
// region recipient state clicks
|
||||
fun onRecipientAddressScanned(address: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address)
|
||||
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
ifLeft = {
|
||||
Timber.w(it)
|
||||
},
|
||||
)
|
||||
}.saveIn(qrScannerJobHolder)
|
||||
}
|
||||
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
uiState = stateFactory.onRecipientAddressValueChange(value)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue