Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-10 18:03:44 +03:00
parent a0ce025263
commit 9cc6f26cf4
21 changed files with 471 additions and 3 deletions

View file

@ -49,6 +49,7 @@ import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -158,6 +159,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var emailSender: EmailSender
@Inject
lateinit var stakingRouter: StakingRouter
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -242,6 +246,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
sendRouter = sendRouter,
qrScanningRouter = qrScanningRouter,
emailSender = emailSender,
stakingRouter = stakingRouter,
),
)
}

View file

@ -167,5 +167,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment()
AppScreen.Staking -> store.inject(getDependency = DaggerGraphState::stakingRouter).getEntryFragment()
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -25,5 +26,6 @@ sealed interface DaggerGraphAction : Action {
val sendRouter: SendRouter,
val qrScanningRouter: QrScanningRouter,
val emailSender: EmailSender,
val stakingRouter: StakingRouter,
) : DaggerGraphAction
}

View file

@ -23,6 +23,7 @@ object DaggerGraphReducer {
sendRouter = action.sendRouter,
qrScanningRouter = action.qrScanningRouter,
emailSender = action.emailSender,
stakingRouter = action.stakingRouter,
)
}
}

View file

@ -30,6 +30,7 @@ import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggle
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -79,4 +80,5 @@ data class DaggerGraphState(
val assetLoader: AssetLoader? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = null,
val detailsEntryPoint: DetailsEntryPoint? = null,
val stakingRouter: StakingRouter? = null,
) : StateType

View file

@ -35,4 +35,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) {
SaveWallet(isDialogFragment = true),
AppCurrencySelector,
ModalNotification(isDialogFragment = true),
Staking,
}

View file

@ -27,6 +27,6 @@ sealed class ScenarioUnavailabilityReason {
data object UnassociatedAsset : ScenarioUnavailabilityReason()
enum class WithdrawalScenario {
SELL, SEND
SELL, SEND // TODO staking create&process STAKING
}
}

View file

@ -5,4 +5,9 @@ import androidx.fragment.app.Fragment
interface StakingRouter {
fun getEntryFragment(): Fragment
companion object {
const val CRYPTO_CURRENCY_KEY = "CRYPTO_CURRENCY_KEY"
const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY"
}
}

View file

@ -47,6 +47,15 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/** Domain */
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
/** Common */
implementation(projects.common)

View file

@ -1,8 +1,9 @@
package com.tangem.features.staking.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.features.staking.impl.presentation.StakingFragment
internal class DefaultStakingRouter : InnerStakingRouter {
override fun getEntryFragment(): Fragment = TODO()
override fun getEntryFragment(): Fragment = StakingFragment.create()
}

View file

