Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-13 19:58:22 +03:00
parent 6018cf0b71
commit f7b10f3ed5
31 changed files with 446 additions and 105 deletions

1
.gitignore vendored
View file

@ -53,3 +53,4 @@ app/src/external/google-services.json
# Kotlin Plugin
.kotlin/
find-latest-release-branch.output

View file

@ -275,14 +275,6 @@ sealed class NotificationUM(val config: NotificationConfig) {
title = resourceReference(id = R.string.yield_module_balance_info_sheet_title, wrappedList(tokenName)),
subtitle = resourceReference(id = R.string.yield_module_balance_info_sheet_subtitle),
)
data class YieldSupplyNotAllAmountSupplied(val formattedAmount: String, val symbol: String) : Warning(
title = resourceReference(
id = R.string.yield_module_amount_not_transfered_to_aave_title,
formatArgs = wrappedList(formattedAmount, symbol),
),
subtitle = TextReference.EMPTY,
)
}
open class Info(
@ -301,7 +293,15 @@ sealed class NotificationUM(val config: NotificationConfig) {
onCloseClick = onCloseClick,
iconTint = iconTint,
),
)
) {
data class YieldSupplyNotAllAmountSupplied(val formattedAmount: String, val symbol: String) : Info(
title = resourceReference(
id = R.string.yield_module_amount_not_transfered_to_aave_title,
formatArgs = wrappedList(formattedAmount, symbol),
),
subtitle = TextReference.EMPTY,
)
}
sealed interface Cardano {

View file

@ -269,7 +269,7 @@
<string name="common_free">無料</string>
<string name="common_from">送信元</string>
<string name="common_generate_addresses">アドレスを同期する</string>
<string name="common_get_started">はじめる</string>
<string name="common_get_started">もっと詳しく</string>
<string name="common_get_token">トークンを取得</string>
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
@ -68,9 +69,18 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.padding(horizontal = 4.dp)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
.clip(TangemTheme.shapes.roundedCorners8)
.background(color = backgroundColor)
.then(
if (state.onClick != null) {
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = state.onClick,
)
} else {
Modifier
},
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
@ -86,11 +96,13 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
imageVector = ImageVector.vectorResource(wrappedIcon),
tint = iconColor,
contentDescription = null,
modifier = Modifier.size(16.dp).clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = { state.onIconClick?.invoke() },
),
modifier = Modifier
.size(16.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = { state.onIconClick?.invoke() },
),
)
}
}

View file

