Updated on 2026-08-14
This commit is contained in:
parent
330ae73357
commit
e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions
1
features/staking/api/.gitignore
vendored
Normal file
1
features/staking/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
24
features/staking/api/build.gradle.kts
Normal file
24
features/staking/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.staking.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain models */
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.staking.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface StakingComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrencyId: CryptoCurrency.ID,
|
||||
val yieldId: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, StakingComponent>
|
||||
}
|
||||
1
features/staking/impl/.gitignore
vendored
Normal file
1
features/staking/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
82
features/staking/impl/build.gradle.kts
Normal file
82
features/staking/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.staking.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.feedback)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.staking.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingModel
|
||||
import com.tangem.features.staking.impl.presentation.ui.StakingScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultStakingComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: StakingComponent.Params,
|
||||
) : StakingComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: StakingModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val currentState by model.uiState.collectAsStateWithLifecycle()
|
||||
StakingScreen(currentState)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : StakingComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: StakingComponent.Params): DefaultStakingComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.staking.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.ParamsInterceptor
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
|
||||
class StakingParamsInterceptor(private val tokenSymbol: String) : ParamsInterceptor {
|
||||
|
||||
override fun id() = ID
|
||||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
|
||||
return event is StakingAnalyticsEvent
|
||||
}
|
||||
|
||||
override fun intercept(params: MutableMap<String, String>) {
|
||||
params[AnalyticsParam.TOKEN_PARAM] = tokenSymbol
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = "StakingParamsInterceptorId"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.tangem.features.staking.impl.analytics.utils
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.staking.analytics.StakeScreenSource
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
|
||||
internal class StakingAnalyticSender(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
fun initialInfoScreen(value: StakingUiState) {
|
||||
val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
|
||||
val validatorCount = validatorState?.balances
|
||||
?.filterNot { it.validator?.address.isNullOrBlank() }
|
||||
?.distinctBy { it.validator?.address }
|
||||
?.size ?: 0
|
||||
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.StakingInfoScreenOpened(
|
||||
validatorsCount = validatorCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun confirmationScreen(value: StakingUiState) {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ConfirmationScreenOpened(
|
||||
validator = validatorName,
|
||||
action = getStakingActionType(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun screenCancel(value: StakingUiState) {
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ButtonCancel(
|
||||
source = when (value.currentStep) {
|
||||
StakingStep.InitialInfo -> StakeScreenSource.Info
|
||||
StakingStep.Amount -> StakeScreenSource.Amount
|
||||
StakingStep.Confirmation -> StakeScreenSource.Confirmation
|
||||
StakingStep.Validators,
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
-> StakeScreenSource.Validators
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendTransactionApprovalAnalytics(tokenCryptoCurrency: CryptoCurrency) {
|
||||
analyticsEventHandler.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Approve(
|
||||
blockchain = tokenCryptoCurrency.network.name,
|
||||
token = tokenCryptoCurrency.symbol,
|
||||
feeType = AnalyticsParam.FeeType.Normal,
|
||||
permissionType = ApproveType.LIMITED.name,
|
||||
),
|
||||
memoType = Basic.TransactionSent.MemoType.Null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendTransactionStakingAnalytics(value: StakingUiState) {
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Staking(
|
||||
blockchain = value.cryptoCurrencyName,
|
||||
token = value.cryptoCurrencySymbol,
|
||||
feeType = AnalyticsParam.FeeType.Normal,
|
||||
),
|
||||
memoType = Basic.TransactionSent.MemoType.Null,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.StakeInProgressScreenOpened(
|
||||
validator = validatorName,
|
||||
action = getStakingActionType(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
|
||||
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
|
||||
val validatorName = validatorState?.chosenValidator?.name ?: return
|
||||
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvent.ButtonAction(
|
||||
action = getStakingActionType(value),
|
||||
validator = validatorName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStakingActionType(value: StakingUiState): StakingActionType {
|
||||
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
return when (value.actionType) {
|
||||
StakingActionCommonType.Enter -> StakingActionType.STAKE
|
||||
is StakingActionCommonType.Exit -> StakingActionType.UNSTAKE
|
||||
is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.staking.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.staking.api.StakingComponent
|
||||
import com.tangem.features.staking.impl.DefaultStakingComponent
|
||||
import com.tangem.features.staking.impl.navigation.DefaultStakingRouter
|
||||
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface StakingModule {
|
||||
|
||||
@Binds
|
||||
fun bindComponentFactory(factory: DefaultStakingComponent.Factory): StakingComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(StakingModel::class)
|
||||
fun bindModel(model: StakingModel): Model
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface StakingComponentModule {
|
||||
|
||||
@Binds
|
||||
@ComponentScoped
|
||||
fun bindRouter(impl: DefaultStakingRouter): InnerStakingRouter
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.staking.impl.navigation
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class DefaultStakingRouter @Inject constructor(
|
||||
private val urlOpener: UrlOpener,
|
||||
private val router: AppRouter,
|
||||
) : InnerStakingRouter {
|
||||
|
||||
override fun openUrl(url: String) {
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
|
||||
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
router.pop { isSuccess ->
|
||||
if (isSuccess) {
|
||||
router.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.staking.impl.navigation
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal interface InnerStakingRouter {
|
||||
|
||||
fun openUrl(url: String)
|
||||
|
||||
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.staking.impl.presentation.model
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal interface StakingClickIntents : AmountScreenClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onNextClick(balanceState: BalanceState? = null)
|
||||
|
||||
fun onActionClick()
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onRefreshSwipe(isRefreshing: Boolean)
|
||||
|
||||
fun onInitialInfoBannerClick()
|
||||
|
||||
fun onInfoClick(infoType: InfoType)
|
||||
|
||||
fun onAmountEnterClick()
|
||||
|
||||
fun getFee()
|
||||
|
||||
override fun onAmountNext() = onNextClick()
|
||||
|
||||
fun openValidators()
|
||||
|
||||
fun onValidatorSelect(validator: Yield.Validator)
|
||||
|
||||
fun openRewardsValidators()
|
||||
|
||||
fun onActiveStake(activeStake: BalanceState)
|
||||
|
||||
fun onActiveStakeAnalytic()
|
||||
|
||||
fun showApprovalBottomSheet()
|
||||
|
||||
fun onApproveTypeChange(approveType: ApproveType)
|
||||
|
||||
fun onApprovalClick()
|
||||
|
||||
fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
)
|
||||
|
||||
fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>)
|
||||
|
||||
fun onNotificationCancel(notification: Class<out NotificationUM>)
|
||||
|
||||
fun onExploreClick()
|
||||
|
||||
fun onShareClick()
|
||||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
|
||||
fun openTokenDetails(cryptoCurrency: CryptoCurrency)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
internal sealed class FeeState {
|
||||
|
||||
data class Content(
|
||||
val fee: Fee?,
|
||||
val rate: BigDecimal?,
|
||||
val isFeeConvertibleToFiat: Boolean,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFeeApproximate: Boolean,
|
||||
) : FeeState()
|
||||
|
||||
data object Loading : FeeState()
|
||||
|
||||
data object Error : FeeState()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
internal enum class InnerConfirmationStakingState {
|
||||
ASSENT,
|
||||
IN_PROGRESS,
|
||||
COMPLETED,
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
|
||||
internal sealed class InnerFeeState {
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
) : InnerFeeState()
|
||||
|
||||
data object Loading : InnerFeeState()
|
||||
|
||||
data object Error : InnerFeeState()
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.RewardBlockType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal sealed class InnerYieldBalanceState {
|
||||
data class Data(
|
||||
val rewardsCrypto: String,
|
||||
val rewardsFiat: String,
|
||||
val rewardBlockType: RewardBlockType,
|
||||
val isActionable: Boolean,
|
||||
val balances: ImmutableList<BalanceState>,
|
||||
) : InnerYieldBalanceState()
|
||||
|
||||
data object Empty : InnerYieldBalanceState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class BalanceState(
|
||||
val groupId: String,
|
||||
val title: TextReference,
|
||||
val type: BalanceType,
|
||||
val subtitle: TextReference?,
|
||||
val isClickable: Boolean,
|
||||
val cryptoValue: String,
|
||||
val cryptoAmount: BigDecimal,
|
||||
val formattedCryptoAmount: TextReference,
|
||||
val fiatAmount: BigDecimal?,
|
||||
val formattedFiatAmount: TextReference,
|
||||
val rawCurrencyId: String?,
|
||||
val validator: Yield.Validator?,
|
||||
val pendingActions: ImmutableList<PendingAction>,
|
||||
val isPending: Boolean,
|
||||
val date: DateTime,
|
||||
)
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
||||
internal object StakingNotification {
|
||||
|
||||
sealed class Error(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM.Error(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.ic_alert_24,
|
||||
buttonState = buttonState,
|
||||
onCloseClick = onCloseClick,
|
||||
) {
|
||||
data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error(
|
||||
title = stringReference(message),
|
||||
subtitle = stringReference(message),
|
||||
)
|
||||
|
||||
data class Common(val subtitle: TextReference) : StakingNotification.Error(
|
||||
title = resourceReference(R.string.common_error),
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM.Warning(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
) {
|
||||
data class TransactionInProgress(
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
) : StakingNotification.Warning(title = title, subtitle = description)
|
||||
|
||||
data object LowStakedBalance : StakingNotification.Warning(
|
||||
title = resourceReference(R.string.staking_notification_low_staked_balance_title),
|
||||
subtitle = resourceReference(R.string.staking_notification_low_staked_balance_text),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Info(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationUM.Info(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
buttonsState = buttonsState,
|
||||
onCloseClick = onCloseClick,
|
||||
) {
|
||||
data class EarnRewards(
|
||||
val subtitleText: TextReference,
|
||||
) : StakingNotification.Info(
|
||||
title = resourceReference(R.string.staking_notification_earn_rewards_title),
|
||||
subtitle = subtitleText,
|
||||
)
|
||||
|
||||
data object StakeEntireBalance : StakingNotification.Info(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
subtitle = resourceReference(R.string.staking_notification_stake_entire_balance_text),
|
||||
)
|
||||
|
||||
data class Unstake(
|
||||
val cooldownPeriodDays: Int,
|
||||
@StringRes val subtitleRes: Int,
|
||||
) : StakingNotification.Info(
|
||||
title = resourceReference(R.string.common_unstake),
|
||||
subtitle = resourceReference(
|
||||
subtitleRes,
|
||||
wrappedList(
|
||||
pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = cooldownPeriodDays,
|
||||
formatArgs = wrappedList(cooldownPeriodDays),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
data class Ordinary(
|
||||
val title: TextReference,
|
||||
val text: TextReference,
|
||||
) : StakingNotification.Info(
|
||||
title = title,
|
||||
subtitle = text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class StakingStateController @Inject constructor(
|
||||
urlOpener: UrlOpener,
|
||||
) {
|
||||
|
||||
val value: StakingUiState get() = uiState.value
|
||||
|
||||
private val mutableUiState: MutableStateFlow<StakingUiState> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
|
||||
|
||||
private val buttonsTransformer = SetButtonsStateTransformer(urlOpener)
|
||||
private val titleTransformer = SetTitleTransformer
|
||||
|
||||
fun update(function: (StakingUiState) -> StakingUiState) {
|
||||
mutableUiState.update(function = function)
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun update(transformer: Transformer<StakingUiState>) {
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun updateAll(vararg transformer: Transformer<StakingUiState>) {
|
||||
transformer.forEach { mutableUiState.update(function = it::transform) }
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mutableUiState.update { getInitialState() }
|
||||
mutableUiState.update(function = buttonsTransformer::transform)
|
||||
mutableUiState.update(function = titleTransformer::transform)
|
||||
}
|
||||
|
||||
fun updateEvent(event: StakingEvent?) {
|
||||
mutableUiState.update {
|
||||
it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent())
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAlert() {
|
||||
mutableUiState.update { it.copy(event = consumedEvent()) }
|
||||
}
|
||||
|
||||
private fun getInitialState(): StakingUiState {
|
||||
return StakingUiState(
|
||||
title = TextReference.EMPTY,
|
||||
subtitle = null,
|
||||
clickIntents = StakingClickIntentsStub,
|
||||
walletName = "",
|
||||
cryptoCurrencyName = "",
|
||||
cryptoCurrencySymbol = "",
|
||||
cryptoCurrencyBlockchainId = "",
|
||||
currentStep = StakingStep.InitialInfo,
|
||||
initialInfoState = StakingStates.InitialInfoState.Empty(),
|
||||
amountState = AmountState.Empty(),
|
||||
validatorState = StakingStates.ValidatorState.Empty(),
|
||||
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
|
||||
confirmationState = StakingStates.ConfirmationState.Empty(),
|
||||
isBalanceHidden = false,
|
||||
event = consumedEvent(),
|
||||
bottomSheetConfig = null,
|
||||
actionType = StakingActionCommonType.Enter,
|
||||
buttonsState = NavigationButtonsState.Empty,
|
||||
balanceState = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender
|
||||
|
||||
internal class StakingStateRouter(
|
||||
private val appRouter: AppRouter,
|
||||
private val stateController: StakingStateController,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
private val analyticSender = StakingAnalyticSender(analyticsEventsHandler)
|
||||
|
||||
fun onBackClick() {
|
||||
analyticSender.screenCancel(stateController.value)
|
||||
appRouter.pop()
|
||||
stateController.clear()
|
||||
}
|
||||
|
||||
fun onNextClick() {
|
||||
when (stateController.value.currentStep) {
|
||||
StakingStep.InitialInfo -> when (val actionType = stateController.value.actionType) {
|
||||
StakingActionCommonType.Enter -> showAmount()
|
||||
is StakingActionCommonType.Exit -> if (actionType.partiallyUnstakeDisabled) {
|
||||
showConfirmation()
|
||||
} else {
|
||||
showAmount()
|
||||
}
|
||||
StakingActionCommonType.Pending.Other,
|
||||
StakingActionCommonType.Pending.Rewards,
|
||||
-> showConfirmation()
|
||||
StakingActionCommonType.Pending.Restake -> showRestakeValidators()
|
||||
}
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Validators,
|
||||
StakingStep.Amount,
|
||||
-> showConfirmation()
|
||||
StakingStep.Confirmation -> showInitial()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
val uiState = stateController.uiState.value
|
||||
when (uiState.currentStep) {
|
||||
StakingStep.InitialInfo -> onBackClick()
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.Amount,
|
||||
-> showInitial()
|
||||
StakingStep.Confirmation -> {
|
||||
when (val actionType = uiState.actionType) {
|
||||
StakingActionCommonType.Enter -> showAmount()
|
||||
is StakingActionCommonType.Pending -> showInitial()
|
||||
is StakingActionCommonType.Exit -> {
|
||||
if (actionType.partiallyUnstakeDisabled) showInitial() else showAmount()
|
||||
}
|
||||
}
|
||||
}
|
||||
StakingStep.Validators -> showConfirmation()
|
||||
}
|
||||
}
|
||||
|
||||
fun showValidators() {
|
||||
stateController.update { it.copy(currentStep = StakingStep.Validators) }
|
||||
}
|
||||
|
||||
private fun showInitial() {
|
||||
analyticSender.initialInfoScreen(stateController.value)
|
||||
stateController.update { it.copy(currentStep = StakingStep.InitialInfo) }
|
||||
}
|
||||
|
||||
fun showRewardsValidators() {
|
||||
analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened)
|
||||
stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) }
|
||||
}
|
||||
|
||||
private fun showAmount() {
|
||||
analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened)
|
||||
stateController.update { it.copy(currentStep = StakingStep.Amount) }
|
||||
}
|
||||
|
||||
private fun showRestakeValidators() {
|
||||
stateController.update { it.copy(currentStep = StakingStep.RestakeValidator) }
|
||||
}
|
||||
|
||||
private fun showConfirmation() {
|
||||
analyticSender.confirmationScreen(stateController.value)
|
||||
stateController.update { it.copy(currentStep = StakingStep.Confirmation) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Ui states of the staking screen
|
||||
*/
|
||||
@Immutable
|
||||
internal data class StakingUiState(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference?,
|
||||
val clickIntents: StakingClickIntents,
|
||||
val walletName: String,
|
||||
val cryptoCurrencyName: String,
|
||||
val cryptoCurrencySymbol: String,
|
||||
val cryptoCurrencyBlockchainId: String,
|
||||
val currentStep: StakingStep,
|
||||
val initialInfoState: StakingStates.InitialInfoState,
|
||||
val amountState: AmountState,
|
||||
val rewardsValidatorsState: StakingStates.RewardsValidatorsState,
|
||||
val confirmationState: StakingStates.ConfirmationState,
|
||||
val validatorState: StakingStates.ValidatorState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val actionType: StakingActionCommonType,
|
||||
val buttonsState: NavigationButtonsState,
|
||||
val event: StateEvent<StakingEvent>,
|
||||
val balanceState: BalanceState?,
|
||||
) {
|
||||
|
||||
fun copyWrapped(
|
||||
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
|
||||
amountState: AmountState = this.amountState,
|
||||
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
|
||||
validatorState: StakingStates.ValidatorState = this.validatorState,
|
||||
): StakingUiState = copy(
|
||||
initialInfoState = initialInfoState,
|
||||
amountState = amountState,
|
||||
confirmationState = confirmationState,
|
||||
validatorState = validatorState,
|
||||
)
|
||||
}
|
||||
|
||||
internal sealed class StakingStates {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/** Initial info state */
|
||||
sealed class InitialInfoState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val showBanner: Boolean,
|
||||
val infoItems: ImmutableList<RoundedListWithDividersItemData>,
|
||||
val aprRange: TextReference,
|
||||
val onInfoClick: (InfoType) -> Unit,
|
||||
val yieldBalance: InnerYieldBalanceState,
|
||||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
) : InitialInfoState()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : InitialInfoState()
|
||||
}
|
||||
|
||||
/** Select validator to claim rewards state */
|
||||
sealed class RewardsValidatorsState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val rewards: ImmutableList<BalanceState>,
|
||||
) : RewardsValidatorsState()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : RewardsValidatorsState()
|
||||
}
|
||||
|
||||
sealed class ValidatorState : StakingStates() {
|
||||
abstract val isClickable: Boolean
|
||||
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
override val isClickable: Boolean,
|
||||
val isVisibleOnConfirmation: Boolean,
|
||||
val chosenValidator: Yield.Validator,
|
||||
val activeValidator: Yield.Validator?,
|
||||
val availableValidators: List<Yield.Validator>,
|
||||
) : ValidatorState()
|
||||
|
||||
data class Empty(
|
||||
override val isClickable: Boolean = false,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : ValidatorState()
|
||||
}
|
||||
|
||||
/** Confirmation state */
|
||||
sealed class ConfirmationState : StakingStates() {
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val innerState: InnerConfirmationStakingState,
|
||||
val feeState: FeeState,
|
||||
val pendingAction: PendingAction?,
|
||||
val pendingActions: ImmutableList<PendingAction>?,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
val footerText: TextReference,
|
||||
val transactionDoneState: TransactionDoneState,
|
||||
val isApprovalNeeded: Boolean,
|
||||
val isAmountEditable: Boolean,
|
||||
val allowance: BigDecimal,
|
||||
val reduceAmountBy: BigDecimal?,
|
||||
) : ConfirmationState()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : ConfirmationState()
|
||||
}
|
||||
}
|
||||
|
||||
enum class StakingStep {
|
||||
InitialInfo,
|
||||
RewardsValidators,
|
||||
Amount,
|
||||
RestakeValidator,
|
||||
Confirmation,
|
||||
Validators,
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.staking.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed class TransactionDoneState {
|
||||
|
||||
data class Content(
|
||||
val timestamp: Long,
|
||||
val txUrl: String,
|
||||
) : TransactionDoneState()
|
||||
|
||||
data object Empty : TransactionDoneState()
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.bottomsheet
|
||||
|
||||
internal enum class InfoType {
|
||||
ANNUAL_PERCENTAGE_RATE,
|
||||
UNBONDING_PERIOD,
|
||||
REWARD_CLAIMING,
|
||||
WARMUP_PERIOD,
|
||||
REWARD_SCHEDULE,
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.bottomsheet
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
|
||||
internal data class StakingActionSelectionBottomSheetConfig(
|
||||
val title: TextReference,
|
||||
val actions: List<PendingAction>,
|
||||
val onActionSelect: (PendingAction) -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.bottomsheet
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class StakingInfoBottomSheetConfig(
|
||||
val title: TextReference,
|
||||
val text: TextReference,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceItem
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import org.joda.time.DateTime
|
||||
import java.util.Calendar
|
||||
|
||||
internal class BalanceItemConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
) : Converter<BalanceItem, BalanceState?> {
|
||||
|
||||
override fun convert(value: BalanceItem): BalanceState? {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
val validator = yield.validators.firstOrNull {
|
||||
value.validatorAddress?.contains(it.address, ignoreCase = true) == true
|
||||
}
|
||||
|
||||
val cryptoAmount = value.amount
|
||||
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
|
||||
|
||||
val title = value.type.getTitle(validator?.name)
|
||||
return title?.let {
|
||||
BalanceState(
|
||||
groupId = value.groupId,
|
||||
validator = validator,
|
||||
title = title,
|
||||
subtitle = getSubtitle(value),
|
||||
type = value.type,
|
||||
cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals),
|
||||
cryptoAmount = cryptoAmount,
|
||||
formattedCryptoAmount = stringReference(
|
||||
cryptoAmount.format { crypto(cryptoCurrency) },
|
||||
),
|
||||
fiatAmount = fiatAmount,
|
||||
formattedFiatAmount = stringReference(
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatAmount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
),
|
||||
rawCurrencyId = value.rawCurrencyId,
|
||||
pendingActions = value.pendingActions.toPersistentList(),
|
||||
isClickable = value.type.isClickable() && !value.isPending,
|
||||
isPending = value.isPending,
|
||||
date = value.date ?: DateTime.now(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BalanceType.getTitle(validatorName: String?) = when (this) {
|
||||
BalanceType.PREPARING,
|
||||
BalanceType.STAKED,
|
||||
-> validatorName?.let { stringReference(it) }
|
||||
BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked)
|
||||
BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking)
|
||||
BalanceType.LOCKED -> resourceReference(R.string.staking_locked)
|
||||
BalanceType.AVAILABLE,
|
||||
BalanceType.REWARDS,
|
||||
BalanceType.UNLOCKING,
|
||||
BalanceType.UNKNOWN,
|
||||
-> null
|
||||
}
|
||||
|
||||
private fun getSubtitle(balance: BalanceItem) = when (balance.type) {
|
||||
BalanceType.UNSTAKING -> getUnbondingDate(balance.date)
|
||||
BalanceType.UNSTAKED -> resourceReference(R.string.staking_tap_to_withdraw)
|
||||
BalanceType.LOCKED -> if (balance.pendingActions.any { it.type == StakingActionType.VOTE_LOCKED }) {
|
||||
resourceReference(R.string.staking_tap_to_unlock_or_vote)
|
||||
} else {
|
||||
resourceReference(R.string.staking_tap_to_unlock)
|
||||
}
|
||||
BalanceType.PREPARING -> {
|
||||
val warmupPeriod = yield.metadata.warmupPeriod.days
|
||||
combinedReference(
|
||||
resourceReference(R.string.staking_details_warmup_period),
|
||||
stringReference(" "),
|
||||
pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)),
|
||||
)
|
||||
}
|
||||
BalanceType.AVAILABLE,
|
||||
BalanceType.STAKED,
|
||||
BalanceType.UNLOCKING,
|
||||
BalanceType.REWARDS,
|
||||
BalanceType.UNKNOWN,
|
||||
-> null
|
||||
}
|
||||
|
||||
private fun getUnbondingDate(date: DateTime?): TextReference? {
|
||||
val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null
|
||||
if (date == null) {
|
||||
return combinedReference(
|
||||
resourceReference(R.string.staking_details_unbonding_period),
|
||||
stringReference(" "),
|
||||
pluralReference(R.plurals.common_days, unbondingPeriod, wrappedList(unbondingPeriod)),
|
||||
)
|
||||
}
|
||||
|
||||
val nowCalendar = Calendar.getInstance()
|
||||
nowCalendar.resetHours()
|
||||
|
||||
val endDate = Calendar.getInstance()
|
||||
endDate.timeInMillis = date.millis
|
||||
endDate.resetHours()
|
||||
|
||||
val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt()
|
||||
return if (days > 0) {
|
||||
resourceReference(
|
||||
R.string.common_left,
|
||||
wrappedList(
|
||||
pluralReference(R.plurals.common_days, days, wrappedList(days)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
resourceReference(R.string.common_today)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Calendar.resetHours() {
|
||||
this[Calendar.HOUR_OF_DAY] = 0
|
||||
this[Calendar.MINUTE] = 0
|
||||
this[Calendar.SECOND] = 0
|
||||
this[Calendar.MILLISECOND] = 0
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceItem
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class RewardsValidatorStateConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
|
||||
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
|
||||
return if (yieldBalance is YieldBalance.Data) {
|
||||
val balances = yieldBalance.balance.items
|
||||
StakingStates.RewardsValidatorsState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
rewards = balances
|
||||
.filter { it.type == BalanceType.REWARDS }
|
||||
.mapRewardBalances(cryptoCurrencyStatus)
|
||||
.toPersistentList(),
|
||||
)
|
||||
} else {
|
||||
StakingStates.RewardsValidatorsState.Empty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
|
||||
this.mapNotNull { balance ->
|
||||
val validator = yield.validators.firstOrNull {
|
||||
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
|
||||
}
|
||||
val cryptoValue = balance.amount
|
||||
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
|
||||
|
||||
validator?.toBalanceState(
|
||||
balance = balance,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoValue = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Yield.Validator.toBalanceState(
|
||||
balance: BalanceItem,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
cryptoValue: BigDecimal,
|
||||
fiatValue: BigDecimal?,
|
||||
): BalanceState {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val cryptoAmount = stringReference(
|
||||
cryptoValue.format {
|
||||
crypto(cryptoCurrency)
|
||||
},
|
||||
)
|
||||
val formattedFiatAmount = stringReference(
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatValue,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
)
|
||||
|
||||
return BalanceState(
|
||||
groupId = balance.groupId,
|
||||
validator = this,
|
||||
title = stringReference(this.name),
|
||||
subtitle = null,
|
||||
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
|
||||
cryptoAmount = cryptoValue,
|
||||
formattedCryptoAmount = cryptoAmount,
|
||||
fiatAmount = fiatValue,
|
||||
formattedFiatAmount = formattedFiatAmount,
|
||||
rawCurrencyId = cryptoCurrency.id.rawCurrencyId?.value,
|
||||
pendingActions = balance.pendingActions.toPersistentList(),
|
||||
isClickable = true,
|
||||
type = balance.type,
|
||||
isPending = balance.isPending,
|
||||
date = balance.date ?: DateTime.now(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.converters
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isBSC
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class YieldBalancesConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val balancesToShowProvider: Provider<List<BalanceItem>>,
|
||||
private val yield: Yield,
|
||||
) : Converter<Unit, InnerYieldBalanceState> {
|
||||
|
||||
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BalanceItemConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
|
||||
}
|
||||
|
||||
override fun convert(value: Unit): InnerYieldBalanceState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
|
||||
val balanceToShowItems = balancesToShowProvider()
|
||||
|
||||
return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) {
|
||||
val cryptoRewardsValue = (yieldBalance as? YieldBalance.Data)?.getRewardStakingBalance()
|
||||
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) {
|
||||
fiatRate.times(cryptoRewardsValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val (type, isActionable) = getRewardBlockType()
|
||||
InnerYieldBalanceState.Data(
|
||||
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
|
||||
rewardsFiat = fiatRewardsValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
rewardBlockType = type,
|
||||
isActionable = isActionable,
|
||||
balances = balanceToShowItems.mapBalances(),
|
||||
)
|
||||
} else {
|
||||
InnerYieldBalanceState.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<BalanceItem>.mapBalances() = asSequence()
|
||||
.filterNot { it.amount.isZero() || it.type == BalanceType.REWARDS }
|
||||
.mapNotNull(balanceItemConverter::convert)
|
||||
.sortedByDescending { it.cryptoAmount }
|
||||
.sortedBy { it.type.order }
|
||||
.toPersistentList()
|
||||
|
||||
private fun getRewardBlockType(): Pair<RewardBlockType, Boolean> {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val blockchainId = cryptoCurrencyStatus.currency.network.id.value
|
||||
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
|
||||
val rewards = yieldBalance?.balance?.items
|
||||
?.filter { it.type == BalanceType.REWARDS && !it.amount.isZero() }
|
||||
|
||||
val isActionable = rewards?.any { it.pendingActions.isNotEmpty() } == true
|
||||
val isRewardsClaimable = rewards?.isNotEmpty() == true
|
||||
|
||||
return when {
|
||||
isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable to false
|
||||
isRewardsClaimable -> RewardBlockType.Rewards to isActionable
|
||||
else -> RewardBlockType.NoRewards to false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class StakingAlertUM : AlertUM {
|
||||
|
||||
data class GenericError(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : StakingAlertUM() {
|
||||
override val title: TextReference = resourceReference(R.string.common_error)
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error)
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class StakingError(
|
||||
val code: String,
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : StakingAlertUM() {
|
||||
override val title: TextReference = resourceReference(R.string.common_error)
|
||||
override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code))
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data object NoAvailableValidators : StakingAlertUM() {
|
||||
override val title = resourceReference(R.string.common_error)
|
||||
override val message = resourceReference(R.string.staking_no_validators_error_message)
|
||||
override val confirmButtonText = resourceReference(R.string.common_ok)
|
||||
override val onConfirmClick = null
|
||||
}
|
||||
|
||||
data class FeeIncreased(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : StakingAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title)
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
}
|
||||
|
||||
data object ValidatorsUnavailable : StakingAlertUM() {
|
||||
override val onConfirmClick: (() -> Unit)? = null
|
||||
override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message)
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class StakingEvent {
|
||||
|
||||
data class ShowSnackBar(val text: TextReference) : StakingEvent()
|
||||
|
||||
data class ShowAlert(val alert: AlertUM) : StakingEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.events
|
||||
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
|
||||
internal class StakingEventFactory(
|
||||
private val stateController: StakingStateController,
|
||||
private val popBackStack: () -> Unit,
|
||||
private val onFailedTxEmailClick: (String) -> Unit,
|
||||
) {
|
||||
|
||||
fun createGenericErrorAlert(error: String) {
|
||||
val alert = StakingEvent.ShowAlert(
|
||||
StakingAlertUM.GenericError(
|
||||
onConfirmClick = { onFailedTxEmailClick(error) },
|
||||
),
|
||||
)
|
||||
stateController.updateEvent(alert)
|
||||
}
|
||||
|
||||
fun createSendTransactionErrorAlert(error: SendTransactionError?) {
|
||||
val alert = error?.let {
|
||||
TransactionErrorAlertConverter(
|
||||
popBackStack = popBackStack,
|
||||
onFailedTxEmailClick = onFailedTxEmailClick,
|
||||
).convert(error)
|
||||
}?.let {
|
||||
StakingEvent.ShowAlert(it)
|
||||
}
|
||||
stateController.updateEvent(alert)
|
||||
}
|
||||
|
||||
fun createStakingErrorAlert(error: StakingError) {
|
||||
val alert = StakingEvent.ShowAlert(
|
||||
StakingAlertUM.StakingError(
|
||||
code = error.toString(),
|
||||
onConfirmClick = { onFailedTxEmailClick(error.toString()) },
|
||||
),
|
||||
)
|
||||
stateController.updateEvent(alert)
|
||||
}
|
||||
|
||||
fun createStakingValidatorsUnavailableAlert() {
|
||||
val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable)
|
||||
stateController.updateEvent(alert)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
|
||||
import com.tangem.domain.staking.FetchActionsUseCase
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
|
||||
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.utils.coroutines.DelayedWork
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakingBalanceUpdater @AssistedInject constructor(
|
||||
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
|
||||
private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase,
|
||||
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
|
||||
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val fetchActionsUseCase: FetchActionsUseCase,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val yield: Yield,
|
||||
) {
|
||||
fun fullUpdate() {
|
||||
coroutineScope.launch {
|
||||
listOf(
|
||||
// we should update network to find pending tx after 1 sec
|
||||
async {
|
||||
fetchPendingTransactionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
networks = setOf(cryptoCurrencyStatus.currency.network),
|
||||
)
|
||||
},
|
||||
// we should update tx history and network for new balances
|
||||
async {
|
||||
updateStakeBalance()
|
||||
},
|
||||
async {
|
||||
updateTxHistory()
|
||||
},
|
||||
async {
|
||||
updateNetworkStatuses()
|
||||
},
|
||||
async {
|
||||
updateProcessingActions()
|
||||
},
|
||||
).awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun partialUpdate() {
|
||||
coroutineScope {
|
||||
listOf(
|
||||
async {
|
||||
updateNetworkStatuses(delay = 0)
|
||||
},
|
||||
async {
|
||||
updateProcessingActions()
|
||||
},
|
||||
).awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initialUpdate() {
|
||||
coroutineScope {
|
||||
listOf(
|
||||
async {
|
||||
updateStakeBalance()
|
||||
},
|
||||
async {
|
||||
updateProcessingActions()
|
||||
},
|
||||
).awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) {
|
||||
updateDelayedNetworkStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
delayMillis = delay,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateStakeBalance() {
|
||||
stakingYieldBalanceUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateTxHistory() {
|
||||
delay(BALANCE_UPDATE_DELAY)
|
||||
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateProcessingActions() {
|
||||
fetchActionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
networkType = yield.token.network,
|
||||
stakingActionStatus = StakingActionStatus.PROCESSING,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
): StakingBalanceUpdater
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BALANCE_UPDATE_DELAY = 11_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.staking.EstimateGasUseCase
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.staking.getCurrentToken
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val estimateGasUseCase: EstimateGasUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val yield: Yield,
|
||||
) {
|
||||
|
||||
suspend fun getFee(
|
||||
onStakingFee: (Fee) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
) {
|
||||
val state = stateController.value
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("Illegal state")
|
||||
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
|
||||
?: error("No validator provided")
|
||||
|
||||
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
?: error("No amount provided")
|
||||
|
||||
val pendingAction = confirmationState.pendingAction
|
||||
val pendingActions = confirmationState.pendingActions
|
||||
|
||||
val validatorAddress = validatorState.chosenValidator.address
|
||||
|
||||
val isEnter = state.actionType == StakingActionCommonType.Enter
|
||||
val isApprovalNeeded = confirmationState.isApprovalNeeded
|
||||
val isAllowanceNotEnough = confirmationState.allowance < amount
|
||||
if (isEnter && isApprovalNeeded && isAllowanceNotEnough) {
|
||||
getApproveFee(
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onApprovalFee = onApprovalFee,
|
||||
onApprovalFeeError = onFeeError,
|
||||
)
|
||||
} else {
|
||||
estimateGas(
|
||||
pendingAction = pendingAction,
|
||||
pendingActions = pendingActions,
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
onStakingFee = onStakingFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun estimateGas(
|
||||
pendingAction: PendingAction?,
|
||||
pendingActions: ImmutableList<PendingAction>?,
|
||||
amount: BigDecimal,
|
||||
validatorAddress: String,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onStakingFee: (Fee) -> Unit,
|
||||
) {
|
||||
val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
?: error("No available address")
|
||||
|
||||
val gasEstimate = if (isCompositePendingActions(
|
||||
networkId = cryptoCurrencyStatus.currency.network.id.value,
|
||||
pendingActions = pendingActions,
|
||||
)
|
||||
) {
|
||||
val result = coroutineScope {
|
||||
pendingActions?.map { action ->
|
||||
async {
|
||||
// Simultaneous or quick api calls can sometimes return ZERO fee
|
||||
estimateFeeRetry {
|
||||
estimateFee(
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
action = action,
|
||||
)
|
||||
}.getOrElse {
|
||||
onStakingFeeError(it)
|
||||
null
|
||||
}
|
||||
}
|
||||
}?.awaitAll()?.filterNotNull()
|
||||
}
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
|
||||
return
|
||||
}
|
||||
|
||||
val totalAmount = result.sumOf { it.amount }
|
||||
val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() }
|
||||
StakingGasEstimate(
|
||||
amount = totalAmount,
|
||||
token = result.first().token,
|
||||
gasLimit = totalGasLimit.toPlainString().orEmpty(),
|
||||
)
|
||||
} else {
|
||||
estimateFee(
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
action = pendingAction,
|
||||
).getOrElse {
|
||||
onStakingFeeError(it)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onStakingFee(
|
||||
Fee.Common(
|
||||
Amount(
|
||||
currencySymbol = gasEstimate.token.symbol,
|
||||
value = gasEstimate.amount,
|
||||
decimals = gasEstimate.token.decimals,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun estimateFee(
|
||||
amount: BigDecimal,
|
||||
sourceAddress: String,
|
||||
validatorAddress: String,
|
||||
action: PendingAction?,
|
||||
) = estimateGasUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = stateController.value.actionType,
|
||||
integrationId = yield.id,
|
||||
amount = amount,
|
||||
address = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun getApproveFee(
|
||||
amount: BigDecimal,
|
||||
validatorAddress: String,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onApprovalFeeError: (GetFeeError) -> Unit,
|
||||
) {
|
||||
getFeeUseCase(
|
||||
amount = amount,
|
||||
destination = validatorAddress,
|
||||
userWallet = userWallet,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
).fold(
|
||||
ifRight = { fee ->
|
||||
onApprovalFee(fee)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
onApprovalFeeError(error)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun estimateFeeRetry(
|
||||
times: Int = 3,
|
||||
delay: Long = 1000,
|
||||
block: suspend () -> Either<StakingError, StakingGasEstimate>,
|
||||
): Either<StakingError, StakingGasEstimate> {
|
||||
repeat(times - 1) {
|
||||
val feeResult = block()
|
||||
feeResult.fold(
|
||||
ifLeft = { return feeResult },
|
||||
ifRight = {
|
||||
if (!it.amount.isZero()) return feeResult
|
||||
},
|
||||
)
|
||||
delay(delay)
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
): StakingFeeTransactionLoader
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.staking.*
|
||||
import com.tangem.domain.staking.model.SubmitHashData
|
||||
import com.tangem.domain.staking.model.stakekit.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.staking.getCurrentToken
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakingTransactionSender @AssistedInject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val getStakingTransactionsUseCase: GetStakingTransactionsUseCase,
|
||||
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val submitHashUseCase: SubmitHashUseCase,
|
||||
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val yield: Yield,
|
||||
@Assisted private val isAmountSubtractAvailable: Boolean,
|
||||
) {
|
||||
|
||||
private val balanceUpdater: StakingBalanceUpdater
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield)
|
||||
|
||||
suspend fun constructAndSendTransactions(
|
||||
onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
onSendSuccess: (String) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
onFeeIncreased: (Fee) -> Unit,
|
||||
) {
|
||||
val state = stateController.value
|
||||
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("No confirmation state")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val amountState = state.amountState as? AmountState.Data ?: error("No amount state")
|
||||
|
||||
val stakingTransactions = getStakingTransactions(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
val fullTransactionsData = getConstructedTransactions(
|
||||
stakingTransactions = stakingTransactions,
|
||||
fee = fee,
|
||||
amount = amountState.amountTextField.cryptoAmount.value.orZero(),
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
if (fullTransactionsData.isNullOrEmpty()) {
|
||||
onConstructError(StakingError.DomainError("fullTransactionsData is null or empty"))
|
||||
return
|
||||
}
|
||||
|
||||
val totalFee = fullTransactionsData.sumOf { it.stakeKitTransaction.gasEstimate?.amount.orZero() }
|
||||
|
||||
if (fee.amount.value.orZero() >= totalFee) {
|
||||
onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction })
|
||||
sendStakingTransaction(
|
||||
fullTransactionsData = fullTransactionsData,
|
||||
onSendSuccess = onSendSuccess,
|
||||
onSendError = onSendError,
|
||||
)
|
||||
} else {
|
||||
onFeeIncreased(
|
||||
Fee.Common(
|
||||
fee.amount.copy(value = totalFee),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransactions(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
val isComposePendingActions = isCompositePendingActions(
|
||||
cryptoCurrencyStatus.currency.network.id.value,
|
||||
confirmationState.pendingActions,
|
||||
)
|
||||
if (isComposePendingActions) {
|
||||
confirmationState.pendingActions?.map { action ->
|
||||
async {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
action = action,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}?.awaitAll()?.flatten()
|
||||
} else {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getConstructedTransactions(
|
||||
stakingTransactions: List<StakingTransaction>?,
|
||||
fee: Fee,
|
||||
amount: BigDecimal,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
stakingTransactions
|
||||
?.filterNot {
|
||||
it.type == StakingTransactionType.APPROVAL || it.status == StakingTransactionStatus.SKIPPED
|
||||
}
|
||||
?.map { transaction ->
|
||||
async {
|
||||
getConstructedStakingTransactionUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.id.value,
|
||||
fee = fee,
|
||||
amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency),
|
||||
transactionId = transaction.id,
|
||||
).fold(
|
||||
ifRight = { (constructedTransaction, transactionData) ->
|
||||
FullTransactionData(
|
||||
stakeKitTransaction = constructedTransaction,
|
||||
tangemTransaction = transactionData,
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
onConstructError(it)
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
?.awaitAll()
|
||||
?.filterNotNull()
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransaction(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
action: PendingAction? = confirmationState.pendingAction,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
): List<StakingTransaction> {
|
||||
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
|
||||
?: error("No validator provided")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
?: error("No available address")
|
||||
val amountState = state.amountState as? AmountState.Data
|
||||
?: error("No amount provided")
|
||||
|
||||
val validatorAddress = validatorState.chosenValidator.address
|
||||
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
|
||||
|
||||
return getStakingTransactionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = state.actionType,
|
||||
integrationId = yield.id,
|
||||
amount = amount,
|
||||
address = defaultAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
).getOrElse {
|
||||
onConstructError(it)
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendStakingTransaction(
|
||||
fullTransactionsData: List<FullTransactionData>,
|
||||
onSendSuccess: (txUrl: String) -> Unit,
|
||||
onSendError: (SendTransactionError?) -> Unit,
|
||||
) {
|
||||
if (fullTransactionsData.isEmpty()) return
|
||||
|
||||
val sortedTransactions = fullTransactionsData.sortedBy { it.stakeKitTransaction.stepIndex }
|
||||
|
||||
val firstTransaction = sortedTransactions.first()
|
||||
val network = firstTransaction.stakeKitTransaction.network
|
||||
|
||||
val sendMode = if (network == NetworkType.SOLANA &&
|
||||
firstTransaction.stakeKitTransaction.type == StakingTransactionType.SPLIT
|
||||
) {
|
||||
TransactionSender.MultipleTransactionSendMode.WAIT_AFTER_FIRST
|
||||
} else {
|
||||
TransactionSender.MultipleTransactionSendMode.DEFAULT
|
||||
}
|
||||
|
||||
sendTransactionUseCase(
|
||||
txsData = sortedTransactions.map { it.tangemTransaction },
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
sendMode = sendMode,
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
onSendError(error)
|
||||
},
|
||||
ifRight = { transactionHashes ->
|
||||
submitHash(
|
||||
transactions = sortedTransactions.map { it.stakeKitTransaction },
|
||||
transactionHashes = transactionHashes,
|
||||
)
|
||||
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = transactionHashes.last(),
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
).getOrElse { "" }
|
||||
|
||||
balanceUpdater.fullUpdate()
|
||||
onSendSuccess(txUrl)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun submitHash(transactions: List<StakingTransaction>, transactionHashes: List<String>) {
|
||||
transactions
|
||||
.zip(transactionHashes)
|
||||
.forEach { (transaction, transactionHash) ->
|
||||
submitHashUseCase(
|
||||
SubmitHashData(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
),
|
||||
)
|
||||
.onLeft {
|
||||
saveUnsubmittedHashUseCase.invoke(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
)
|
||||
}.onRight {
|
||||
Timber.d("Successful hash submission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal {
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
|
||||
val feeValue = fee.amount.value ?: error("No fee value")
|
||||
val isEnterAction = stateController.value.actionType == StakingActionCommonType.Enter
|
||||
|
||||
return checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy.orZero(),
|
||||
)
|
||||
}
|
||||
|
||||
private data class FullTransactionData(
|
||||
val stakeKitTransaction: StakingTransaction,
|
||||
val tangemTransaction: TransactionData.Compiled,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
yield: Yield,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
): StakingTransactionSender
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType.Coin
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal object ConfirmationStatePreviewData {
|
||||
|
||||
private val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "MATIC",
|
||||
value = BigDecimal(0.159806),
|
||||
decimals = 18,
|
||||
type = Coin,
|
||||
),
|
||||
)
|
||||
|
||||
val assentStakingState = StakingStates.ConfirmationState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
innerState = InnerConfirmationStakingState.ASSENT,
|
||||
feeState = FeeState.Content(
|
||||
fee = fee,
|
||||
rate = BigDecimal.ONE,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
),
|
||||
footerText = stringReference("You stake \$715.11 and will be receiving ~\$35 monthly"),
|
||||
notifications = persistentListOf(
|
||||
StakingNotification.Info.EarnRewards(
|
||||
subtitleText = resourceReference(
|
||||
id = R.string.staking_notification_earn_rewards_text_period_day,
|
||||
formatArgs = wrappedList("Solana"),
|
||||
),
|
||||
),
|
||||
),
|
||||
transactionDoneState = TransactionDoneState.Empty,
|
||||
pendingAction = null,
|
||||
isApprovalNeeded = false,
|
||||
allowance = BigDecimal.ZERO,
|
||||
reduceAmountBy = null,
|
||||
pendingActions = null,
|
||||
isAmountEditable = true,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.RewardBlockType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal object InitialStakingStatePreview {
|
||||
val defaultState = StakingStates.InitialInfoState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
showBanner = true,
|
||||
aprRange = stringReference("2.54-5.12%"),
|
||||
infoItems = persistentListOf(
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_available,
|
||||
startText = TextReference.Res(R.string.staking_details_available),
|
||||
endText = TextReference.Str("15 SOL"),
|
||||
isEndTextHideable = true,
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_annual_percentage_rate,
|
||||
startText = TextReference.Res(R.string.staking_details_annual_percentage_rate),
|
||||
endText = TextReference.Str("2.54-5.12%"),
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_unbonding_period,
|
||||
startText = TextReference.Res(R.string.staking_details_unbonding_period),
|
||||
endText = TextReference.Str("3d"),
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_minimum_requirement,
|
||||
startText = TextReference.Res(R.string.staking_details_minimum_requirement),
|
||||
endText = TextReference.Str("12 SOL"),
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_reward_claiming,
|
||||
startText = TextReference.Res(R.string.staking_details_reward_claiming),
|
||||
endText = TextReference.Str("Auto"),
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_warmup_period,
|
||||
startText = TextReference.Res(R.string.staking_details_warmup_period),
|
||||
endText = TextReference.Str("Days"),
|
||||
),
|
||||
RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_reward_schedule,
|
||||
startText = TextReference.Res(R.string.staking_details_reward_schedule),
|
||||
endText = TextReference.Str("Block"),
|
||||
),
|
||||
),
|
||||
onInfoClick = {},
|
||||
yieldBalance = InnerYieldBalanceState.Empty,
|
||||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
|
||||
)
|
||||
|
||||
val stateWithYield = defaultState.copy(
|
||||
yieldBalance = InnerYieldBalanceState.Data(
|
||||
rewardsFiat = "100 $",
|
||||
rewardsCrypto = "100 SOL",
|
||||
rewardBlockType = RewardBlockType.RewardUnavailable,
|
||||
isActionable = true,
|
||||
balances = persistentListOf(
|
||||
BalanceState(
|
||||
groupId = "groupId",
|
||||
title = stringReference("Binance"),
|
||||
cryptoValue = "100",
|
||||
formattedCryptoAmount = stringReference("100 SOL"),
|
||||
cryptoAmount = "100".toBigDecimal(),
|
||||
fiatAmount = null,
|
||||
formattedFiatAmount = stringReference("100 $"),
|
||||
rawCurrencyId = null,
|
||||
validator = Yield.Validator(
|
||||
address = "address",
|
||||
status = Yield.Validator.ValidatorStatus.ACTIVE,
|
||||
name = "Binance",
|
||||
image = null,
|
||||
website = null,
|
||||
apr = "5".toBigDecimal(),
|
||||
commission = null,
|
||||
stakedBalance = null,
|
||||
votingPower = null,
|
||||
preferred = false,
|
||||
isStrategicPartner = false,
|
||||
),
|
||||
pendingActions = persistentListOf(),
|
||||
isClickable = true,
|
||||
type = BalanceType.STAKED,
|
||||
subtitle = null,
|
||||
isPending = false,
|
||||
date = DateTime.now(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal object ValidatorStatePreviewData {
|
||||
|
||||
private val validatorList = listOf(
|
||||
Yield.Validator(
|
||||
address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951",
|
||||
status = ValidatorStatus.ACTIVE,
|
||||
name = "Luganodes",
|
||||
image = "https://assets.stakek.it/validators/luganodes.png",
|
||||
apr = BigDecimal("0.054823398040640445"),
|
||||
commission = 0.1,
|
||||
stakedBalance = "355544384.45009977",
|
||||
website = "https://luganodes.com/",
|
||||
votingPower = 0.09778360195377911,
|
||||
preferred = true,
|
||||
isStrategicPartner = false,
|
||||
),
|
||||
Yield.Validator(
|
||||
address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32",
|
||||
status = ValidatorStatus.ACTIVE,
|
||||
name = "InfStones",
|
||||
image = "https://assets.stakek.it/validators/infstones.png",
|
||||
apr = BigDecimal("0.057786472172836965"),
|
||||
commission = 0.05,
|
||||
stakedBalance = "12495684.05643019",
|
||||
website = "https://infstones.com/",
|
||||
votingPower = 0.0034366257754399774,
|
||||
preferred = true,
|
||||
isStrategicPartner = true,
|
||||
),
|
||||
Yield.Validator(
|
||||
address = "0xd14a87025109013b0a2354a775cb335f926af65a",
|
||||
status = ValidatorStatus.ACTIVE,
|
||||
name = "Kiln",
|
||||
image = "https://assets.stakek.it/validators/kiln.png",
|
||||
apr = BigDecimal("0.057786472172836965"),
|
||||
commission = 0.05,
|
||||
stakedBalance = "85400369.96393165",
|
||||
website = "https://infstones.com/",
|
||||
votingPower = 0.023487238579718264,
|
||||
preferred = true,
|
||||
isStrategicPartner = false,
|
||||
),
|
||||
)
|
||||
|
||||
val validatorState = StakingStates.ValidatorState.Data(
|
||||
availableValidators = validatorList,
|
||||
chosenValidator = validatorList.first(),
|
||||
isPrimaryButtonEnabled = true,
|
||||
activeValidator = null,
|
||||
isClickable = true,
|
||||
isVisibleOnConfirmation = true,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.stub
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal object StakingClickIntentsStub : StakingClickIntents {
|
||||
|
||||
override fun onBackClick() {}
|
||||
|
||||
override fun onNextClick(balanceState: BalanceState?) {}
|
||||
|
||||
override fun onActionClick() {}
|
||||
|
||||
override fun onPrevClick() {}
|
||||
|
||||
override fun onRefreshSwipe(isRefreshing: Boolean) {}
|
||||
|
||||
override fun onInitialInfoBannerClick() {}
|
||||
|
||||
override fun onInfoClick(infoType: InfoType) {}
|
||||
|
||||
override fun onAmountEnterClick() {}
|
||||
|
||||
override fun onAmountValueChange(value: String) {}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {}
|
||||
|
||||
override fun onMaxValueClick() {}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {}
|
||||
|
||||
override fun onAmountNext() {}
|
||||
|
||||
override fun openValidators() {}
|
||||
|
||||
override fun onValidatorSelect(validator: Yield.Validator) {}
|
||||
|
||||
override fun openRewardsValidators() {}
|
||||
|
||||
override fun showApprovalBottomSheet() {}
|
||||
|
||||
override fun onApproveTypeChange(approveType: ApproveType) {}
|
||||
|
||||
override fun onApprovalClick() {}
|
||||
|
||||
override fun onExploreClick() {}
|
||||
|
||||
override fun onShareClick() {}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {}
|
||||
|
||||
override fun onActiveStake(activeStake: BalanceState) {}
|
||||
|
||||
override fun getFee() {}
|
||||
|
||||
override fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {}
|
||||
|
||||
override fun onNotificationCancel(notification: Class<out NotificationUM>) {}
|
||||
|
||||
override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {}
|
||||
|
||||
override fun onActiveStakeAnalytic() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class AddStakingErrorTransformer(
|
||||
private val error: StakingError? = null,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmationState =
|
||||
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
|
||||
|
||||
val notifications = buildList {
|
||||
addAll(confirmationState.notifications)
|
||||
error?.let { add(convertToNotification(it)) }
|
||||
}.toPersistentList()
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = confirmationState.copy(
|
||||
notifications = notifications,
|
||||
feeState = FeeState.Error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertToNotification(error: StakingError): NotificationUM {
|
||||
return StakingNotification.Error.Common(
|
||||
subtitle = stringReference(error.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object DismissBottomSheetStateTransformer : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(bottomSheetConfig = null)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class HideBalanceStateTransformer(
|
||||
private val isBalanceHidden: Boolean,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetAmountDataTransformer(
|
||||
private val clickIntents: StakingClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val title = if (prevState.actionType is StakingActionCommonType.Exit) {
|
||||
resourceReference(R.string.staking_staked_amount)
|
||||
} else {
|
||||
stringReference(userWalletProvider().name)
|
||||
}
|
||||
val cryptoBalanceValue = cryptoCurrencyStatusProvider().value
|
||||
val (amount, fiatAmount) = if (prevState.actionType != StakingActionCommonType.Enter) {
|
||||
prevState.balanceState?.cryptoAmount to prevState.balanceState?.fiatAmount
|
||||
} else {
|
||||
cryptoBalanceValue.amount to cryptoBalanceValue.fiatAmount
|
||||
}
|
||||
val maxEnterAmount = EnterAmountBoundary(
|
||||
amount = amount,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatRate = cryptoBalanceValue.fiatRate,
|
||||
)
|
||||
|
||||
return prevState.copy(
|
||||
amountState = AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
iconStateConverter = iconStateConverter,
|
||||
maxEnterAmount = maxEnterAmount,
|
||||
).convert(
|
||||
AmountParameters(
|
||||
title = title,
|
||||
value = "",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SetButtonsStateTransformer(
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
val buttonsState = if (prevState.isButtonsVisible()) {
|
||||
NavigationButtonsState.Data(
|
||||
primaryButton = getPrimaryButton(prevState),
|
||||
prevButton = getPrevButton(prevState),
|
||||
extraButtons = getExtraButtons(prevState),
|
||||
txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl,
|
||||
onTextClick = urlOpener::openUrl,
|
||||
)
|
||||
} else {
|
||||
NavigationButtonsState.Empty
|
||||
}
|
||||
|
||||
return prevState.copy(buttonsState = buttonsState)
|
||||
}
|
||||
|
||||
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
|
||||
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val innerConfirmState = confirmState?.innerState
|
||||
|
||||
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
|
||||
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
|
||||
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
|
||||
|
||||
val isIconVisible = isConfirmation && !isCompleted
|
||||
return NavigationButton(
|
||||
textReference = prevState.getButtonText(),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
isSecondary = false,
|
||||
isIconVisible = isIconVisible,
|
||||
showProgress = isInProgress,
|
||||
isEnabled = prevState.isButtonEnabled(),
|
||||
onClick = { prevState.onPrimaryClick() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
|
||||
return NavigationButton(
|
||||
textReference = TextReference.EMPTY,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onPrevClick,
|
||||
).takeIf { prevState.currentStep.isPrevButtonVisible() }
|
||||
}
|
||||
|
||||
private fun getExtraButtons(prevState: StakingUiState): ImmutableList<NavigationButton> {
|
||||
return persistentListOf(
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onExploreClick,
|
||||
),
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
isSecondary = true,
|
||||
isIconVisible = true,
|
||||
showProgress = false,
|
||||
isEnabled = true,
|
||||
onClick = prevState.clickIntents::onShareClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) {
|
||||
StakingStep.RewardsValidators -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun StakingUiState.getButtonText(): TextReference {
|
||||
return when (currentStep) {
|
||||
StakingStep.InitialInfo -> {
|
||||
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
|
||||
resourceReference(R.string.staking_stake_more)
|
||||
} else {
|
||||
resourceReference(R.string.common_stake)
|
||||
}
|
||||
}
|
||||
|
||||
StakingStep.Confirmation -> getConfirmationButtonText()
|
||||
StakingStep.Validators -> resourceReference(R.string.common_continue)
|
||||
StakingStep.Amount,
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.RewardsValidators,
|
||||
-> resourceReference(R.string.common_next)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingUiState.getConfirmationButtonText(): TextReference {
|
||||
val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val amountState = amountState as? AmountState.Data
|
||||
return if (confirmationState != null && amountState != null) {
|
||||
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
|
||||
resourceReference(R.string.common_close)
|
||||
} else {
|
||||
when (actionType) {
|
||||
StakingActionCommonType.Enter -> {
|
||||
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||
if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
|
||||
resourceReference(R.string.give_permission_title)
|
||||
} else {
|
||||
resourceReference(R.string.common_stake)
|
||||
}
|
||||
}
|
||||
is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
|
||||
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resourceReference(R.string.common_close)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingUiState.onPrimaryClick() {
|
||||
when (currentStep) {
|
||||
StakingStep.InitialInfo -> clickIntents.onNextClick()
|
||||
StakingStep.Validators,
|
||||
StakingStep.RestakeValidator,
|
||||
-> clickIntents.onNextClick()
|
||||
StakingStep.Amount -> clickIntents.onAmountEnterClick()
|
||||
StakingStep.Confirmation -> onConfirmationClick()
|
||||
StakingStep.RewardsValidators -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingUiState.onConfirmationClick() {
|
||||
val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val amountState = amountState as? AmountState.Data
|
||||
if (confirmationState != null && amountState != null) {
|
||||
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
|
||||
clickIntents.onNextClick()
|
||||
} else {
|
||||
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||
val isEnterAction = actionType == StakingActionCommonType.Enter
|
||||
if (isEnterAction && confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
|
||||
clickIntents.showApprovalBottomSheet()
|
||||
} else {
|
||||
clickIntents.onActionClick()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clickIntents.onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
|
||||
StakingStep.InitialInfo,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.Confirmation,
|
||||
StakingStep.Validators,
|
||||
-> false
|
||||
StakingStep.Amount,
|
||||
-> true
|
||||
}
|
||||
|
||||
private fun StakingUiState.isButtonEnabled(): Boolean {
|
||||
return when (currentStep) {
|
||||
StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled
|
||||
StakingStep.Amount -> amountState.isPrimaryButtonEnabled
|
||||
StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled
|
||||
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.Validators,
|
||||
-> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetConfirmationStateAssentTransformer(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
private val fee: Fee,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
confirmationState = prevState.confirmationState.copyWrapped(fee),
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState {
|
||||
if (this is StakingStates.ConfirmationState.Data) {
|
||||
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
|
||||
return copy(
|
||||
feeState = FeeState.Content(
|
||||
fee = fee,
|
||||
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
|
||||
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
isFeeApproximate = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = true,
|
||||
)
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SetConfirmationStateCompletedTransformer(
|
||||
private val txUrl: String,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
confirmationState = prevState.confirmationState.copyWrapped(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
|
||||
return if (this is StakingStates.ConfirmationState.Data) {
|
||||
copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
innerState = InnerConfirmationStakingState.COMPLETED,
|
||||
footerText = TextReference.EMPTY,
|
||||
notifications = persistentListOf(),
|
||||
transactionDoneState = TransactionDoneState.Content(
|
||||
timestamp = System.currentTimeMillis(),
|
||||
txUrl = txUrl,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object SetConfirmationStateEmptyTransformer : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
actionType = StakingActionCommonType.Enter,
|
||||
validatorState = StakingStates.ValidatorState.Empty(),
|
||||
confirmationState = StakingStates.ConfirmationState.Empty(),
|
||||
balanceState = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
confirmationState = prevState.confirmationState.copyWrapped(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
|
||||
return if (this is StakingStates.ConfirmationState.Data) {
|
||||
copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
innerState = InnerConfirmationStakingState.IN_PROGRESS,
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.staking.model.StakingApproval
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SetConfirmationStateInitTransformer(
|
||||
private val isEnter: Boolean,
|
||||
private val isExplicitExit: Boolean,
|
||||
private val balanceState: BalanceState?,
|
||||
private val stakingApproval: StakingApproval,
|
||||
private val stakingAllowance: BigDecimal,
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val yieldArgs: Yield.Args,
|
||||
private val pendingActions: ImmutableList<PendingAction>? = null,
|
||||
private val pendingAction: PendingAction? = pendingActions?.firstOrNull(),
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val networkId
|
||||
get() = cryptoCurrencyStatus.currency.network.id.value
|
||||
|
||||
private val isComposePendingActions
|
||||
get() = isCompositePendingActions(networkId, pendingActions)
|
||||
|
||||
private val isTronStakedBalance
|
||||
get() = isTronStakedBalance(networkId, pendingAction)
|
||||
|
||||
private val isImplicitExit: Boolean
|
||||
get() = pendingAction == null && pendingActions?.isEmpty() == true || isTronStakedBalance
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val actionType = when {
|
||||
isEnter -> StakingActionCommonType.Enter
|
||||
isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartialUnstakeDisabled(prevState))
|
||||
else -> when (pendingAction?.type) {
|
||||
StakingActionType.STAKE -> StakingActionCommonType.Enter
|
||||
StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartialUnstakeDisabled(prevState))
|
||||
StakingActionType.CLAIM_REWARDS,
|
||||
StakingActionType.RESTAKE_REWARDS,
|
||||
-> StakingActionCommonType.Pending.Rewards
|
||||
StakingActionType.VOTE_LOCKED,
|
||||
StakingActionType.RESTAKE,
|
||||
-> StakingActionCommonType.Pending.Restake
|
||||
else -> StakingActionCommonType.Pending.Other
|
||||
}
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
actionType = actionType,
|
||||
balanceState = balanceState,
|
||||
confirmationState = StakingStates.ConfirmationState.Data(
|
||||
isPrimaryButtonEnabled = false,
|
||||
innerState = InnerConfirmationStakingState.ASSENT,
|
||||
feeState = FeeState.Loading,
|
||||
notifications = persistentListOf(),
|
||||
footerText = TextReference.EMPTY,
|
||||
transactionDoneState = TransactionDoneState.Empty,
|
||||
isApprovalNeeded = stakingApproval is StakingApproval.Needed,
|
||||
allowance = stakingAllowance,
|
||||
isAmountEditable = actionType == StakingActionCommonType.Enter ||
|
||||
actionType is StakingActionCommonType.Exit &&
|
||||
!actionType.partiallyUnstakeDisabled,
|
||||
reduceAmountBy = null,
|
||||
pendingAction = pendingAction,
|
||||
pendingActions = pendingActions.takeIf { isComposePendingActions },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isPartialUnstakeDisabled(state: StakingUiState): Boolean {
|
||||
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
|
||||
val isValidatorPreferred = balanceState?.validator?.preferred == true
|
||||
if (isSolana && !isValidatorPreferred) {
|
||||
return true
|
||||
}
|
||||
|
||||
val exitArgs = yieldArgs.exit ?: return false
|
||||
val exitAmount = exitArgs.args[Yield.Args.ArgType.AMOUNT] ?: return false
|
||||
val min = exitAmount.minimum ?: return false
|
||||
val max = exitAmount.maximum ?: return false
|
||||
return !min.isPositive() && !max.isPositive()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetConfirmationStateLoadingTransformer(
|
||||
private val yield: Yield,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = possibleConfirmationState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeState = FeeState.Loading,
|
||||
footerText = getFooter(prevState),
|
||||
) ?: prevState.confirmationState,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFooter(state: StakingUiState): TextReference {
|
||||
val amountState = state.amountState as? AmountState.Data
|
||||
|
||||
val isEnterAction = state.actionType == StakingActionCommonType.Enter
|
||||
|
||||
val amountDecimal = amountState?.amountTextField?.fiatAmount?.value
|
||||
val amountValue = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amountDecimal,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
val rewardSchedule = getRewardScheduleText(
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
networkId = cryptoCurrency.network.id.value,
|
||||
decapitalize = true,
|
||||
)
|
||||
return if (isEnterAction && amountDecimal != null && rewardSchedule != null) {
|
||||
resourceReference(
|
||||
id = R.string.staking_summary_description_text,
|
||||
formatArgs = wrappedList(amountValue, rewardSchedule),
|
||||
)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object SetConfirmationStateResetAssentTransformer : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmationState = prevState.confirmationState
|
||||
return prevState.copy(
|
||||
confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) {
|
||||
confirmationState.copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
innerState = InnerConfirmationStakingState.ASSENT,
|
||||
)
|
||||
} else {
|
||||
confirmationState
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.common.extensions.remove
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceItem
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.*
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
|
||||
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isPolkadot
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SetInitialDataStateTransformer(
|
||||
private val clickIntents: StakingClickIntents,
|
||||
private val yield: Yield,
|
||||
private val isAnyTokenStaked: Boolean,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val balancesToShowProvider: Provider<List<BalanceItem>>,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
|
||||
}
|
||||
|
||||
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
YieldBalancesConverter(
|
||||
cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider,
|
||||
balancesToShowProvider,
|
||||
yield,
|
||||
)
|
||||
}
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return prevState.copy(
|
||||
title = TextReference.EMPTY,
|
||||
cryptoCurrencyName = cryptoCurrency.name,
|
||||
cryptoCurrencySymbol = cryptoCurrency.symbol,
|
||||
cryptoCurrencyBlockchainId = cryptoCurrency.network.id.value,
|
||||
clickIntents = clickIntents,
|
||||
currentStep = StakingStep.InitialInfo,
|
||||
initialInfoState = createInitialInfoState(),
|
||||
amountState = createInitialAmountState(),
|
||||
confirmationState = StakingStates.ConfirmationState.Empty(),
|
||||
rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit),
|
||||
bottomSheetConfig = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createInitialInfoState(): StakingStates.InitialInfoState.Data {
|
||||
val yieldBalance = yieldBalancesConverter.convert(Unit)
|
||||
return StakingStates.InitialInfoState.Data(
|
||||
isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(),
|
||||
showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
|
||||
aprRange = getAprRange(yield.preferredValidators),
|
||||
infoItems = getInfoItems(),
|
||||
onInfoClick = clickIntents::onInfoClick,
|
||||
yieldBalance = yieldBalance,
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
onRefresh = { clickIntents.onRefreshSwipe(it.value) },
|
||||
isRefreshing = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInfoItems(): PersistentList<RoundedListWithDividersItemData> {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
return listOfNotNull(
|
||||
createAnnualPercentageRateItem(),
|
||||
createAvailableItem(cryptoCurrencyStatus),
|
||||
createUnbondingPeriodItem(),
|
||||
createMinimumRequirementItem(cryptoCurrencyStatus),
|
||||
createRewardClaimingItem(),
|
||||
createWarmupPeriodItem(),
|
||||
createRewardScheduleItem(),
|
||||
).toPersistentList()
|
||||
}
|
||||
|
||||
private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData {
|
||||
val validators = yield.preferredValidators
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_annual_percentage_rate,
|
||||
startText = TextReference.Res(R.string.staking_details_annual_percentage_rate),
|
||||
endText = getAprRange(validators),
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE) },
|
||||
isEndTextHighlighted = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAvailableItem(cryptoCurrencyStatus: CryptoCurrencyStatus): RoundedListWithDividersItemData {
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_available,
|
||||
startText = TextReference.Res(R.string.staking_details_available),
|
||||
endText = TextReference.Str(
|
||||
value = cryptoCurrencyStatus.value.amount.format { crypto(cryptoCurrencyStatus.currency) },
|
||||
),
|
||||
isEndTextHideable = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days ?: return null
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_unbonding_period,
|
||||
startText = TextReference.Res(R.string.staking_details_unbonding_period),
|
||||
endText = pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = cooldownPeriodDays,
|
||||
formatArgs = wrappedList(cooldownPeriodDays),
|
||||
),
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.UNBONDING_PERIOD) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMinimumRequirementItem(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): RoundedListWithDividersItemData? {
|
||||
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null
|
||||
if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null
|
||||
|
||||
val formattedAmount = minimumCryptoAmount.format { crypto(cryptoCurrencyStatus.currency) }
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_minimum_requirement,
|
||||
startText = TextReference.Res(R.string.staking_details_minimum_requirement),
|
||||
endText = TextReference.Str(formattedAmount),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRewardClaimingItem(): RoundedListWithDividersItemData? {
|
||||
val rewardClaiming = yield.metadata.rewardClaiming
|
||||
val endTextId = rewardClaimingResources[rewardClaiming] ?: return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_reward_claiming,
|
||||
startText = TextReference.Res(R.string.staking_details_reward_claiming),
|
||||
endText = TextReference.Res(endTextId),
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? {
|
||||
val warmupPeriodDays = yield.metadata.warmupPeriod.days
|
||||
if (warmupPeriodDays == 0) return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_warmup_period,
|
||||
startText = TextReference.Res(R.string.staking_details_warmup_period),
|
||||
endText = pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = warmupPeriodDays,
|
||||
formatArgs = wrappedList(warmupPeriodDays),
|
||||
),
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
|
||||
val endTextReference = getRewardScheduleText(
|
||||
rewardSchedule = yield.metadata.rewardSchedule,
|
||||
networkId = cryptoCurrencyStatusProvider().currency.network.id.value,
|
||||
decapitalize = false,
|
||||
) ?: return null
|
||||
|
||||
return RoundedListWithDividersItemData(
|
||||
id = R.string.staking_details_reward_schedule,
|
||||
startText = resourceReference(R.string.staking_details_reward_schedule),
|
||||
endText = endTextReference,
|
||||
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createInitialAmountState(): AmountState {
|
||||
val cryptoBalanceValue = cryptoCurrencyStatusProvider().value
|
||||
val maxEnterAmount = EnterAmountBoundary(
|
||||
amount = cryptoBalanceValue.amount,
|
||||
fiatAmount = cryptoBalanceValue.fiatAmount,
|
||||
fiatRate = cryptoBalanceValue.fiatRate,
|
||||
)
|
||||
return AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
iconStateConverter = iconStateConverter,
|
||||
maxEnterAmount = maxEnterAmount,
|
||||
).convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWalletProvider().name),
|
||||
value = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAprRange(validators: List<Yield.Validator>): TextReference {
|
||||
val aprValues = validators
|
||||
.filter { it.preferred }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.mapNotNull { it.apr }
|
||||
?: validators.mapNotNull { it.apr }
|
||||
|
||||
val minApr = aprValues.min()
|
||||
val maxApr = aprValues.max()
|
||||
|
||||
val formattedMinApr = minApr.format { percent() }.remove("%")
|
||||
val formattedMaxApr = maxApr.format { percent() }
|
||||
|
||||
if (maxApr - minApr < EQUALITY_THRESHOLD) {
|
||||
return stringReference("$formattedMinApr%")
|
||||
}
|
||||
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val EQUALITY_THRESHOLD = BigDecimal(1E-10)
|
||||
|
||||
val rewardClaimingResources = mapOf(
|
||||
Yield.Metadata.RewardClaiming.MANUAL to R.string.staking_reward_claiming_manual,
|
||||
Yield.Metadata.RewardClaiming.AUTO to R.string.staking_reward_claiming_auto,
|
||||
Yield.Metadata.RewardSchedule.UNKNOWN to null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetInitialLoadingStateTransformer(
|
||||
private val isRefreshing: Boolean,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val initialState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data
|
||||
return prevState.copy(
|
||||
initialInfoState = initialState?.copy(
|
||||
pullToRefreshConfig = prevState.initialInfoState.pullToRefreshConfig.copy(
|
||||
isRefreshing = isRefreshing,
|
||||
),
|
||||
) ?: prevState.initialInfoState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.isNullOrEmpty
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object SetTitleTransformer : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val actionType = prevState.actionType
|
||||
val currentStep = prevState.currentStep
|
||||
|
||||
val title = when (currentStep) {
|
||||
StakingStep.Amount -> resourceReference(R.string.send_amount_label)
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.Validators,
|
||||
-> resourceReference(R.string.staking_validators)
|
||||
StakingStep.RewardsValidators -> resourceReference(R.string.common_claim_rewards)
|
||||
StakingStep.InitialInfo -> resourceReference(
|
||||
R.string.staking_title_stake,
|
||||
wrappedList(prevState.cryptoCurrencyName),
|
||||
)
|
||||
|
||||
StakingStep.Confirmation -> {
|
||||
when (actionType) {
|
||||
StakingActionCommonType.Enter -> resourceReference(
|
||||
R.string.staking_title_stake,
|
||||
wrappedList(prevState.cryptoCurrencyName),
|
||||
)
|
||||
is StakingActionCommonType.Exit -> resourceReference(
|
||||
R.string.staking_title_unstake,
|
||||
wrappedList(prevState.cryptoCurrencyName),
|
||||
)
|
||||
else -> {
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val title = confirmationState?.pendingAction?.type?.getPendingActionTitle()
|
||||
|
||||
title.takeIf { !it.isNullOrEmpty() }
|
||||
?: resourceReference(
|
||||
id = R.string.staking_title_stake,
|
||||
formatArgs = wrappedList(prevState.cryptoCurrencyName),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.Enter) {
|
||||
stringReference(prevState.walletName)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal class ShowActionSelectorBottomSheetTransformer(
|
||||
private val pendingActions: ImmutableList<PendingAction>,
|
||||
private val onActionSelect: (PendingAction) -> Unit,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = StakingActionSelectionBottomSheetConfig(
|
||||
title = resourceReference(R.string.common_choose_action),
|
||||
actions = pendingActions,
|
||||
onActionSelect = onActionSelect,
|
||||
),
|
||||
onDismissRequest = onDismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ShowInfoBottomSheetStateTransformer(
|
||||
private val infoType: InfoType,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
onDismissRequest = onDismiss,
|
||||
isShown = true,
|
||||
content = when (infoType) {
|
||||
InfoType.ANNUAL_PERCENTAGE_RATE -> StakingInfoBottomSheetConfig(
|
||||
title = resourceReference(R.string.staking_details_annual_percentage_rate),
|
||||
text = resourceReference(R.string.staking_details_annual_percentage_rate_info),
|
||||
)
|
||||
InfoType.UNBONDING_PERIOD -> StakingInfoBottomSheetConfig(
|
||||
title = resourceReference(R.string.staking_details_unbonding_period),
|
||||
text = resourceReference(R.string.staking_details_unbonding_period_info),
|
||||
)
|
||||
InfoType.REWARD_CLAIMING -> StakingInfoBottomSheetConfig(
|
||||
title = resourceReference(R.string.staking_details_reward_claiming),
|
||||
text = resourceReference(R.string.staking_details_reward_claiming_info),
|
||||
)
|
||||
InfoType.WARMUP_PERIOD -> StakingInfoBottomSheetConfig(
|
||||
title = resourceReference(R.string.staking_details_warmup_period),
|
||||
text = resourceReference(R.string.staking_details_warmup_period_info),
|
||||
)
|
||||
InfoType.REWARD_SCHEDULE -> StakingInfoBottomSheetConfig(
|
||||
title = resourceReference(R.string.staking_details_reward_schedule),
|
||||
text = resourceReference(R.string.staking_details_reward_schedule_info),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class AmountChangeStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: String,
|
||||
private val yield: Yield,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val actionType = prevState.actionType
|
||||
val maxEnterAmount = if (actionType is StakingActionCommonType.Exit) {
|
||||
EnterAmountBoundary(
|
||||
amount = prevState.balanceState?.cryptoAmount,
|
||||
fiatAmount = prevState.balanceState?.fiatAmount,
|
||||
fiatRate = cryptoCurrencyStatus.value.fiatRate,
|
||||
)
|
||||
} else {
|
||||
maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
val updatedAmountState = AmountFieldChangeTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxEnterAmount = maxEnterAmount,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = value,
|
||||
).transform(prevState.amountState)
|
||||
|
||||
return prevState.copy(
|
||||
amountState = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
yield = yield,
|
||||
actionType = prevState.actionType,
|
||||
).transform(updatedAmountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class AmountCurrencyChangeStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val value: Boolean,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class AmountMaxValueStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val actionType: StakingActionCommonType,
|
||||
private val yield: Yield,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val maxEnterAmount = if (actionType is StakingActionCommonType.Exit) {
|
||||
EnterAmountBoundary(
|
||||
amount = prevState.balanceState?.cryptoAmount,
|
||||
fiatAmount = prevState.balanceState?.fiatAmount,
|
||||
fiatRate = cryptoCurrencyStatus.value.fiatRate,
|
||||
)
|
||||
} else {
|
||||
maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
val updatedAmountState = AmountFieldSetMaxAmountTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = maxEnterAmount,
|
||||
minAmount = minimumTransactionAmount,
|
||||
).transform(prevState.amountState)
|
||||
return prevState.copy(
|
||||
amountState = AmountRequirementStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
yield = yield,
|
||||
actionType = prevState.actionType,
|
||||
).transform(updatedAmountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class AmountPasteDismissStateTransformer : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class AmountReduceByStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: ReduceByData,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountReduceByTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = value,
|
||||
).transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class AmountReduceToStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val minimumTransactionAmount: EnterAmountBoundary?,
|
||||
private val value: BigDecimal,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
amountState = AmountReduceToTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = value,
|
||||
).transform(prevState.amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class AmountRequirementStateTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val yield: Yield,
|
||||
private val actionType: StakingActionCommonType,
|
||||
) : Transformer<AmountState> {
|
||||
override fun transform(prevState: AmountState): AmountState {
|
||||
return if (prevState is AmountState.Data) {
|
||||
updateWithError(
|
||||
prevState,
|
||||
actionType,
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWithError(amountState: AmountState.Data, actionType: StakingActionCommonType): AmountState {
|
||||
val requirementError = getRequirementError(amountState)
|
||||
val isIntegerOnlyError = isIntegerOnlyError(amountState, actionType)
|
||||
|
||||
val cryptoAmount = amountState.amountTextField.cryptoAmount
|
||||
val roundedDownCrypto = cryptoAmount.value
|
||||
?.setScale(0, RoundingMode.DOWN)
|
||||
?.parseBigDecimal(0)
|
||||
.orEmpty()
|
||||
|
||||
val isAmountZeroOrNull = if (amountState.amountTextField.isFiatValue) {
|
||||
amountState.amountTextField.fiatAmount.value.isNullOrZero()
|
||||
} else {
|
||||
amountState.amountTextField.cryptoAmount.value.isNullOrZero()
|
||||
}
|
||||
|
||||
val errorText = when {
|
||||
amountState.amountTextField.isError -> amountState.amountTextField.error
|
||||
requirementError != null -> requirementError
|
||||
isIntegerOnlyError -> when (actionType) {
|
||||
StakingActionCommonType.Enter -> resourceReference(
|
||||
R.string.staking_amount_tron_integer_error,
|
||||
wrappedList(roundedDownCrypto),
|
||||
)
|
||||
is StakingActionCommonType.Exit -> resourceReference(
|
||||
R.string.staking_amount_tron_integer_error_unstaking,
|
||||
wrappedList(roundedDownCrypto),
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
val isError = amountState.amountTextField.isError || requirementError != null
|
||||
return amountState.copy(
|
||||
isPrimaryButtonEnabled = !isAmountZeroOrNull && !isError,
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
isError = isError,
|
||||
isWarning = isIntegerOnlyError,
|
||||
error = errorText ?: amountState.amountTextField.error,
|
||||
keyboardOptions = amountState.amountTextField.keyboardOptions.copy(
|
||||
imeAction = ImeAction.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRequirementError(prevState: AmountState.Data): TextReference? {
|
||||
val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return null
|
||||
|
||||
val isAlreadyErrorState = prevState.amountTextField.isError
|
||||
val isAmountZero = amountDecimal.isZero()
|
||||
|
||||
if (isAlreadyErrorState || isAmountZero) return null
|
||||
|
||||
return when (actionType) {
|
||||
StakingActionCommonType.Enter -> {
|
||||
val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
|
||||
enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error)
|
||||
}
|
||||
is StakingActionCommonType.Exit -> {
|
||||
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT)
|
||||
exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean {
|
||||
val cryptoAmountValue = amountState.amountTextField.cryptoAmount.value ?: return false
|
||||
|
||||
val isEnterOrExit = actionType == StakingActionCommonType.Enter || actionType is StakingActionCommonType.Exit
|
||||
val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value)
|
||||
|
||||
val isIntegerOnly = cryptoAmountValue.isZero() || cryptoAmountValue.remainder(BigDecimal.ONE).isZero()
|
||||
|
||||
return isEnterOrExit && isTron && !isIntegerOnly
|
||||
}
|
||||
|
||||
private fun AddressArgument.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? {
|
||||
val isExceedsRequirements = maximum?.compareTo(amount) == -1 ||
|
||||
minimum?.compareTo(amount) == 1
|
||||
|
||||
return resourceReference(
|
||||
errorTextRes,
|
||||
wrappedList(
|
||||
minimum.format {
|
||||
crypto(cryptoCurrencyStatus.currency)
|
||||
},
|
||||
),
|
||||
).takeIf { required && isExceedsRequirements }
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val amountState: AmountState,
|
||||
val actionType: StakingActionCommonType,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class AmountRoundToIntegerTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
|
||||
val amountTextField = amountState.amountTextField
|
||||
if (amountTextField.value.isEmpty()) return prevState
|
||||
|
||||
val cryptoAmount = amountState.amountTextField.cryptoAmount
|
||||
val fiatAmount = amountState.amountTextField.fiatAmount
|
||||
val fiatDecimals = fiatAmount.decimals
|
||||
|
||||
val roundedDownCrypto = cryptoAmount.value?.setScale(0, RoundingMode.DOWN)
|
||||
val roundedDownFiat = roundedDownCrypto?.multiply(cryptoCurrencyStatus.value.fiatRate)
|
||||
|
||||
val value = roundedDownCrypto?.parseBigDecimal(0).orEmpty()
|
||||
val fiatValue = roundedDownFiat?.parseBigDecimal(fiatDecimals, RoundingMode.HALF_UP).orEmpty()
|
||||
|
||||
return prevState.copy(
|
||||
amountState = amountState.copy(
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
cryptoAmount = cryptoAmount.copy(value = roundedDownCrypto),
|
||||
fiatAmount = fiatAmount.copy(value = roundedDownFiat),
|
||||
value = value,
|
||||
fiatValue = fiatValue,
|
||||
isWarning = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.approval
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetApprovalBottomSheetInProgressTransformer(
|
||||
private val onDismiss: () -> Unit,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val approvalBottomSheetConfig = prevState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
|
||||
return prevState.copy(
|
||||
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
|
||||
onDismissRequest = onDismiss,
|
||||
isShown = true,
|
||||
content = approvalBottomSheetConfig?.let { config ->
|
||||
config.copy(
|
||||
data = config.data.copy(
|
||||
approveButton = config.data.approveButton.copy(
|
||||
enabled = false,
|
||||
loading = true,
|
||||
),
|
||||
cancelButton = config.data.cancelButton.copy(
|
||||
enabled = false,
|
||||
),
|
||||
),
|
||||
onCancel = onDismiss,
|
||||
)
|
||||
} as TangemBottomSheetConfigContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.approval
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetApprovalBottomSheetTypeChangeTransformer(
|
||||
private val approveType: ApproveType,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val approvalBottomSheetConfig = prevState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
|
||||
|
||||
return prevState.copy(
|
||||
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
|
||||
content = approvalBottomSheetConfig?.copy(
|
||||
data = approvalBottomSheetConfig.data.copy(approveType = approveType),
|
||||
) as TangemBottomSheetConfigContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.approval
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal object SetApprovalInProgressTransformer : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val state = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val notifications = state?.notifications?.toMutableList() ?: mutableListOf()
|
||||
|
||||
notifications.add(
|
||||
StakingNotification.Warning.TransactionInProgress(
|
||||
title = resourceReference(R.string.warning_approval_in_progress_title),
|
||||
description = resourceReference(R.string.warning_approval_in_progress_message),
|
||||
),
|
||||
)
|
||||
|
||||
val updatedConfirmationState = state?.copy(
|
||||
notifications = notifications.toPersistentList(),
|
||||
isPrimaryButtonEnabled = false,
|
||||
) ?: prevState.confirmationState
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = updatedConfirmationState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.approval
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SetConfirmationStateAssentApprovalTransformer(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
private val fee: TransactionFee,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
return prevState.copy(
|
||||
confirmationState = prevState.confirmationState.copyWrapped(),
|
||||
bottomSheetConfig = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
|
||||
return if (this is StakingStates.ConfirmationState.Data) {
|
||||
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
|
||||
copy(
|
||||
innerState = InnerConfirmationStakingState.ASSENT,
|
||||
feeState = FeeState.Content(
|
||||
fee = fee.normal,
|
||||
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
|
||||
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
isFeeApproximate = false,
|
||||
),
|
||||
isPrimaryButtonEnabled = true,
|
||||
isApprovalNeeded = true,
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.approval
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.*
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ShowApprovalBottomSheetTransformer(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val cryptoCurrencyValue = cryptoCurrencyStatusProvider().value
|
||||
|
||||
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
|
||||
val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data ?: return prevState
|
||||
val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState
|
||||
val fee = feeState.fee ?: return prevState
|
||||
|
||||
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
|
||||
val validatorAddress = validatorState.chosenValidator.address
|
||||
val feeCryptoValue = fee.amount.value.format {
|
||||
crypto(fee.amount.currencySymbol, fee.amount.decimals)
|
||||
}
|
||||
val feeFiatValue = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value),
|
||||
fiatCurrencyCode = appCurrencyProvider().code,
|
||||
fiatCurrencySymbol = appCurrencyProvider().symbol,
|
||||
)
|
||||
return prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onDismiss,
|
||||
content = GiveTxPermissionBottomSheetConfig(
|
||||
data = GiveTxPermissionState.ReadyForRequest(
|
||||
currency = cryptoCurrency.symbol,
|
||||
amount = amountState.amountTextField.value,
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
walletAddress = walletAddress,
|
||||
spenderAddress = validatorAddress,
|
||||
fee = resourceReference(
|
||||
R.string.common_crypto_fiat_format,
|
||||
wrappedList(feeCryptoValue, feeFiatValue),
|
||||
),
|
||||
approveButton = ApprovePermissionButton(
|
||||
enabled = true,
|
||||
loading = false,
|
||||
onClick = prevState.clickIntents::onApprovalClick,
|
||||
),
|
||||
cancelButton = CancelPermissionButton(
|
||||
enabled = true,
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.give_permission_staking_subtitle,
|
||||
formatArgs = wrappedList(cryptoCurrency.symbol),
|
||||
),
|
||||
dialogText = resourceReference(R.string.give_permission_staking_footer),
|
||||
footerText = resourceReference(R.string.staking_give_permission_fee_footer),
|
||||
onChangeApproveType = prevState.clickIntents::onApproveTypeChange,
|
||||
),
|
||||
onCancel = onDismiss,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.confirmation
|
||||
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SetUpdatedAllowanceTransformer(
|
||||
private val allowance: BigDecimal,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
return prevState.copy(
|
||||
confirmationState = confirmationState?.copy(allowance = allowance) ?: prevState.confirmationState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.notifications
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddStakingNotificationsTransformer(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
private val currencyWarning: CryptoCurrencyWarning?,
|
||||
private val validatorError: Throwable?,
|
||||
private val feeError: GetFeeError?,
|
||||
private val currencyCheck: CryptoCurrencyCheck,
|
||||
private val isSubtractAvailable: Boolean,
|
||||
private val yield: Yield,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
yield = yield,
|
||||
isSubtractAvailable = isSubtractAvailable,
|
||||
)
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
|
||||
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
|
||||
val feeState = confirmationState.feeState as? FeeState.Content
|
||||
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||
val feeValue = feeState?.fee?.amount?.value.orZero()
|
||||
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
|
||||
|
||||
val isEnterAction = prevState.actionType == StakingActionCommonType.Enter
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
balance = balance,
|
||||
isSubtractAvailable = isSubtractAvailable,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
|
||||
val sendingAmount = if (isEnterAction) {
|
||||
checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
).max(minimumRequirement)
|
||||
} else {
|
||||
// No amount is taken from account balance on exit or pending actions
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
|
||||
val notifications = buildList {
|
||||
// errors
|
||||
addErrorNotifications(
|
||||
prevState = prevState,
|
||||
feeError = feeError,
|
||||
sendingAmount = sendingAmount,
|
||||
onReload = prevState.clickIntents::getFee,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
// warnings
|
||||
addWarningNotifications(
|
||||
prevState = prevState,
|
||||
amountState = amountState,
|
||||
feeState = feeState,
|
||||
sendingAmount = sendingAmount,
|
||||
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
|
||||
)
|
||||
|
||||
stakingInfoNotificationsFactory.addInfoNotifications(
|
||||
notifications = this,
|
||||
prevState = prevState,
|
||||
sendingAmount = sendingAmount,
|
||||
actionAmount = amountValue,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = confirmationState.copy(
|
||||
notifications = notifications.toImmutableList(),
|
||||
isPrimaryButtonEnabled = notifications.none {
|
||||
it is StakingNotification.Error ||
|
||||
it is NotificationUM.Error ||
|
||||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
|
||||
it is StakingNotification.Warning.TransactionInProgress
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addErrorNotifications(
|
||||
prevState: StakingUiState,
|
||||
onReload: () -> Unit,
|
||||
feeError: GetFeeError?,
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val network = cryptoCurrency.network
|
||||
|
||||
addFeeUnreachableNotification(
|
||||
feeError = feeError,
|
||||
tokenName = cryptoCurrencyStatusProvider().currency.name,
|
||||
onReload = onReload,
|
||||
)
|
||||
addStakeExceedBalanceNotification(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
actionType = prevState.actionType,
|
||||
isSubtractionAvailable = isSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onClick = prevState.clickIntents::openTokenDetails,
|
||||
)
|
||||
addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning = currencyWarning,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId),
|
||||
onClick = prevState.clickIntents::openTokenDetails,
|
||||
onAnalyticsEvent = { /* no-op */ },
|
||||
)
|
||||
if (!BlockchainUtils.isCardano(network.id.value)) {
|
||||
addDustWarningNotification(
|
||||
dustValue = currencyCheck.dustValue,
|
||||
feeValue = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
addTransactionLimitErrorNotification(
|
||||
currencyCheck = currencyCheck,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
feeValue = feeValue,
|
||||
onReduceClick = prevState.clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
addReserveAmountErrorNotification(
|
||||
reserveAmount = currencyCheck.reserveAmount,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
isAccountFunded = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addWarningNotifications(
|
||||
prevState: StakingUiState,
|
||||
amountState: AmountState.Data,
|
||||
feeState: FeeState.Content?,
|
||||
sendingAmount: BigDecimal,
|
||||
isFeeCoverage: Boolean,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
addRentExemptionNotification(
|
||||
rentWarning = currencyCheck.rentWarning,
|
||||
)
|
||||
|
||||
addExistentialWarningNotification(
|
||||
existentialDeposit = currencyCheck.existentialDeposit,
|
||||
feeAmount = feeState?.fee?.amount?.value.orZero(),
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onReduceClick = prevState.clickIntents::onAmountReduceByClick,
|
||||
)
|
||||
addFeeCoverageNotification(
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
amountField = amountState.amountTextField,
|
||||
sendingValue = sendingAmount,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
// blockchain specific
|
||||
addValidateTransactionNotifications(
|
||||
dustValue = currencyCheck.dustValue.orZero(),
|
||||
minAdaValue = (feeState?.fee as? Fee.CardanoToken)?.minAdaValue,
|
||||
validationError = validatorError,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
onReduceClick = prevState.clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addStakeExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
sendingAmount: BigDecimal,
|
||||
actionType: StakingActionCommonType,
|
||||
isSubtractionAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
onClick: (CryptoCurrency) -> Unit,
|
||||
) {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (!isSubtractionAvailable) return
|
||||
|
||||
val showNotification = sendingAmount + feeAmount > balance
|
||||
if (showNotification) {
|
||||
val notification = if (actionType == StakingActionCommonType.Enter) {
|
||||
NotificationUM.Error.TotalExceedsBalance
|
||||
} else {
|
||||
with(cryptoCurrencyStatus.currency) {
|
||||
NotificationUM.Error.ExceedsBalance(
|
||||
networkIconId = networkIconResId,
|
||||
networkName = name,
|
||||
currencyName = name,
|
||||
feeName = name,
|
||||
feeSymbol = symbol,
|
||||
mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId),
|
||||
onClick = { onClick(this) },
|
||||
)
|
||||
}
|
||||
}
|
||||
add(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.notifications
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class DismissStakingNotificationsStateTransformer(
|
||||
private val notification: Class<out NotificationUM>,
|
||||
) : Transformer<StakingUiState> {
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val updatedNotifications = confirmationState?.notifications
|
||||
?.filterNot { it::class == notification }?.toPersistentList()
|
||||
?: persistentListOf()
|
||||
|
||||
return prevState.copy(
|
||||
confirmationState = confirmationState?.copy(
|
||||
notifications = updatedNotifications,
|
||||
) ?: StakingStates.ConfirmationState.Empty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.notifications
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.extensions.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.YieldBalance
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class StakingInfoNotificationsFactory(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val yield: Yield,
|
||||
private val isSubtractAvailable: Boolean,
|
||||
) {
|
||||
|
||||
/**
|
||||
* @param notifications current notification to display
|
||||
* @param prevState current screen state to update
|
||||
* @param sendingAmount amount being transferred from user account
|
||||
* @param actionAmount any amount being transferred or used action
|
||||
* @param feeValue fee amount payed from user account
|
||||
*/
|
||||
fun addInfoNotifications(
|
||||
notifications: MutableList<NotificationUM>,
|
||||
prevState: StakingUiState,
|
||||
sendingAmount: BigDecimal,
|
||||
actionAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
) = with(notifications) {
|
||||
addStakingLowBalanceNotification(prevState, actionAmount)
|
||||
|
||||
when (prevState.actionType) {
|
||||
StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue)
|
||||
is StakingActionCommonType.Exit -> addExitInfoNotifications()
|
||||
is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addExitInfoNotifications() {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
|
||||
if (cooldownPeriodDays != null) {
|
||||
add(
|
||||
StakingNotification.Info.Unstake(
|
||||
cooldownPeriodDays = cooldownPeriodDays,
|
||||
subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) {
|
||||
R.string.staking_notification_unstake_cosmos_text
|
||||
} else {
|
||||
R.string.staking_notification_unstake_text
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addEnterInfoNotifications(
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
) {
|
||||
addTronRevoteNotification()
|
||||
addStakingEntireBalanceNotification(sendingAmount, feeValue)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addPendingInfoNotifications(prevState: StakingUiState) {
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
val pendingActionType = confirmationState?.pendingAction?.type
|
||||
val (titleReference, textReference) = when (pendingActionType) {
|
||||
StakingActionType.CLAIM_REWARDS -> {
|
||||
resourceReference(R.string.common_claim) to
|
||||
resourceReference(R.string.staking_notification_claim_rewards_text)
|
||||
}
|
||||
StakingActionType.RESTAKE_REWARDS -> {
|
||||
resourceReference(R.string.staking_restake) to
|
||||
resourceReference(R.string.staking_notification_restake_rewards_text)
|
||||
}
|
||||
StakingActionType.CLAIM_UNSTAKED,
|
||||
StakingActionType.WITHDRAW,
|
||||
-> {
|
||||
resourceReference(R.string.staking_withdraw) to
|
||||
resourceReference(R.string.staking_notification_withdraw_text)
|
||||
}
|
||||
StakingActionType.UNLOCK_LOCKED -> {
|
||||
val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days
|
||||
if (cooldownPeriodDays != null) {
|
||||
resourceReference(R.string.staking_unlocked_locked) to resourceReference(
|
||||
R.string.staking_notification_unlock_text,
|
||||
wrappedList(
|
||||
pluralReference(
|
||||
id = R.plurals.common_days,
|
||||
count = cooldownPeriodDays,
|
||||
formatArgs = wrappedList(cooldownPeriodDays),
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
}
|
||||
StakingActionType.VOTE_LOCKED -> {
|
||||
resourceReference(R.string.staking_revote) to
|
||||
resourceReference(R.string.staking_notifications_revote_tron_text)
|
||||
}
|
||||
StakingActionType.RESTAKE -> {
|
||||
resourceReference(R.string.staking_restake) to
|
||||
resourceReference(R.string.staking_notification_restake_text)
|
||||
}
|
||||
else -> null to null
|
||||
}
|
||||
|
||||
if (titleReference != null && textReference != null) {
|
||||
add(
|
||||
StakingNotification.Info.Ordinary(
|
||||
title = titleReference,
|
||||
text = textReference,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTronRevoteNotification() {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value)
|
||||
val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance
|
||||
?.items?.any {
|
||||
it.type == BalanceType.PREPARING ||
|
||||
it.type == BalanceType.STAKED ||
|
||||
it.type == BalanceType.LOCKED
|
||||
} == true
|
||||
if (isTron && hasStakedBalance) {
|
||||
add(
|
||||
StakingNotification.Info.Ordinary(
|
||||
title = resourceReference(R.string.staking_revote),
|
||||
text = resourceReference(R.string.staking_notifications_revote_tron_text),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addStakingEntireBalanceNotification(
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
|
||||
val isEntireBalance = sendingAmount.plus(feeValue) == balance
|
||||
|
||||
if (isEntireBalance && isSubtractAvailable) {
|
||||
add(StakingNotification.Info.StakeEntireBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addStakingLowBalanceNotification(
|
||||
prevState: StakingUiState,
|
||||
actionAmount: BigDecimal,
|
||||
) {
|
||||
if (prevState.actionType !is StakingActionCommonType.Exit) return
|
||||
|
||||
val maxAmount = prevState.balanceState?.cryptoAmount ?: return
|
||||
val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return
|
||||
|
||||
val amountLeft = maxAmount - actionAmount
|
||||
val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero()
|
||||
|
||||
if (exitRequirements.required && isNotEnoughLeft) {
|
||||
add(StakingNotification.Warning.LowStakedBalance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.transformers.validator
|
||||
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ValidatorSelectChangeTransformer(
|
||||
private val yield: Yield,
|
||||
private val selectedValidator: Yield.Validator?,
|
||||
) : Transformer<StakingUiState> {
|
||||
|
||||
override fun transform(prevState: StakingUiState): StakingUiState {
|
||||
val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data
|
||||
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
val isRestake = prevState.actionType == StakingActionCommonType.Pending.Restake
|
||||
val isEnter = prevState.actionType == StakingActionCommonType.Enter
|
||||
val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo
|
||||
val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED
|
||||
|
||||
val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake }
|
||||
?: validatorState?.activeValidator
|
||||
val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator }
|
||||
|
||||
val selectedValidator = if (isRestake && isFromInfoScreen) {
|
||||
filteredValidators.firstOrNull()
|
||||
} else {
|
||||
selectedValidator
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
validatorState = StakingStates.ValidatorState.Data(
|
||||
chosenValidator = selectedValidator ?: yield.preferredValidators.first(),
|
||||
availableValidators = filteredValidators,
|
||||
isPrimaryButtonEnabled = true,
|
||||
isClickable = true,
|
||||
activeValidator = activeValidator,
|
||||
isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Check and calculates subtracted amount
|
||||
*/
|
||||
internal fun checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value)
|
||||
val feeValueRounded = if (isTron) {
|
||||
feeValue.round(MathContext(0, RoundingMode.UP))
|
||||
} else {
|
||||
feeValue
|
||||
}
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValueRounded,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
return if (isFeeCoverage) {
|
||||
val reducedAmount = balance.minus(reduceAmountBy).minus(feeValueRounded)
|
||||
if (isTron(cryptoCurrencyStatus.currency.network.id.value)) {
|
||||
reducedAmount.setScale(0, RoundingMode.DOWN)
|
||||
} else {
|
||||
reducedAmount
|
||||
}
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sending amount with fee is greater than balance
|
||||
*/
|
||||
internal fun checkFeeCoverage(
|
||||
isSubtractAvailable: Boolean,
|
||||
balance: BigDecimal,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
): Boolean {
|
||||
if (!isSubtractAvailable) return false
|
||||
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
|
||||
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isBSC
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) {
|
||||
StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards)
|
||||
StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards)
|
||||
StakingActionType.CLAIM_UNSTAKED,
|
||||
StakingActionType.WITHDRAW,
|
||||
-> resourceReference(R.string.staking_withdraw)
|
||||
StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake)
|
||||
StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked)
|
||||
StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked)
|
||||
StakingActionType.VOTE -> resourceReference(R.string.staking_vote)
|
||||
StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke)
|
||||
StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked)
|
||||
StakingActionType.REVOTE -> resourceReference(R.string.staking_revote)
|
||||
StakingActionType.REBOND -> resourceReference(R.string.staking_rebond)
|
||||
StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate)
|
||||
StakingActionType.STAKE -> resourceReference(R.string.common_stake)
|
||||
StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake)
|
||||
StakingActionType.UNKNOWN -> TextReference.EMPTY
|
||||
null -> TextReference.EMPTY
|
||||
}
|
||||
|
||||
internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean {
|
||||
val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions
|
||||
val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions)
|
||||
val isBscRestake = isBSC(networkId) && activeStake.pendingActions.any {
|
||||
it.type == StakingActionType.RESTAKE
|
||||
}
|
||||
|
||||
return isSingleAction && !isBscRestake || isCompositePendingActions
|
||||
}
|
||||
|
||||
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isBSC(networkId)) {
|
||||
activeStake.pendingActions.plus(
|
||||
PendingAction(
|
||||
type = StakingActionType.UNSTAKE,
|
||||
passthrough = "",
|
||||
args = null,
|
||||
),
|
||||
).toPersistentList()
|
||||
} else {
|
||||
activeStake.pendingActions
|
||||
}
|
||||
|
||||
internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean {
|
||||
return isTron(networkId) && pendingAction?.type == StakingActionType.REVOTE
|
||||
}
|
||||
|
||||
internal fun isCompositePendingActions(networkId: String, pendingActions: ImmutableList<PendingAction>?): Boolean {
|
||||
return when {
|
||||
isSolana(networkId) -> pendingActions?.any { it.type == StakingActionType.WITHDRAW } == true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.utils
|
||||
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.COSMOS_SCHEDULE
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.StakingRewardSchedule.SOLANA_SCHEDULE
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
|
||||
|
||||
private data object StakingRewardSchedule {
|
||||
val COSMOS_SCHEDULE = 5 to 12
|
||||
val SOLANA_SCHEDULE = 2 to 3
|
||||
}
|
||||
|
||||
internal fun getRewardScheduleText(
|
||||
rewardSchedule: Yield.Metadata.RewardSchedule,
|
||||
networkId: String,
|
||||
decapitalize: Boolean,
|
||||
): TextReference? {
|
||||
return when (rewardSchedule) {
|
||||
Yield.Metadata.RewardSchedule.WEEK -> resourceReference(
|
||||
id = R.string.staking_reward_schedule_week,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
Yield.Metadata.RewardSchedule.HOUR -> resourceReference(
|
||||
id = R.string.staking_reward_schedule_hour,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
Yield.Metadata.RewardSchedule.DAY -> resourceReference(
|
||||
id = R.string.staking_reward_schedule_day,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
Yield.Metadata.RewardSchedule.MONTH -> resourceReference(
|
||||
id = R.string.staking_reward_schedule_month,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
Yield.Metadata.RewardSchedule.BLOCK,
|
||||
Yield.Metadata.RewardSchedule.EPOCH,
|
||||
Yield.Metadata.RewardSchedule.ERA,
|
||||
-> getCustomRewardSchedule(
|
||||
networkId = networkId,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomRewardSchedule(networkId: String, decapitalize: Boolean = false): TextReference? {
|
||||
return when {
|
||||
isSolana(networkId) -> {
|
||||
combinedReference(
|
||||
resourceReference(id = R.string.staking_reward_schedule_each_plural, decapitalize = decapitalize),
|
||||
stringReference(NON_BREAKING_SPACE.toString()),
|
||||
stringReference("${SOLANA_SCHEDULE.first}$MINUS${SOLANA_SCHEDULE.second}$NON_BREAKING_SPACE"),
|
||||
pluralReference(
|
||||
id = R.plurals.common_days_no_param,
|
||||
count = SOLANA_SCHEDULE.second,
|
||||
),
|
||||
)
|
||||
}
|
||||
isCosmos(networkId) -> {
|
||||
combinedReference(
|
||||
resourceReference(id = R.string.staking_reward_schedule_each_plural, decapitalize = decapitalize),
|
||||
stringReference(NON_BREAKING_SPACE.toString()),
|
||||
stringReference("${COSMOS_SCHEDULE.first}$MINUS${COSMOS_SCHEDULE.second}$NON_BREAKING_SPACE"),
|
||||
resourceReference(R.string.common_second_no_param),
|
||||
)
|
||||
}
|
||||
isTron(networkId) -> resourceReference(id = R.string.staking_reward_schedule_day, decapitalize = decapitalize)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
||||
@Composable
|
||||
internal fun StakingClaimRewardsValidatorContent(
|
||||
state: StakingStates.RewardsValidatorsState,
|
||||
clickIntents: StakingClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state !is StakingStates.RewardsValidatorsState.Data) return
|
||||
Column(
|
||||
modifier = Modifier // Do not put fillMaxSize() in here
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
state.rewards.forEachIndexed { index, item ->
|
||||
key(item.title.resolveReference() + index) {
|
||||
InputRowImageInfo(
|
||||
subtitle = item.title,
|
||||
caption = combinedReference(
|
||||
resourceReference(R.string.staking_details_apr),
|
||||
annotatedReference {
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = item.validator?.apr.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
),
|
||||
infoTitle = item.formattedFiatAmount,
|
||||
infoSubtitle = item.formattedCryptoAmount,
|
||||
imageUrl = item.validator?.image.orEmpty(),
|
||||
onImageError = { ValidatorImagePlaceholder() },
|
||||
modifier = modifier
|
||||
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
enabled = item.pendingActions.isNotEmpty(),
|
||||
onClick = {
|
||||
clickIntents.onActiveStake(item)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.TransactionDoneState
|
||||
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
|
||||
import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData
|
||||
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
|
||||
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
|
||||
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
|
||||
import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun StakingConfirmationContent(
|
||||
amountState: AmountState,
|
||||
state: StakingStates.ConfirmationState,
|
||||
validatorState: StakingStates.ValidatorState,
|
||||
clickIntents: StakingClickIntents,
|
||||
) {
|
||||
if (state !is StakingStates.ConfirmationState.Data) return
|
||||
val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED
|
||||
val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
val doneState = state.transactionDoneState
|
||||
AnimatedVisibility(
|
||||
visible = doneState is TransactionDoneState.Content,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.common_in_progress),
|
||||
subtitle = resourceReference(R.string.staking_transaction_in_progress_text),
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress,
|
||||
isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED,
|
||||
onClick = clickIntents::onPrevClick,
|
||||
)
|
||||
ValidatorBlock(
|
||||
validatorState = validatorState,
|
||||
isClickable = !isTransactionInProgress,
|
||||
onClick = clickIntents::openValidators,
|
||||
)
|
||||
StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent)
|
||||
NotificationsBlock(notifications = state.notifications)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_StakingConfirmationContent() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
StakingConfirmationContent(
|
||||
amountState = AmountStatePreviewData.amountState,
|
||||
state = ConfirmationStatePreviewData.assentStakingState,
|
||||
validatorState = ValidatorStatePreviewData.validatorState,
|
||||
clickIntents = StakingClickIntentsStub,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
|
||||
@Composable
|
||||
internal fun StakingEventEffect(event: StateEvent<StakingEvent>, snackbarHostState: SnackbarHostState) {
|
||||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = alertConfig) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
alertConfig?.let {
|
||||
StakingAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is StakingEvent.ShowSnackBar -> {
|
||||
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
|
||||
}
|
||||
is StakingEvent.ShowAlert -> {
|
||||
alertConfig = value.alert
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButtonUM
|
||||
val dismissButton: DialogButtonUM?
|
||||
|
||||
val onActionClick = state.onConfirmClick
|
||||
if (onActionClick != null) {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
onActionClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResourceSafe(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
} else {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
dismissButton = null
|
||||
}
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismiss,
|
||||
title = state.title?.resolveReference(),
|
||||
dismissButton = dismissButton,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Density
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
|
||||
import com.tangem.core.ui.components.list.roundedListWithDividersItems
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.RewardBlockType
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview
|
||||
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.utils.StringsSigns.DOT
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
||||
private const val BANNER_BLOCK_KEY = "BannerBlock"
|
||||
private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock"
|
||||
private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock"
|
||||
private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton"
|
||||
|
||||
@Composable
|
||||
internal fun StakingInitialInfoContent(
|
||||
state: StakingStates.InitialInfoState,
|
||||
buttonState: NavigationButtonsState,
|
||||
clickIntents: StakingClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
if (state !is StakingStates.InitialInfoState.Data) return
|
||||
|
||||
TangemPullToRefreshContainer(
|
||||
config = state.pullToRefreshConfig,
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = alignLastToBottom(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state.showBanner) {
|
||||
item(key = BANNER_BLOCK_KEY) {
|
||||
Column(
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
BannerBlock(onClick = clickIntents::onInitialInfoBannerClick)
|
||||
SpacerH12()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.roundedListWithDividersItems(
|
||||
rows = state.infoItems,
|
||||
footerContent = { SpacerH12() },
|
||||
hideEndText = isBalanceHidden,
|
||||
)
|
||||
|
||||
activeStakingBlock(
|
||||
state = state,
|
||||
clickIntents = clickIntents,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
|
||||
item(STAKE_PRIMARY_BUTTON_KEY) {
|
||||
SpacerH12()
|
||||
StakeButtonBlock(buttonState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.activeStakingBlock(
|
||||
state: StakingStates.InitialInfoState.Data,
|
||||
clickIntents: StakingClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
val innerYieldBalanceState = state.yieldBalance as? InnerYieldBalanceState.Data ?: return
|
||||
|
||||
item(key = STAKING_REWARD_BLOCK_KEY) {
|
||||
Column(modifier = Modifier.animateItem()) {
|
||||
StakingRewardBlock(
|
||||
yieldBalanceState = state.yieldBalance,
|
||||
onRewardsClick = clickIntents::openRewardsValidators,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
}
|
||||
|
||||
if (innerYieldBalanceState.balances.isNotEmpty()) {
|
||||
item(ACTIVE_STAKING_BLOCK_KEY) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.staking_your_stakes),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = 0,
|
||||
lastIndex = 1 + state.yieldBalance.balances.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
)
|
||||
}
|
||||
itemsIndexed(
|
||||
items = state.yieldBalance.balances,
|
||||
key = { index, balance ->
|
||||
// Staked balance does not have unique identifier.
|
||||
buildString {
|
||||
append(balance.hashCode())
|
||||
append("_")
|
||||
append(index)
|
||||
}
|
||||
},
|
||||
) { index, balance ->
|
||||
ActiveStakingBlock(
|
||||
balance = balance,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = clickIntents::onActiveStake,
|
||||
onAnalytic = clickIntents::onActiveStakeAnalytic,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index + 1,
|
||||
lastIndex = state.yieldBalance.balances.lastIndex + 1,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BannerBlock(onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
painter = painterResource(R.drawable.img_staking_banner),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) {
|
||||
append(stringResourceSafe(R.string.staking_details_banner_text))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StakingRewardBlock(
|
||||
yieldBalanceState: InnerYieldBalanceState.Data,
|
||||
onRewardsClick: () -> Unit,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
val (text, textColor) = when (yieldBalanceState.rewardBlockType) {
|
||||
RewardBlockType.Rewards -> {
|
||||
annotatedReference {
|
||||
append(yieldBalanceState.rewardsFiat.orMaskWithStars(isBalanceHidden))
|
||||
appendSpace()
|
||||
append(DOT)
|
||||
appendSpace()
|
||||
append(yieldBalanceState.rewardsCrypto.orMaskWithStars(isBalanceHidden))
|
||||
} to TangemTheme.colors.text.primary1
|
||||
}
|
||||
RewardBlockType.RewardUnavailable -> {
|
||||
resourceReference(R.string.staking_details_auto_claiming_rewards_daily_text) to
|
||||
TangemTheme.colors.text.tertiary
|
||||
}
|
||||
RewardBlockType.NoRewards -> {
|
||||
resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary
|
||||
}
|
||||
}
|
||||
val isShowIcon = yieldBalanceState.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable
|
||||
InputRowDefault(
|
||||
title = resourceReference(R.string.staking_rewards),
|
||||
text = text,
|
||||
iconRes = R.drawable.ic_chevron_right_24.takeIf { isShowIcon },
|
||||
textColor = textColor,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
enabled = yieldBalanceState.isActionable,
|
||||
onClick = onRewardsClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveStakingBlock(
|
||||
balance: BalanceState,
|
||||
isBalanceHidden: Boolean,
|
||||
onClick: (BalanceState) -> Unit,
|
||||
onAnalytic: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (icon, iconTint) = balance.type.getIcon()
|
||||
InputRowImageInfo(
|
||||
subtitle = balance.title,
|
||||
caption = balance.subtitle ?: balance.getAprText(),
|
||||
infoTitle = balance.formattedFiatAmount.orMaskWithStars(isBalanceHidden),
|
||||
infoSubtitle = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden),
|
||||
imageUrl = balance.getImage(),
|
||||
iconRes = icon,
|
||||
iconTint = iconTint,
|
||||
subtitleEndIconRes = R.drawable.ic_staking_pending_transaction.takeIf { balance.isPending },
|
||||
onImageError = { ValidatorImagePlaceholder() },
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
enabled = balance.isClickable,
|
||||
onClick = {
|
||||
onAnalytic()
|
||||
onClick(balance)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StakeButtonBlock(buttonState: NavigationButtonsState) {
|
||||
val state = buttonState as? NavigationButtonsState.Data
|
||||
val primaryButton = state?.primaryButton
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
state?.onTextClick?.let { StakingTosText(it) }
|
||||
NavigationPrimaryButton(primaryButton = primaryButton)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BalanceState.getAprText() = combinedReference(
|
||||
resourceReference(R.string.staking_details_apr),
|
||||
annotatedReference {
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = validator?.apr.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun BalanceType.getIcon() = when (this) {
|
||||
BalanceType.UNSTAKING -> R.drawable.ic_connection_18 to TangemTheme.colors.icon.accent
|
||||
BalanceType.UNSTAKED -> R.drawable.ic_connection_18 to TangemTheme.colors.icon.informative
|
||||
BalanceType.LOCKED -> R.drawable.ic_lock_24 to TangemTheme.colors.icon.informative
|
||||
else -> null to TangemTheme.colors.icon.informative
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BalanceState.getImage() = when (type) {
|
||||
BalanceType.UNSTAKING,
|
||||
BalanceType.UNSTAKED,
|
||||
BalanceType.LOCKED,
|
||||
-> null
|
||||
else -> validator?.image
|
||||
}
|
||||
|
||||
private val textGradientColors = listOf(
|
||||
TangemColorPalette.White,
|
||||
Color(0xff8fb4df),
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun alignLastToBottom() = remember {
|
||||
object : Arrangement.Vertical {
|
||||
override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
|
||||
var currentOffset = 0
|
||||
|
||||
sizes.forEachIndexed { index, size ->
|
||||
if (index == sizes.lastIndex) {
|
||||
outPositions[index] = totalSize - size
|
||||
} else {
|
||||
outPositions[index] = currentOffset
|
||||
currentOffset += size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun StakingInitialInfoContent_Preview(
|
||||
@PreviewParameter(StakingInitialInfoContentPreviewProvider::class) feeState: StakingStates.InitialInfoState.Data,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StakingInitialInfoContent(
|
||||
state = feeState,
|
||||
buttonState = NavigationButtonsState.Empty,
|
||||
clickIntents = StakingClickIntentsStub,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider<StakingStates.InitialInfoState.Data> {
|
||||
override val values: Sequence<StakingStates.InitialInfoState.Data>
|
||||
get() = sequenceOf(
|
||||
InitialStakingStatePreview.defaultState,
|
||||
InitialStakingStatePreview.stateWithYield,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenContent
|
||||
import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStep
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig
|
||||
import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet
|
||||
import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
|
||||
@Composable
|
||||
internal fun StakingScreen(uiState: StakingUiState) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
||||
BackHandler(onBack = uiState.clickIntents::onPrevClick)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
StakingAppBar(
|
||||
uiState = uiState,
|
||||
)
|
||||
StakingScreenContent(
|
||||
uiState = uiState,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
NavigationButtonsBlock(
|
||||
buttonState = uiState.buttonsState.takeUnless { uiState.currentStep == StakingStep.InitialInfo }
|
||||
?: NavigationButtonsState.Empty,
|
||||
footerText = confirmationState?.footerText.takeIf { uiState.currentStep == StakingStep.Confirmation },
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
)
|
||||
StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig)
|
||||
}
|
||||
|
||||
StakingEventEffect(
|
||||
event = uiState.event,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
|
||||
if (bottomSheetConfig == null) return
|
||||
when (bottomSheetConfig.content) {
|
||||
is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig)
|
||||
is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig)
|
||||
is StakingActionSelectionBottomSheetConfig -> StakingActionSelectorBottomSheet(bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StakingAppBar(uiState: StakingUiState) {
|
||||
val (backIcon, click) = when (uiState.currentStep) {
|
||||
StakingStep.Amount,
|
||||
StakingStep.Confirmation,
|
||||
-> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick
|
||||
StakingStep.Validators,
|
||||
StakingStep.RewardsValidators,
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.InitialInfo,
|
||||
-> R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick
|
||||
}
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = uiState.title.resolveReference(),
|
||||
subtitle = uiState.subtitle?.resolveReference(),
|
||||
backIconRes = backIcon,
|
||||
onBackClick = click,
|
||||
backgroundColor = TangemTheme.colors.background.secondary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) {
|
||||
val currentScreen = uiState.currentStep
|
||||
var currentStateProxy by remember { mutableStateOf(currentScreen) }
|
||||
var isTransitionAnimationRunning by remember { mutableStateOf(false) }
|
||||
|
||||
// Prevent quick screen changes to avoid some of the transition animation distortions
|
||||
LaunchedEffect(currentScreen) {
|
||||
snapshotFlow { isTransitionAnimationRunning }
|
||||
.withIndex()
|
||||
.map { (index, running) ->
|
||||
if (running && index != 0) delay(timeMillis = 200)
|
||||
running
|
||||
}
|
||||
.first { !it }
|
||||
|
||||
currentStateProxy = currentScreen
|
||||
}
|
||||
// Restrict pressing the back button while screen transition is running to avoid most of the animation distortions
|
||||
BackHandler(enabled = isTransitionAnimationRunning) {}
|
||||
|
||||
// Box is needed to fix animation with resizing of AnimatedContent
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedContent(
|
||||
targetState = currentStateProxy,
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
label = "Staking Screen Navigation",
|
||||
transitionSpec = {
|
||||
val direction = if (initialState.ordinal < targetState.ordinal) {
|
||||
AnimatedContentTransitionScope.SlideDirection.Start
|
||||
} else {
|
||||
AnimatedContentTransitionScope.SlideDirection.End
|
||||
}
|
||||
|
||||
slideIntoContainer(towards = direction, animationSpec = tween())
|
||||
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
|
||||
},
|
||||
) { state ->
|
||||
isTransitionAnimationRunning = transition.targetState != transition.currentState
|
||||
|
||||
when (state) {
|
||||
StakingStep.InitialInfo -> StakingInitialInfoContent(
|
||||
state = uiState.initialInfoState,
|
||||
buttonState = uiState.buttonsState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
)
|
||||
StakingStep.RewardsValidators -> {
|
||||
StakingClaimRewardsValidatorContent(
|
||||
state = uiState.rewardsValidatorsState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
}
|
||||
StakingStep.Amount -> AmountScreenContent(
|
||||
amountState = uiState.amountState,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.secondary),
|
||||
)
|
||||
StakingStep.Confirmation -> StakingConfirmationContent(
|
||||
amountState = uiState.amountState,
|
||||
state = uiState.confirmationState,
|
||||
validatorState = uiState.validatorState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
StakingStep.RestakeValidator,
|
||||
StakingStep.Validators,
|
||||
-> StakingValidatorListContent(
|
||||
state = uiState.validatorState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
||||
private const val TERMS_OF_USE_KEY = "termsOfUse"
|
||||
private const val PRIVACY_POLICY_KEY = "privacyPolicy"
|
||||
|
||||
private const val TERMS_OF_USE_URL = "https://docs.stakek.it/docs/terms-of-use"
|
||||
private const val PRIVACY_POLICY_URL = "https://docs.stakek.it/docs/privacy-policy"
|
||||
|
||||
@Composable
|
||||
internal fun StakingTosText(onTextClick: (String) -> Unit) {
|
||||
val termsOfUse = stringResourceSafe(R.string.common_terms_of_use)
|
||||
val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy)
|
||||
val tosText = stringResourceSafe(R.string.staking_legal, termsOfUse, privacyPolicy)
|
||||
|
||||
val clickableAnnotation = buildAnnotatedString {
|
||||
append(tosText.substringBefore(termsOfUse))
|
||||
|
||||
pushStringAnnotation(TERMS_OF_USE_KEY, "")
|
||||
appendColored(termsOfUse, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
|
||||
append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy))
|
||||
|
||||
pushStringAnnotation(PRIVACY_POLICY_KEY, "")
|
||||
appendColored(privacyPolicy, TangemTheme.colors.text.accent)
|
||||
pop()
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = clickableAnnotation,
|
||||
style = TangemTheme.typography.caption2.copy(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
onClick = { offset ->
|
||||
clickableAnnotation.getStringAnnotations(
|
||||
tag = TERMS_OF_USE_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
).firstOrNull()?.let {
|
||||
onTextClick(TERMS_OF_USE_URL)
|
||||
}
|
||||
|
||||
clickableAnnotation.getStringAnnotations(
|
||||
tag = PRIVACY_POLICY_KEY,
|
||||
start = offset,
|
||||
end = offset,
|
||||
).firstOrNull()?.let {
|
||||
onTextClick(PRIVACY_POLICY_URL)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageSelector
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData
|
||||
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
|
||||
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
||||
/**
|
||||
* Staking screen with validators
|
||||
*/
|
||||
@Composable
|
||||
internal fun StakingValidatorListContent(
|
||||
state: StakingStates.ValidatorState,
|
||||
clickIntents: StakingClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
if (state is StakingStates.ValidatorState.Data) {
|
||||
val validators = state.availableValidators
|
||||
items(
|
||||
count = validators.size,
|
||||
key = { validators[it].address },
|
||||
contentType = { validators[it]::class.java },
|
||||
) { index ->
|
||||
val item = validators[index]
|
||||
|
||||
InputRowImageSelector(
|
||||
subtitle = stringReference(item.name),
|
||||
caption = combinedReference(
|
||||
resourceReference(R.string.staking_details_annual_percentage_rate),
|
||||
annotatedReference {
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = item.apr.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
),
|
||||
imageUrl = item.image.orEmpty(),
|
||||
isSelected = item == state.chosenValidator,
|
||||
onSelect = { clickIntents.onValidatorSelect(item) },
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = validators.lastIndex,
|
||||
radius = TangemTheme.dimens.radius12,
|
||||
addDefaultPadding =
|
||||
false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
subtitleExtraContent = {
|
||||
ValidatorLabel(item.isStrategicPartner)
|
||||
},
|
||||
selectorContent = { checked, _, _ ->
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier,
|
||||
visible = checked,
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
onImageError = { ValidatorImagePlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.ValidatorLabel(isStrategicPartner: Boolean) {
|
||||
if (isStrategicPartner) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.staking_validators_label),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.constant,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 6.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(TangemTheme.colors.text.accent)
|
||||
.padding(horizontal = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun StakingValidatorListContent_Preview(
|
||||
@PreviewParameter(StakingValidatorListContentPreviewProvider::class)
|
||||
data: StakingStates.ValidatorState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StakingValidatorListContent(
|
||||
state = data,
|
||||
clickIntents = StakingClickIntentsStub,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider<StakingStates.ValidatorState> {
|
||||
override val values: Sequence<StakingStates.ValidatorState>
|
||||
get() = sequenceOf(ValidatorStatePreviewData.validatorState)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun ValidatorImagePlaceholder() {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_staking_filled_18)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.inactive,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.icon.primary1, CircleShape)
|
||||
.padding(TangemTheme.dimens.size9),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui.block
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.CardWithIcon
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingNotification
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun NotificationsBlock(notifications: ImmutableList<NotificationUM>) {
|
||||
notifications.forEach { notification ->
|
||||
key(notification) {
|
||||
if (notification is StakingNotification.Warning.TransactionInProgress) {
|
||||
CardWithIcon(
|
||||
title = notification.title.resolveReference(),
|
||||
description = notification.description.resolveReference(),
|
||||
icon = {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Notification(
|
||||
config = notification.config,
|
||||
iconTint = when (notification) {
|
||||
is StakingNotification.Info,
|
||||
is NotificationUM.Info,
|
||||
-> TangemTheme.colors.icon.accent
|
||||
|
||||
is StakingNotification.Warning,
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Error.ExceedsBalance,
|
||||
is NotificationUM.Warning,
|
||||
-> null
|
||||
|
||||
is StakingNotification.Error,
|
||||
is NotificationUM.Error,
|
||||
-> TangemTheme.colors.icon.warning
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui.block
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) {
|
||||
val backgroundColor = if (isTransactionSent) {
|
||||
TangemTheme.colors.background.action
|
||||
} else {
|
||||
TangemTheme.colors.button.disabled
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(backgroundColor)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_network_fee_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) {
|
||||
when (feeState) {
|
||||
is FeeState.Content -> {
|
||||
val feeAmount = feeState.fee?.amount
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
preDot = stringReference(
|
||||
feeAmount?.value.format {
|
||||
crypto(
|
||||
symbol = feeAmount?.currencySymbol.orEmpty(),
|
||||
decimals = feeAmount?.decimals ?: 0,
|
||||
).fee(canBeLower = feeState.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeState.isFeeConvertibleToFiat) {
|
||||
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = feeAmount?.currencySymbol?.length,
|
||||
isSelected = true,
|
||||
showDivider = false,
|
||||
showSelectedAppearance = false,
|
||||
paddingValues = PaddingValues(),
|
||||
)
|
||||
}
|
||||
is FeeState.Loading -> {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
isSelected = true,
|
||||
paddingValues = PaddingValues(),
|
||||
showDivider = false,
|
||||
)
|
||||
FeeLoading(feeState)
|
||||
}
|
||||
is FeeState.Error -> {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
isSelected = true,
|
||||
paddingValues = PaddingValues(),
|
||||
)
|
||||
FeeError(feeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeLoading(feeState: FeeState) {
|
||||
AnimatedContent(
|
||||
targetState = feeState,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Loading) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
height = TangemTheme.dimens.size24,
|
||||
width = TangemTheme.dimens.size90,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeError(feeState: FeeState) {
|
||||
AnimatedContent(
|
||||
targetState = feeState,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeState.Error) {
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState) {
|
||||
TangemThemePreview {
|
||||
StakingFeeBlock(
|
||||
feeState = value,
|
||||
isTransactionSent = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
|
||||
|
||||
override val values: Sequence<FeeState>
|
||||
get() = sequenceOf(
|
||||
contentState,
|
||||
FeeState.Loading,
|
||||
FeeState.Error,
|
||||
)
|
||||
|
||||
private val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "MATIC",
|
||||
value = BigDecimal(0.159806),
|
||||
decimals = 18,
|
||||
type = AmountType.Coin,
|
||||
),
|
||||
)
|
||||
|
||||
private val contentState = FeeState.Content(
|
||||
fee = fee,
|
||||
rate = BigDecimal.ONE,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui.block
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
||||
@Composable
|
||||
internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClickable: Boolean, onClick: () -> Unit) {
|
||||
val state = validatorState as? StakingStates.ValidatorState.Data ?: return
|
||||
if (!state.isVisibleOnConfirmation) return
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
enabled = state.isClickable && isClickable,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
),
|
||||
) {
|
||||
InputRowImageInfo(
|
||||
title = resourceReference(R.string.staking_validator),
|
||||
subtitle = stringReference(state.chosenValidator.name),
|
||||
infoTitle = annotatedReference {
|
||||
append(resourceReference(R.string.staking_details_apr).resolveReference())
|
||||
appendSpace()
|
||||
appendColored(
|
||||
text = state.chosenValidator.apr.orZero().format { percent() },
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
imageUrl = state.chosenValidator.image.orEmpty(),
|
||||
onImageError = { ValidatorImagePlaceholder() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui.bottomsheet
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.R
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun StakingActionSelectorBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<StakingActionSelectionBottomSheetConfig>(
|
||||
config = config,
|
||||
title = { content ->
|
||||
TangemBottomSheetTitle(title = content.title)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
) { content ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing32,
|
||||
)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
content.actions.forEachIndexed { index, action ->
|
||||
InputRowDefault(
|
||||
text = action.type.getPendingActionTitle(),
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
showDivider = index != content.actions.lastIndex,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
content.onActionSelect(action)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_StakingActionSelectorBottomSheet() {
|
||||
TangemThemePreview {
|
||||
StakingActionSelectorBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = StakingActionSelectionBottomSheetConfig(
|
||||
title = resourceReference(R.string.common_select_action),
|
||||
actions = persistentListOf(
|
||||
PendingAction(
|
||||
type = StakingActionType.CLAIM_REWARDS,
|
||||
passthrough = "",
|
||||
args = null,
|
||||
),
|
||||
PendingAction(
|
||||
type = StakingActionType.RESTAKE_REWARDS,
|
||||
passthrough = "",
|
||||
args = null,
|
||||
),
|
||||
),
|
||||
onActionSelect = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.features.staking.impl.presentation.ui.bottomsheet
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig
|
||||
|
||||
@Composable
|
||||
fun StakingInfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
TangemBottomSheet<StakingInfoBottomSheetConfig>(
|
||||
config = config,
|
||||
title = { content ->
|
||||
TangemBottomSheetTitle(title = content.title)
|
||||
},
|
||||
) { content ->
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(scrollState),
|
||||
) {
|
||||
Text(
|
||||
text = content.text.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing28,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_StakingInfoBottomSheet() {
|
||||
TangemThemePreview {
|
||||
StakingInfoBottomSheet(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = StakingInfoBottomSheetConfig(
|
||||
title = stringReference("Title"),
|
||||
text = stringReference(
|
||||
"""
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula
|
||||
tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat,
|
||||
at bibendum eros lacinia. Proin hendrerit massa non velit congue,
|
||||
in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada.
|
||||
Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat.
|
||||
Donec sagittis enim in metus lacinia, vel tempor nunc bibendum.
|
||||
""".trimIndent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
Loading…
Add table
Add a link
Reference in a new issue