Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-18 10:45:27 +05:00
commit 26c5c486bb
17 changed files with 111 additions and 31 deletions

View file

@ -45,7 +45,7 @@
},
{
"name": "NEW_ATTESTATION_ENABLED",
"version": "5.23.0"
"version": "undefined"
},
{
"name": "TWIN_REFACTORING_ENABLED",

View file

@ -23,9 +23,10 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
} else {
sendHttpError(code, analyticsErrorHandler)
val errorBody = errorBody()?.string().orEmpty() // !!!Beware!!! string() closes stream after invocation
sendHttpError(code, analyticsErrorHandler, errorBody)
ApiResponseError.HttpException(code, message(), errorBody()?.string())
ApiResponseError.HttpException(code, message(), errorBody)
}
} catch (e: Exception) {
Timber.e(e, "UnknownException occured")
@ -39,6 +40,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
private fun <T : Any> Response<T>.sendHttpError(
code: ApiResponseError.HttpException.Code,
analyticsErrorHandler: AnalyticsErrorHandler,
errorBody: String,
) {
val fullRequestUrl = raw().request.url.toUrl()
val shortUrl = fullRequestUrl.authority + fullRequestUrl.path
@ -46,7 +48,7 @@ private fun <T : Any> Response<T>.sendHttpError(
ApiErrorEvent(
endpoint = shortUrl,
code = code.numericCode,
message = errorBody()?.string().orEmpty(),
message = errorBody,
),
)
}

View file

@ -22,6 +22,7 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
internal class ManagedCryptoCurrencyFactory(
private val excludedBlockchains: ExcludedBlockchains,
@ -187,11 +188,18 @@ internal class ManagedCryptoCurrencyFactory(
decimals = blockchain.decimals(),
isL2Network = l2BlockchainsList.contains(blockchain),
)
network.canHandleTokens -> SourceNetwork.Default(
network = network,
decimals = decimals ?: return null,
contractAddress = contractAddress,
)
network.canHandleTokens -> {
val formattedContractAddress = blockchain.reformatContractAddress(contractAddress)
if (formattedContractAddress == null) {
Timber.w("Couldn't reformat $contractAddress")
return null
}
SourceNetwork.Default(
network = network,
decimals = decimals ?: return null,
contractAddress = formattedContractAddress,
)
}
else -> null
}
}

View file

@ -72,7 +72,7 @@ internal class TokenMarketInfoConverter(
TokenMarketInfo.Network(
networkId = network.networkId,
exchangeable = network.exchangeable,
contractAddress = network.contractAddress,
contractAddress = blockchain.reformatContractAddress(network.contractAddress),
decimalCount = network.decimalCount,
)
}

View file