@ -8,6 +8,7 @@ data class LabelUM(
val style: LabelStyle,
@DrawableRes val icon: Int? = null,
val onIconClick: (() -> Unit)? = null,
val onClick: (() -> Unit)? = null,
)
enum class LabelStyle {

View file

@ -1,10 +1,13 @@
package com.tangem.data.walletmanager.utils
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData
import com.tangem.blockchain.common.*
import com.tangem.blockchain.yieldsupply.providers.ethereum.factory.EthereumYieldSupplyDeployCallData
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyInitTokenCallData
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyReactivateTokenCallData
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address
import com.tangem.domain.models.network.TxInfo
import com.tangem.utils.converter.Converter
@ -37,25 +40,12 @@ internal class TransactionDataToTxHistoryItemConverter(
addressType = TxInfo.AddressType.User(value.destinationAddress),
),
sourceType = TxInfo.SourceType.Single(value.sourceAddress),
interactionAddressType = TxInfo.InteractionAddressType.User(
address = if (isOutgoing) value.destinationAddress else value.sourceAddress,
),
interactionAddressType = getInteractionAddressType(value, isOutgoing),
status = when (value.status) {
TransactionStatus.Confirmed -> TxInfo.TransactionStatus.Confirmed
TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed
},
type = when (val extras = value.extras) {
is EthereumTransactionExtras -> {
when (extras.callData) {
is EthereumYieldSupplyDeployCallData,
is EthereumYieldSupplyEnterCallData,
-> TxInfo.TransactionType.YieldSupply.Enter
is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit
else -> TxInfo.TransactionType.Transfer
}
}
else -> TxInfo.TransactionType.Transfer
},
type = getTransactionType(value.extras),
amount = amount,
)
}
@ -89,4 +79,41 @@ internal class TransactionDataToTxHistoryItemConverter(
return amountToken.contractAddress.equals(feeToken.contractAddress, ignoreCase = true) &&
amountToken.symbol.equals(feeToken.symbol, ignoreCase = true)
}
private fun getInteractionAddressType(
value: TransactionData.Uncompiled,
isOutgoing: Boolean,
): TxInfo.InteractionAddressType = when (val extras = value.extras) {
is EthereumTransactionExtras -> {
when (val callData = extras.callData) {
is ApprovalERC20TokenCallData -> TxInfo.InteractionAddressType.Contract(
address = callData.spenderAddress,
)
else -> TxInfo.InteractionAddressType.User(
address = if (isOutgoing) value.destinationAddress else value.sourceAddress,
)
}
}
else -> TxInfo.InteractionAddressType.User(
address = if (isOutgoing) value.destinationAddress else value.sourceAddress,
)
}
private fun getTransactionType(extras: TransactionExtras?): TxInfo.TransactionType {
return when (extras) {
is EthereumTransactionExtras -> {
when (extras.callData) {
is EthereumYieldSupplyDeployCallData,
is EthereumYieldSupplyReactivateTokenCallData,
is EthereumYieldSupplyInitTokenCallData,
is EthereumYieldSupplyEnterCallData,
-> TxInfo.TransactionType.YieldSupply.Enter
is EthereumYieldSupplyExitCallData -> TxInfo.TransactionType.YieldSupply.Exit
is ApprovalERC20TokenCallData -> TxInfo.TransactionType.Approve
else -> TxInfo.TransactionType.Transfer
}
}
else -> TxInfo.TransactionType.Transfer
}
}
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.walletManager)
implementation(projects.domain.legacy)
implementation(projects.domain.txhistory.models)
implementation(projects.libs.blockchainSdk)

View file