@ -2,4 +2,7 @@ package com.tangem.features.staking.impl.navigation
import com.tangem.features.staking.api.navigation.StakingRouter
interface InnerStakingRouter : StakingRouter
interface InnerStakingRouter : StakingRouter {
// TODO staking
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.staking.impl.presentation
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import javax.inject.Inject
/**
* Staking fragment
*/
@AndroidEntryPoint
internal class StakingFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var router: StakingRouter
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
private val viewModel by viewModels<StakingViewModel>()
private val innerStakingRouter: InnerStakingRouter
get() = requireNotNull(router as? InnerStakingRouter) {
"innerStakingRouter should be instance of InnerStakingRouter"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
viewModel.setRouter(
innerStakingRouter,
StakingStateRouter(
fragmentManager = WeakReference(parentFragmentManager),
),
)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.tertiary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val currentState = viewModel.stakingStateRouter.currentState.collectAsStateWithLifecycle()
// StakingScreen(viewModel.uiState, currentState.value)
}
override fun onDestroy() {
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
companion object {
/** Create staking fragment instance */
fun create(): StakingFragment = StakingFragment()
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.staking.impl.R
@Immutable
internal sealed class StakingAlertState {
abstract val title: TextReference?
abstract val message: TextReference
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
open val onConfirmClick: (() -> Unit)? = null
data class GenericError(
override val title: TextReference? = TODO(),
override val onConfirmClick: () -> Unit,
) : StakingAlertState() {
override val message: TextReference = resourceReference(R.string.common_unknown_error)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class StakingEvent {
data class ShowSnackBar(val text: TextReference) : StakingEvent()
data class ShowAlert(val alert: StakingAlertState) : StakingEvent()
}

View file

@ -0,0 +1,47 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.event.consumedEvent
import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
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() {
val value: StakingUiState get() = uiState.value
private val mutableUiState: MutableStateFlow<StakingUiState> = MutableStateFlow(value = getInitialState())
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
fun update(function: (StakingUiState) -> StakingUiState) {
mutableUiState.update(function = function)
}
fun update(transformer: StakingScreenStateTransformer) {
mutableUiState.update(function = transformer::transform)
}
fun clear() {
mutableUiState.update { getInitialState() }
}
private fun getInitialState(): StakingUiState {
return StakingUiState(
clickIntents = object : StakingClickIntents {},
cryptoCurrencyName = "",
currentScreen = StakingUiStateType.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = StakingStates.AmountState.Empty(),
validatorState = StakingStates.ValidatorState.Empty(),
confirmStakingState = StakingStates.ConfirmStakingState.Empty(),
isBalanceHidden = false,
event = consumedEvent(),
)
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.lang.ref.WeakReference
internal class StakingStateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
) {
private var mutableCurrentState: MutableStateFlow<StakingUiStateType> = MutableStateFlow(getInitState())
val currentState: StateFlow<StakingUiStateType>
get() = mutableCurrentState.asStateFlow()
fun clear() {
mutableCurrentState.update { getInitState() }
}
fun popBackStack() {
fragmentManager.get()?.popBackStack()
}
fun onBackClick(isSuccess: Boolean = false) {
val type = currentState.value
when {
isSuccess -> popBackStack()
else -> when (type) {
StakingUiStateType.Amount -> showInitial()
StakingUiStateType.ValidatorAndFee -> showAmount()
else -> popBackStack()
}
}
}
fun onNextClick() {
when (currentState.value) {
StakingUiStateType.InitialInfo -> showAmount()
StakingUiStateType.Amount,
StakingUiStateType.ValidatorAndFee,
-> showConfirm()
StakingUiStateType.Confirm -> onBackClick()
}
}
fun onPrevClick() {
when (currentState.value) {
StakingUiStateType.Amount -> showInitial()
else -> popBackStack()
}
}
private fun showInitial() {
mutableCurrentState.update { StakingUiStateType.InitialInfo }
}
fun showAmount() {
mutableCurrentState.update { StakingUiStateType.Amount }
}
fun showValidator() {
mutableCurrentState.update { StakingUiStateType.ValidatorAndFee }
}
fun showConfirm() {
mutableCurrentState.update { StakingUiStateType.Confirm }
}
private fun getInitState() = StakingUiStateType.InitialInfo
}

View file

@ -0,0 +1,101 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
/**
* Ui states of the staking screen
*/
@Immutable
internal data class StakingUiState(
val clickIntents: StakingClickIntents,
val cryptoCurrencyName: String,
val currentScreen: StakingUiStateType,
val initialInfoState: StakingStates.InitialInfoState,
val amountState: StakingStates.AmountState,
val validatorState: StakingStates.ValidatorState,
val confirmStakingState: StakingStates.ConfirmStakingState,
val isBalanceHidden: Boolean,
val event: StateEvent<StakingEvent>,
) {
fun copyWrapped(
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
amountState: StakingStates.AmountState = this.amountState,
validatorState: StakingStates.ValidatorState = this.validatorState,
confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState,
): StakingUiState = copy(
initialInfoState = initialInfoState,
amountState = amountState,
validatorState = validatorState,
confirmStakingState = confirmStakingState,
)
}
internal sealed class StakingStates {
abstract val isPrimaryButtonEnabled: Boolean
/** Initial info state */
sealed class InitialInfoState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : InitialInfoState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : InitialInfoState()
}
/** Amount state */
sealed class AmountState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : AmountState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : AmountState()
}
/** Validator state */
sealed class ValidatorState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : ValidatorState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : ValidatorState()
}
/** Fee state */
sealed class FeeState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : FeeState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : FeeState()
}
/** Confirm state */
sealed class ConfirmStakingState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : ConfirmStakingState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : ConfirmStakingState()
}
}
enum class StakingUiStateType {
InitialInfo,
Amount,
ValidatorAndFee,
Confirm,
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.StakingUiState
internal class HideBalanceStateTransformer(
private val isBalanceHidden: Boolean,
) : StakingScreenStateTransformer {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(isBalanceHidden = isBalanceHidden)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.StakingUiState
internal interface StakingScreenStateTransformer {
fun transform(prevState: StakingUiState): StakingUiState
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.staking.impl.presentation.viewmodel
internal interface StakingClickIntents {
// TODO staking
}

View file

@ -0,0 +1,84 @@
package com.tangem.features.staking.impl.presentation.viewmodel
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@HiltViewModel
internal class StakingViewModel @Inject constructor(
private val stateController: StakingStateController,
private val dispatchers: CoroutineDispatcherProvider,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
val uiState: StateFlow<StakingUiState> = stateController.uiState
val value: StakingUiState get() = uiState.value
var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private set
private val cryptoCurrency: CryptoCurrency = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_KEY]
?: error("This screen can't open without `CryptoCurrency`")
private val userWalletId: UserWalletId = savedStateHandle.get<String>(StakingRouter.USER_WALLET_ID_KEY)
?.let { stringValue -> UserWalletId(stringValue) }
?: error("This screen can't open without `UserWalletId`")
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var innerRouter: InnerStakingRouter by Delegates.notNull()
init {
subscribeOnBalanceHiding()
subscribeOnCurrencyStatusUpdates()
}
fun setRouter(router: InnerStakingRouter, stakingStateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stakingStateRouter
}
private fun subscribeOnCurrencyStatusUpdates() {
viewModelScope.launch {
getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrency.id).fold(
ifRight = {
cryptoCurrencyStatus = it
},
ifLeft = {
// TODO staking error
},
)
}
}
private fun subscribeOnBalanceHiding() {
getBalanceHidingSettingsUseCase()
.conflate()
.distinctUntilChanged()
.onEach {
stateController.update(transformer = HideBalanceStateTransformer(it.isBalanceHidden))
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
}
}