@ -63,11 +63,17 @@ internal class DefaultOnrampTransactionRepository(
}.map(transactionConverter::convert)
}
override suspend fun updateTransactionStatus(externalTxId: String, status: OnrampStatus.Status) =
withContext(dispatchers.io) {
val updatedTx = getTransactionById(externalTxId)?.copy(status = status) ?: return@withContext
storeTransaction(updatedTx)
}
override suspend fun updateTransactionStatus(
externalTxId: String,
externalTxUrl: String,
status: OnrampStatus.Status,
) = withContext(dispatchers.io) {
val updatedTx = getTransactionById(externalTxId)?.copy(
externalTxUrl = externalTxUrl,
status = status,
) ?: return@withContext
storeTransaction(updatedTx)
}
override suspend fun removeTransaction(externalTxId: String) {
withContext(dispatchers.io) {

View file

@ -10,7 +10,12 @@ class OnrampUpdateTransactionStatusUseCase(
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(externalTxId: String, status: OnrampStatus.Status) = Either.catch {
onrampTransactionRepository.updateTransactionStatus(externalTxId = externalTxId, status = status)
}.mapLeft(errorResolver::resolve)
suspend operator fun invoke(externalTxId: String, externalTxUrl: String, status: OnrampStatus.Status) =
Either.catch {
onrampTransactionRepository.updateTransactionStatus(
externalTxId = externalTxId,
externalTxUrl = externalTxUrl,
status = status,
)
}.mapLeft(errorResolver::resolve)
}

View file

@ -14,7 +14,7 @@ interface OnrampTransactionRepository {
fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<OnrampTransaction>>
suspend fun updateTransactionStatus(externalTxId: String, status: OnrampStatus.Status)
suspend fun updateTransactionStatus(externalTxId: String, externalTxUrl: String, status: OnrampStatus.Status)
suspend fun removeTransaction(externalTxId: String)
}

View file

@ -201,7 +201,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
params = SendConfirmComponent.Params(
state = model.uiState.value,
userWallet = model.userWallet,
currentRoute = currentRoute.filterIsInstance<SendRoute.Confirm>(),
currentRoute = currentRoute,
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,

View file

@ -140,7 +140,7 @@ internal class SendConfirmComponent(
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Confirm>,
val currentRoute: Flow<SendRoute>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
) {

View file

@ -490,7 +490,7 @@ internal class SendConfirmModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
).filter { it.second is SendRoute.Confirm }.onEach { (state, _) ->
val amountUM = state.amountUM as? AmountState.Data
val confirmUM = state.confirmUM
params.callback.onResult(

View file

@ -31,11 +31,12 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
@ -74,6 +75,7 @@ internal class SendModel @Inject constructor(
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger,
) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require()
@ -110,6 +112,7 @@ internal class SendModel @Inject constructor(
}
override fun onAmountResult(amountUM: AmountState) {
predefinedAmountValue = null // reset predefined amount
_uiState.update { it.copy(amountUM = amountUM) }
}
@ -118,6 +121,7 @@ internal class SendModel @Inject constructor(
}
override fun onResult(sendUM: SendUM) {
predefinedAmountValue = null // reset predefined amount
_uiState.update { sendUM }
}
@ -227,7 +231,13 @@ internal class SendModel @Inject constructor(
private fun onQrCodeScanned(address: String) {
val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull()
// Decompose component can be active or inactive depending on its state and navigation stack
// If it is in inactive state use parameter to pass value to amount component
predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals)
// If it is in active state use flow to update value in amount component
modelScope.launch {
predefinedAmountValue?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) }
}
}
private fun onFailedTxEmailClick(errorMessage: String? = null) {

View file

@ -25,14 +25,33 @@ interface SendAmountReduceListener {
val ignoreReduceTriggerFlow: Flow<Unit>
}
/**
* Trigger amount change from another component.
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateQRTrigger {
suspend fun triggerUpdateAmount(amountValue: String)
}
/**
* Trigger amount change from another component.
* Different from another triggers because it takes raw string instead of BigDecimal
*/
interface SendAmountUpdateQRListener {
val updateAmountTriggerFlow: Flow<String>
}
@Singleton
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
SendAmountReduceTrigger,
SendAmountReduceListener {
SendAmountReduceListener,
SendAmountUpdateQRTrigger,
SendAmountUpdateQRListener {
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
override val ignoreReduceTriggerFlow = MutableSharedFlow<Unit>()
override val updateAmountTriggerFlow = MutableSharedFlow<String>()
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
reduceByTriggerFlow.emit(reduceBy)
@ -45,4 +64,8 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() :
override suspend fun triggerIgnoreReduce() {
ignoreReduceTriggerFlow.emit(Unit)
}
override suspend fun triggerUpdateAmount(amountValue: String) {
updateAmountTriggerFlow.emit(amountValue)
}
}

View file

@ -1,8 +1,7 @@
package com.tangem.features.send.v2.subcomponents.amount.di
import com.tangem.features.send.v2.subcomponents.amount.*
import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -20,4 +19,12 @@ internal interface SendAmountModule {
@Singleton
@Binds
fun provideSendAmountReduceListener(impl: DefaultSendAmountReduceTrigger): SendAmountReduceListener
@Singleton
@Binds
fun provideSendAmountUpdateQRListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRListener
@Singleton
@Binds
fun provideSendAmountUpdateQRTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRTrigger
}

View file

@ -27,6 +27,7 @@ import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenS
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRListener
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
@ -50,6 +51,7 @@ internal class SendAmountModel @Inject constructor(
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val sendAmountReduceListener: SendAmountReduceListener,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val sendAmountUpdateQRListener: SendAmountUpdateQRListener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), AmountScreenClickIntents {
@ -71,6 +73,7 @@ internal class SendAmountModel @Inject constructor(
subscribeOnAmountReduceByTriggerUpdates()
subscribeOnAmountReduceToTriggerUpdates()
subscribeOnAmountIgnoreReduceTriggerUpdates()
subscribeOnAmountUpdateQRTriggerUpdates()
}
private fun initMinBoundary() {
@ -105,8 +108,8 @@ internal class SendAmountModel @Inject constructor(
),
)
}
params.predefinedAmountValue?.let(::onAmountValueChange)
}
params.predefinedAmountValue?.let(::onAmountValueChange)
}
fun updateState(amountUM: AmountState) {
@ -215,6 +218,14 @@ internal class SendAmountModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnAmountUpdateQRTriggerUpdates() {
sendAmountUpdateQRListener.updateAmountTriggerFlow
.onEach { amount ->
onAmountValueChange(amount)
saveResult()
}.launchIn(modelScope)
}
private fun saveResult() {
val params = params as? SendAmountComponentParams.AmountParams ?: return
params.callback.onAmountResult(uiState.value)

View file

@ -15,9 +15,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -116,7 +116,11 @@ internal class OnrampStatusFactory @AssistedInject constructor(
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status)
onrampUpdateTransactionStatusUseCase(
externalTxId = externalTxId,
externalTxUrl = statusModel.externalTxUrl.orEmpty(),
status = statusModel.status,
)
}
}

View file

@ -72,7 +72,11 @@ internal class OnrampStatusFactory @Inject constructor(
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status)
onrampUpdateTransactionStatusUseCase(
externalTxId = externalTxId,
externalTxUrl = statusModel.externalTxUrl.orEmpty(),
status = statusModel.status,
)
}
},
)

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "releases-5.23-1032"
tangemBlockchainSdk = "releases-5.23-1037"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "releases-5.23-456"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^