@ -12,6 +12,8 @@ import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository
@ -22,6 +24,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
internal class DefaultYieldSupplyRepository(
@ -109,16 +112,21 @@ internal class DefaultYieldSupplyRepository(
override suspend fun saveTokenProtocolStatus(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus,
yieldSupplyEnterStatus: YieldSupplyEnterStatus?,
) {
statusMap["${userWalletId}_${cryptoCurrency.id.value}"] = yieldSupplyEnterStatus
val key = getTokenProtocolStatusKey(userWalletId, cryptoCurrency)
if (yieldSupplyEnterStatus != null) {
statusMap[key] = yieldSupplyEnterStatus
} else {
statusMap.remove(key)
}
}
override fun getTokenProtocolStatus(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldSupplyEnterStatus? {
return statusMap["${userWalletId}_${cryptoCurrency.id.value}"]
return statusMap[getTokenProtocolStatusKey(userWalletId, cryptoCurrency)]
}
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {
@ -127,4 +135,44 @@ internal class DefaultYieldSupplyRepository(
token.copy(backendId = chainIdMap[token.chainId])
}
}
override suspend fun getTokenPendingStatus(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): YieldSupplyEnterStatus? = try {
val cryptoCurrency = cryptoCurrencyStatus.currency
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found")
val pendingTxs = cryptoCurrencyStatus.value.pendingTransactions
val yieldAddress = walletManager.calculateYieldModuleAddress()
val hasRecentYieldEnterTxs = pendingTxs.hasYieldEnterTransactions(yieldAddress)
val hasRecentYieldExitTxs = pendingTxs.hasYieldExitTransactions()
when {
hasRecentYieldEnterTxs -> YieldSupplyEnterStatus.Enter
hasRecentYieldExitTxs -> YieldSupplyEnterStatus.Exit
else -> null
}
} catch (e: Exception) {
Timber.e(e, "Failed to get pending yield supply status")
null
}
private fun Set<TxInfo>.hasYieldEnterTransactions(yieldAddress: String) = any {
it.type == TxInfo.TransactionType.YieldSupply.Enter ||
it.type == TxInfo.TransactionType.Approve &&
(it.interactionAddressType as? TxInfo.InteractionAddressType.Contract)?.address == yieldAddress
}
private fun Set<TxInfo>.hasYieldExitTransactions() = any {
it.type == TxInfo.TransactionType.YieldSupply.Exit
}
private fun getTokenProtocolStatusKey(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String =
"${userWalletId}_${cryptoCurrency.id.value}"
}

View file

@ -160,10 +160,20 @@ sealed class StakingAnalyticsEvent(
),
)
data class UnitializedAddress(val token: String) : StakingAnalyticsEvent(
data class UninitializedAddress(val token: String) : StakingAnalyticsEvent(
event = "Notice - Uninitialized Address",
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
)
data class UninitializedAddressScreen(val token: String) : StakingAnalyticsEvent(
event = "Uninitialized Address Screen",
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
)
data class ButtonActivate(val token: String) : StakingAnalyticsEvent(
event = "Button - Activate",
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
)
}
enum class StakeScreenSource {

View file

@ -1,6 +1,7 @@
package com.tangem.domain.yield.supply
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.domain.yield.supply.models.YieldSupplyEnterStatus
@ -76,7 +77,7 @@ interface YieldSupplyRepository {
suspend fun saveTokenProtocolStatus(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus,
yieldSupplyEnterStatus: YieldSupplyEnterStatus?,
)
/**
@ -91,4 +92,20 @@ interface YieldSupplyRepository {
* @return the last action intent or null if nothing has been recorded
*/
fun getTokenProtocolStatus(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus?
/**
* Get the pending status of the yield protocol action for the given wallet and currency, if any.
*
* Used to determine whether the UI should display an intermediate "processing" state
* until the protocol status retrieved from backend reflects the change. The value is
* transient (inmemory only) and is not persisted across app restarts.
*
* @param userWalletId the wallet to query
* @param cryptoCurrencyStatus the currency status to query
* @return the pending action intent or null if nothing has been recorded
*/
suspend fun getTokenPendingStatus(
userWalletId: UserWalletId,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): YieldSupplyEnterStatus?
}

View file

@ -9,6 +9,7 @@ import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import java.math.BigDecimal
// TODO split this interface to click intents and other interaction events
@Suppress("TooManyFunctions")
internal interface StakingClickIntents : AmountScreenClickIntents {
@ -72,5 +73,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onActivateTonAccountNotificationClick()
fun onActivateTonAccountNotificationShow()
fun onActivateTonAccountClick()
}

View file

@ -953,6 +953,11 @@ internal class StakingModel @Inject constructor(
}
override fun onActivateTonAccountNotificationClick() {
analyticsEventHandler.send(
StakingAnalyticsEvent.UninitializedAddressScreen(
token = cryptoCurrencyStatus.currency.symbol,
),
)
modelScope.launch {
stateController.update(
ShowTonInitializeBottomSheetTransformer(
@ -1006,7 +1011,20 @@ internal class StakingModel @Inject constructor(
}
}
override fun onActivateTonAccountNotificationShow() {
analyticsEventHandler.send(
StakingAnalyticsEvent.UninitializedAddress(
token = cryptoCurrencyStatus.currency.symbol,
),
)
}
override fun onActivateTonAccountClick() {
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonActivate(
token = cryptoCurrencyStatus.currency.symbol,
),
)
modelScope.launch {
tonAccountInitializeTransaction?.let { transaction ->
sendTransactionUseCase.invoke(

View file

@ -81,5 +81,7 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onActivateTonAccountNotificationClick() {}
override fun onActivateTonAccountNotificationShow() {}
override fun onActivateTonAccountClick() {}
}

View file

@ -295,6 +295,7 @@ internal class AddStakingNotificationsTransformer(
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (isTon(cryptoCurrencyNetworkIdValue) && !isAccountInitialized) {
prevState.clickIntents.onActivateTonAccountNotificationShow()
add(
StakingNotification.Warning.InitializeTonAccount(
onInitializeClick = prevState.clickIntents::onActivateTonAccountNotificationClick,

View file

@ -189,6 +189,6 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
}
interface ModelCallback {
fun onResult(sendWithSwapUM: SendWithSwapUM)
fun onResult(route: SendWithSwapRoute, sendWithSwapUM: SendWithSwapUM)
}
}

View file

@ -425,7 +425,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
val confirmUM = state.confirmUM
val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isTransactionInProcess
params.callback.onResult(
state.copy(
route = SendWithSwapRoute.Confirm,
sendWithSwapUM = state.copy(
navigationUM = NavigationUM.Content(
title = resourceReference(id = R.string.send_with_swap_confirm_title),
subtitle = null,

View file

@ -116,8 +116,10 @@ internal class SendWithSwapModel @Inject constructor(
uiState.update { it.copy(destinationUM = destinationUM) }
}
override fun onResult(sendWithSwapUM: SendWithSwapUM) {
uiState.value = sendWithSwapUM
override fun onResult(route: SendWithSwapRoute, sendWithSwapUM: SendWithSwapUM) {
if (currentRoute.value == route) {
uiState.value = sendWithSwapUM
}
}
override fun onNavigationResult(navigationUM: NavigationUM) {

View file

@ -36,7 +36,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
@ -62,7 +61,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val notificationsRepository: NotificationsRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountDependencies: AccountDependencies,
) {
@ -134,7 +132,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
!notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
)
addYieldSupplyNotifications(flattenCurrencies)
// Remove in first iteration of yield supply feature
// addYieldSupplyNotifications(flattenCurrencies)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
@ -319,14 +318,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
) {
addIf(
element = WalletNotification.Warning.YeildSupplyApprove,
condition = flattenCurrencies.hasTokensWithActivatedSupplyWithoutApprove(),
)
}
// private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
// flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
// ) {
// addIf(
// element = WalletNotification.Warning.YeildSupplyApprove,
// condition = flattenCurrencies.hasTokensWithActivatedSupplyWithoutApprove(),
// )
// }
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?,
@ -371,13 +370,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable }
}
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasTokensWithActivatedSupplyWithoutApprove(): Boolean {
val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false
val yieldSupplyEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled
return yieldSupplyEnabled && flattenCurrencies.any {
it.value.yieldSupplyStatus?.isAllowedToSpend == false
}
}
// Remove in first iteration of yield supply feature
// private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.hasTokensWithActivatedSupplyWithoutApprove(): Boolean {
// val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false
// val yieldSupplyEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled
// return yieldSupplyEnabled && flattenCurrencies.any {
// it.value.yieldSupplyStatus?.isAllowedToSpend == false
// }
// }
private fun MutableList<WalletNotification>.addRateTheAppNotification(
isReadyToShowRating: Boolean,

View file

@ -12,10 +12,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
@ -114,7 +116,10 @@ internal fun YieldSupplyApyContent(
ApyChart(
isChartLoading = isLoading,
chartComponent = chartComponent,
modifier = Modifier.padding(horizontal = 12.dp),
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.background(TangemTheme.colors.background.action)
.padding(12.dp),
)
SpacerH16()

View file

@ -26,6 +26,7 @@ internal sealed class YieldSupplyUM {
val rewardsApy: TextReference,
val onClick: () -> Unit,
val showWarningIcon: Boolean,
val showInfoIcon: Boolean,
) : YieldSupplyUM()
@Immutable

View file

@ -1,5 +1,6 @@
package com.tangem.features.yield.supply.impl.main.model
import android.os.SystemClock
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute.YieldSupplyPromo
@ -35,9 +36,9 @@ import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.transformer.update
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@ -46,7 +47,7 @@ import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class YieldSupplyModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -72,6 +73,7 @@ internal class YieldSupplyModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<Unit> = SlotNavigation()
private val handleNavigation = params.handleNavigation
private val cryptoCurrency = params.cryptoCurrency
var userWallet: UserWallet by Delegates.notNull()
@ -89,10 +91,17 @@ internal class YieldSupplyModel @Inject constructor(
private var lastYieldSupplyStatus: YieldSupplyStatus? = null
private val fetchCurrencyJobHolder = JobHolder()
private var lastStatusCheckTimestamp = 0L
init {
checkIfYieldSupplyIsAvailable()
params.handleNavigation?.let { handle ->
if (handle) {
val protocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWalletId = params.userWalletId,
cryptoCurrency = cryptoCurrency,
)
if (handleNavigation != null && protocolStatus == null) {
if (handleNavigation) {
modelScope.launch {
delay(timeMillis = 1000)
bottomSheetNavigation.activate(Unit)
@ -193,48 +202,88 @@ internal class YieldSupplyModel @Inject constructor(
}
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
)
val tokenPendingStatus = yieldSupplyRepository.getTokenPendingStatus(
userWallet.walletId,
cryptoCurrencyStatus,
)
val isActive = yieldSupplyStatus?.isActive == true
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
val processing = uiState.value is YieldSupplyUM.Processing
Timber.d(
"currentUiState ${uiState.value.javaClass} \n" +
"yieldSupplyStatus $yieldSupplyStatus" +
"YIELD " +
"yieldSupplyStatus $yieldSupplyStatus " +
"tokenProtocolStatus $tokenProtocolStatus " +
"tokenPendingStatus $tokenPendingStatus " +
"isActive $isActive " +
"processing $processing " +
"isCryptoCurrencyStatusFromCache $isCryptoCurrencyStatusFromCache",
)
if (isCryptoCurrencyStatusFromCache && processing) {
return
return@launch
}
when {
!isActive && tokenProtocolStatus == YieldSupplyEnterStatus.Enter -> {
uiState.update { YieldSupplyUM.Processing.Enter }
fetchCurrencyWithDelay()
tokenProtocolStatus != null && tokenPendingStatus != null -> {
showProcessing(tokenPendingStatus)
lastStatusCheckTimestamp = 0L
}
isActive && tokenProtocolStatus == YieldSupplyEnterStatus.Exit -> {
uiState.update { YieldSupplyUM.Processing.Exit }
fetchCurrencyWithDelay()
tokenProtocolStatus == YieldSupplyEnterStatus.Exit && isActive ||
tokenProtocolStatus == YieldSupplyEnterStatus.Enter && !isActive -> {
if (lastStatusCheckTimestamp != 0L) {
if (SystemClock.elapsedRealtime() - lastStatusCheckTimestamp > MAX_STATUS_CHECK_LIMIT) {
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
} else {
showProcessing(tokenProtocolStatus)
}
} else {
showProcessing(tokenProtocolStatus)
lastStatusCheckTimestamp = SystemClock.elapsedRealtime()
}
}
else -> {
loadStatus(cryptoCurrencyStatus)
lastStatusCheckTimestamp = 0L
}
}
}
private fun showProcessing(status: YieldSupplyEnterStatus) {
uiState.update {
when (status) {
YieldSupplyEnterStatus.Enter -> YieldSupplyUM.Processing.Enter
YieldSupplyEnterStatus.Exit -> YieldSupplyUM.Processing.Exit
}
}
fetchCurrencyWithDelay()
}
private fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
modelScope
.launch {
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyEnterStatus = null,
)
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
if (isActive) {
if (yieldSupplyStatus?.isActive == true) {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yieldSupplyStatus = requireNotNull(yieldSupplyStatus),
yieldSupplyStatus = yieldSupplyStatus,
)
} else {
loadTokenStatus()
}
}
}
}
private fun fetchCurrencyWithDelay() {
@ -253,8 +302,8 @@ internal class YieldSupplyModel @Inject constructor(
private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend ||
cryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied()
val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend
val showInfoIcon = cryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied()
if (!yieldSupplyStatus.isAllowedToSpend) {
analyticsEventsHandler.send(
YieldSupplyAnalytics.NoticeApproveNeeded(
@ -282,6 +331,7 @@ internal class YieldSupplyModel @Inject constructor(
),
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = showInfoIcon,
apy = tokenStatus.apy.toString(),
)
}
@ -298,6 +348,7 @@ internal class YieldSupplyModel @Inject constructor(
rewardsApy = TextReference.EMPTY,
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = showInfoIcon,
apy = "",
)
}
@ -328,5 +379,6 @@ internal class YieldSupplyModel @Inject constructor(
private companion object {
const val PROCESSING_UPDATE_DELAY = 10_000L
const val MAX_STATUS_CHECK_LIMIT = 10_000L
}
}

View file

@ -149,12 +149,21 @@ private fun SupplyContent(supplyUM: YieldSupplyUM.Content, modifier: Modifier =
)
}
SpacerW8()
AnimatedVisibility(supplyUM.showWarningIcon) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = TangemTheme.colors.icon.attention,
)
AnimatedContent(
targetState = supplyUM,
) { currentState ->
when {
currentState.showWarningIcon -> Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = TangemTheme.colors.icon.attention,
)
currentState.showInfoIcon -> Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_alert_circle_red_20),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
}
}
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
@ -355,6 +364,7 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
onClick = {},
apy = "5.1",
showWarningIcon = false,
showInfoIcon = true,
),
YieldSupplyUM.Content(
title = stringReference("Aave lending is active "),
@ -363,6 +373,7 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
onClick = {},
apy = "5.1",
showWarningIcon = true,
showInfoIcon = false,
),
YieldSupplyUM.Loading,
YieldSupplyUM.Processing.Enter,

View file

@ -4,13 +4,16 @@ import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -57,7 +60,7 @@ internal fun YieldSupplyPromoContent(
onClick = clickIntents::onUrlClick,
)
PrimaryButton(
text = stringResourceSafe(R.string.yield_module_start_earning),
text = stringResourceSafe(R.string.common_continue),
onClick = clickIntents::onStartEarningClick,
modifier = Modifier
.padding(
@ -106,7 +109,7 @@ private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickInt
text = yieldSupplyPromoUM.subtitle,
style = LabelStyle.REGULAR,
icon = R.drawable.ic_information_24,
onIconClick = clickIntents::onApyInfoClick,
onClick = clickIntents::onApyInfoClick,
),
)
SpacerH32()
@ -131,7 +134,11 @@ private fun YieldStatusAppBar(onBackClick: () -> Unit, onHowItWorksClick: () ->
imageVector = ImageVector.vectorResource(R.drawable.ic_back_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier.clickable(onClick = onBackClick),
modifier = Modifier.clickable(
onClick = onBackClick,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
),
)
SpacerWMax()
Text(

View file

@ -195,7 +195,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
blockchain = cryptoCurrency.network.name,
),
)
NotificationUM.Warning.YieldSupplyNotAllAmountSupplied(
NotificationUM.Info.YieldSupplyNotAllAmountSupplied(
formattedAmount = formattedAmount,
symbol = cryptoCurrency.symbol,
)

View file

@ -26,7 +26,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.decompose.ComposableContentComponent
@ -61,7 +64,7 @@ internal fun YieldSupplyActiveContent(
.padding(12.dp),
) {
CurrentApy(state.apy)
chartComponent.Content(Modifier.padding(bottom = 12.dp))
chartComponent.Content(Modifier)
}
AnimatedVisibility(state.notifications.isNotEmpty()) {
@ -71,6 +74,11 @@ internal fun YieldSupplyActiveContent(
Notification(
config = notificationUM.config,
containerColor = TangemTheme.colors.background.action,
iconTint = if (notificationUM is NotificationUM.Info) {
TangemTheme.colors.icon.accent
} else {
null
},
)
}
}
@ -104,7 +112,7 @@ internal fun YieldSupplyActiveContent(
@Composable
private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(vertical = 12.dp)) {
Column(modifier = modifier) {
Text(
modifier = Modifier,
text = stringResourceSafe(R.string.yield_module_earn_sheet_current_apy_title),
@ -156,7 +164,11 @@ private fun YieldSupplyActiveMyFunds(
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action)
.fillMaxWidth()
.padding(12.dp),
.padding(
top = 12.dp,
start = 12.dp,
end = 12.dp,
),
) {
Text(
text = stringResourceSafe(R.string.yield_module_earn_sheet_my_funds_title),

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -91,7 +92,7 @@ internal fun YieldSupplyFeePolicyContent(
YieldSupplyFeeUM.Loading -> null
}
YieldSupplyFeeRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_current_fee_title),
title = resourceReference(R.string.common_estimated_fee),
value = currentFee,
modifier = Modifier.padding(vertical = 12.dp),
)
@ -100,6 +101,10 @@ internal fun YieldSupplyFeePolicyContent(
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null
}
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
YieldSupplyFeeRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_max_fee_title),
value = maxFee,

View file

@ -144,7 +144,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
val transactionListData = yieldSupplyStartEarningUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = maxFee.nativeMaxFee,
maxNetworkFee = maxFee.tokenMaxFee,
).getOrNull()
if (transactionListData == null) {

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1301"
tangemBlockchainSdk = "develop-1306"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-564"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -4,6 +4,7 @@ import com.android.build.gradle.AppExtension
import com.tangem.plugin.configuration.model.AppConfig
import com.tangem.plugin.configuration.model.BuildType
import com.tangem.plugin.configuration.utils.BuildConfigFieldFactory
import com.tangem.plugin.configuration.utils.VersionNameProvider
import org.gradle.api.Project
import com.android.build.gradle.internal.dsl.BuildType as AndroidBuildType
@ -29,11 +30,11 @@ private fun AppExtension.configureDefaultConfig(project: Project) {
AppConfig.versionCode
}
versionName = if (project.hasProperty("versionName")) {
project.property("versionName") as String
} else {
AppConfig.versionName
}
// Get version name from property, git branch, or default config
val versionNameProvider = VersionNameProvider(project)
versionName = versionNameProvider.getVersionName()
project.logger.lifecycle("Resolved versionName: $versionName")
buildFeatures.buildConfig = true

View file

@ -3,8 +3,6 @@ package com.tangem.plugin.configuration.model
internal object AppConfig {
const val packageName = "com.tangem.wallet"
const val versionCode = 1
const val versionName = "1.0.0-SNAPSHOT"
// const val versionName = "100.0.0-SNAPSHOT" //TODO: [REDACTED_JIRA]
const val minSdkVersion = 24
const val targetSdkVersion = 35
const val compileSdkVersion = 35

View file

@ -0,0 +1,105 @@
package com.tangem.plugin.configuration.utils
import org.gradle.api.Project
import org.gradle.api.provider.Provider
/**
* Provides version name based on current git branch
*/
internal class VersionNameProvider(
private val project: Project,
) {
/**
* Get version name for current branch.
* If -PversionName is provided, uses that value.
* Otherwise, derives version from git branch name.
*/
fun getVersionName(): String {
// Check if versionName is provided as gradle property
if (project.hasProperty("versionName")) {
return project.property("versionName") as String
}
// Get current branch name using Provider API (configuration cache compatible)
val currentBranch = getCurrentBranchProvider().get()
// Try to extract version from branch name (releases/X.Y)
val versionFromBranch = extractVersionFromBranch(currentBranch)
if (versionFromBranch != null) {
return versionFromBranch
}
// For other branches (develop, feature/*), find latest release branch and increment minor
val latestReleaseBranch = findLatestReleaseBranch()
if (latestReleaseBranch != null) {
val versionFromLatest = extractVersionFromBranch(latestReleaseBranch)
if (versionFromLatest != null) {
return incrementMinorVersion(versionFromLatest)
}
}
// Fallback to default if nothing works
return "1.0.0-SNAPSHOT"
}
private fun getCurrentBranchProvider(): Provider<String> {
return project.providers.exec {
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
}.standardOutput.asText.map { it.trim() }
}
private fun findLatestReleaseBranch(): String? {
val scriptPath = project.rootProject.file("tangem-android-tools/CI/shell_scripts/find-latest-release-branch.sh")
if (!scriptPath.exists()) {
project.logger.warn("Script not found: $scriptPath")
return null
}
val currentBranch = getCurrentBranchProvider().get()
val outputFile = project.rootProject.file("find-latest-release-branch.output")
return try {
project.providers.exec {
commandLine("sh", scriptPath.absolutePath, currentBranch)
}.standardOutput.asText.get()
if (!outputFile.exists()) {
project.logger.warn("Script output file not found")
return null
}
outputFile.readText().trim().also { result ->
project.logger.lifecycle("Found latest release branch: $result")
outputFile.delete()
}
} catch (e: Exception) {
project.logger.warn("Failed to execute script: ${e.message}")
outputFile.delete()
null
}
}
private fun extractVersionFromBranch(branch: String): String? {
val regex = Regex("""^releases/(\d+)\.(\d+)(?:\.(\d+))?$""")
val matchResult = regex.find(branch) ?: return null
val major = matchResult.groupValues[1]
val minor = matchResult.groupValues[2]
val patch = matchResult.groupValues[3].ifEmpty { "0" }
return "$major.$minor.$patch"
}
private fun incrementMinorVersion(version: String): String {
val parts = version.split(".")
if (parts.size < 2) return version
val major = parts[0]
val minor = parts[1].toIntOrNull() ?: return version
val newMinor = minor + 1
return "$major.$newMinor.0"
}
}