Updated on 2026-08-14
This commit is contained in:
commit
cef36091da
379 changed files with 8948 additions and 8976 deletions
|
|
@ -41,6 +41,8 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
|
|
@ -57,8 +59,6 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
internal data class ButtonState(
|
||||
val isEnabled: Boolean,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
internal data class CustomTokenData(
|
||||
val contractAddressTextField: TextFieldState,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
internal data class Derivation(
|
||||
val networkName: String,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
internal data class EnterCustomDerivationState(
|
||||
val value: String,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state
|
||||
|
||||
internal sealed class TextFieldState {
|
||||
object Loading : TextFieldState()
|
||||
|
|
@ -8,6 +8,7 @@ internal sealed class TextFieldState {
|
|||
val isEnabled: Boolean,
|
||||
val error: AddCustomTokenWarning? = null,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val onFocusExit: () -> Unit,
|
||||
) : TextFieldState()
|
||||
|
||||
fun isInputValid(): Boolean = this is Editable && value.isNotBlank() && error == null
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.factory
|
||||
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.managetokens.presentation.common.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.*
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
internal class CustomTokensStateFactory(
|
||||
internal class AddCustomTokenStateFactory(
|
||||
private val currentStateProvider: Provider<AddCustomTokenState>,
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
private val clickIntents: AddCustomTokenClickIntents,
|
||||
) {
|
||||
|
||||
fun getInitialState(): AddCustomTokenState {
|
||||
|
|
@ -201,21 +201,25 @@ internal class CustomTokensStateFactory(
|
|||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -268,6 +272,8 @@ internal class CustomTokensStateFactory(
|
|||
value = contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
|
||||
),
|
||||
nameTextField = TextFieldState.Loading,
|
||||
symbolTextField = TextFieldState.Loading,
|
||||
|
|
@ -279,7 +285,7 @@ internal class CustomTokensStateFactory(
|
|||
fun handleAddressError(error: AddCustomTokenError): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
return when (error) {
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
||||
addTokenAddressFieldError(AddCustomTokenWarning.InvalidContractAddress)
|
||||
.copy(
|
||||
addTokenButton = uiState.addTokenButton.copy(isEnabled = false),
|
||||
|
|
@ -288,7 +294,7 @@ internal class CustomTokensStateFactory(
|
|||
.toPersistentSet(),
|
||||
)
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty ->
|
||||
AddCustomTokenError.FIELD_IS_EMPTY ->
|
||||
removeTokenAddressError()
|
||||
.copy(
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.factory
|
||||
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.DerivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.managetokens.presentation.customtokens.state.AddCustomTokenState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class AddCustomTokenStateToCryptoCurrencyConverter(
|
||||
|
|
@ -39,7 +39,7 @@ internal class AddCustomTokenStateToCryptoCurrencyConverter(
|
|||
private fun parseTokenOrNull(tokenData: CustomTokenData?): CryptoCurrencyFactory.Token? {
|
||||
val contractAddress = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val symbol = (tokenData?.symbolTextField as? TextFieldState.Editable)?.value
|
||||
val name = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val name = (tokenData?.nameTextField as? TextFieldState.Editable)?.value
|
||||
val decimals = (tokenData?.decimalsTextField as? TextFieldState.Editable)?.value?.toIntOrNull()
|
||||
return if (
|
||||
!contractAddress.isNullOrBlank() && !symbol.isNullOrBlank() && !name.isNullOrBlank() && decimals != null
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.factory
|
||||
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class ContractAddressToCustomTokenDataConverter(
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
private val clickIntents: AddCustomTokenClickIntents,
|
||||
) : Converter<String, CustomTokenData> {
|
||||
override fun convert(value: String): CustomTokenData {
|
||||
return CustomTokenData(
|
||||
|
|
@ -14,21 +14,25 @@ internal class ContractAddressToCustomTokenDataConverter(
|
|||
value = value,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.factory
|
||||
|
||||
import com.tangem.domain.tokens.model.FoundToken
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensClickIntents
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class FoundTokenToCustomTokenDataConverter(
|
||||
private val clickIntents: CustomTokensClickIntents,
|
||||
private val clickIntents: AddCustomTokenClickIntents,
|
||||
) : Converter<FoundToken, CustomTokenData> {
|
||||
override fun convert(value: FoundToken): CustomTokenData {
|
||||
return CustomTokenData(
|
||||
|
|
@ -15,21 +15,25 @@ internal class FoundTokenToCustomTokenDataConverter(
|
|||
value = value.contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = value.name,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = value.symbol,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = value.decimals.toString(),
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.factory
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.factory
|
||||
|
||||
import com.tangem.core.ui.extensions.getActiveIconResByNetworkId
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.previewdata
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.WalletState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.*
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
|
|
@ -38,6 +38,7 @@ internal object AddCustomTokenPreviewData {
|
|||
value = "0x4ace7262705b68bcba5b91de96889349394",
|
||||
isEnabled = false,
|
||||
onValueChange = {},
|
||||
onFocusExit = {},
|
||||
),
|
||||
nameTextField = TextFieldState.Loading,
|
||||
symbolTextField = TextFieldState.Loading,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.previewdata
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata
|
||||
|
||||
import com.tangem.managetokens.presentation.customtokens.state.ChooseDerivationState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.Derivation
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.state.previewdata
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata
|
||||
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.ChooseNetworkState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object ChooseNetworkCustomPreviewData {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -12,9 +12,11 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
|
@ -25,13 +27,13 @@ import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetCon
|
|||
import com.tangem.managetokens.presentation.common.ui.EventEffect
|
||||
import com.tangem.managetokens.presentation.common.ui.components.Alert
|
||||
import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock
|
||||
import com.tangem.managetokens.presentation.customtokens.state.AddCustomTokenState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.previewdata.AddCustomTokenPreviewData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.AddCustomTokenPreviewData
|
||||
|
||||
@Composable
|
||||
internal fun CustomTokensScreen(state: AddCustomTokenState, modifier: Modifier = Modifier) {
|
||||
internal fun AddCustomTokenScreen(state: AddCustomTokenState, modifier: Modifier = Modifier) {
|
||||
var alertState by remember { mutableStateOf<AlertState?>(value = null) }
|
||||
|
||||
EventEffect(
|
||||
|
|
@ -46,11 +48,14 @@ internal fun CustomTokensScreen(state: AddCustomTokenState, modifier: Modifier =
|
|||
|
||||
@Composable
|
||||
private fun Content(state: AddCustomTokenState, modifier: Modifier = Modifier) {
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing10,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
|
|
@ -78,12 +83,14 @@ private fun Content(state: AddCustomTokenState, modifier: Modifier = Modifier) {
|
|||
.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
CustomTokenItemsList(state = state)
|
||||
PrimaryButton(
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
onClick = state.addTokenButton.onClick,
|
||||
enabled = state.addTokenButton.isEnabled,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (keyboard is Keyboard.Closed) {
|
||||
PrimaryButton(
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
onClick = state.addTokenButton.onClick,
|
||||
enabled = state.addTokenButton.isEnabled,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.chooseWalletState is ChooseWalletState.Choose && state.chooseWalletState.show) {
|
||||
|
|
@ -243,7 +250,7 @@ private fun TokenTextFieldTitle(state: TextFieldState?, title: String) {
|
|||
@Composable
|
||||
private fun Preview_ChooseDerivationScreen_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
CustomTokensScreen(state = AddCustomTokenPreviewData.state)
|
||||
AddCustomTokenScreen(state = AddCustomTokenPreviewData.state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +258,6 @@ private fun Preview_ChooseDerivationScreen_Light() {
|
|||
@Composable
|
||||
private fun Preview_ChooseDerivationScreen_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
CustomTokensScreen(state = AddCustomTokenPreviewData.state)
|
||||
AddCustomTokenScreen(state = AddCustomTokenPreviewData.state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -16,8 +16,8 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock
|
||||
import com.tangem.managetokens.presentation.customtokens.state.ChooseDerivationState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.previewdata.ChooseDerivationPreviewData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseDerivationPreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseDerivationScreen(state: ChooseDerivationState, modifier: Modifier = Modifier) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -16,8 +16,8 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.ui.components.NetworkItem
|
||||
import com.tangem.managetokens.presentation.customtokens.state.ChooseNetworkState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.previewdata.ChooseNetworkCustomPreviewData
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseNetworkCustomPreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modifier = Modifier) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.ui.components.AdditionalTextInputDialogParams
|
|||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.components.TextInputDialog
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.customtokens.state.EnterCustomDerivationState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.EnterCustomDerivationState
|
||||
|
||||
@Composable
|
||||
internal fun CustomDerivationDialog(state: EnterCustomDerivationState) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.ui
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -6,13 +6,17 @@ import androidx.compose.foundation.text.BasicTextField
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.customtokens.state.TextFieldState
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState
|
||||
|
||||
@Composable
|
||||
internal fun TokenTextField(
|
||||
|
|
@ -20,6 +24,11 @@ internal fun TokenTextField(
|
|||
placeholder: String,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
) {
|
||||
val isInitiallyComposed = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = true) {
|
||||
isInitiallyComposed.value = true
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
onValueChange = state.onValueChange,
|
||||
|
|
@ -32,7 +41,12 @@ internal fun TokenTextField(
|
|||
),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.icon.primary1),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
if (!it.isFocused && isInitiallyComposed.value) {
|
||||
state.onFocusExit()
|
||||
}
|
||||
},
|
||||
decorationBox = { innerTextField ->
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
if (state.value.isEmpty()) {
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.viewmodels
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.viewmodels
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.Derivation
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation
|
||||
|
||||
internal interface CustomTokensClickIntents {
|
||||
@Suppress("TooManyFunctions")
|
||||
internal interface AddCustomTokenClickIntents {
|
||||
|
||||
fun onNetworkSelected(networkItemState: NetworkItemState)
|
||||
|
||||
|
|
@ -25,6 +26,14 @@ internal interface CustomTokensClickIntents {
|
|||
|
||||
fun onDecimalsChange(input: String)
|
||||
|
||||
fun onContractAddressFocusExit()
|
||||
|
||||
fun onTokenNameFocusExit()
|
||||
|
||||
fun onSymbolFocusExit()
|
||||
|
||||
fun onDecimalsFocusExit()
|
||||
|
||||
fun onDerivationSelected(derivation: Derivation)
|
||||
|
||||
fun onChooseDerivationClick()
|
||||
|
|
@ -1,29 +1,34 @@
|
|||
package com.tangem.managetokens.presentation.customtokens.viewmodels
|
||||
package com.tangem.managetokens.presentation.addcustomtoken.viewmodels
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.HDWalletError
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.managetokens.presentation.common.analytics.ManageTokens
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.*
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.AddCustomTokenStateToCryptoCurrencyConverter
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.ContractAddressToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.CustomTokensStateFactory
|
||||
import com.tangem.managetokens.presentation.customtokens.state.factory.FoundTokenToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.*
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateToCryptoCurrencyConverter
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.factory.ContractAddressToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateFactory
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.state.factory.FoundTokenToCustomTokenDataConverter
|
||||
import com.tangem.managetokens.presentation.router.InnerManageTokensRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -39,9 +44,9 @@ import kotlinx.coroutines.withContext
|
|||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@HiltViewModel
|
||||
internal class CustomTokensViewModel @Inject constructor(
|
||||
internal class AddCustomTokenViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
|
|
@ -52,11 +57,12 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
private val validateContractAddressUseCase: ValidateContractAddressUseCase,
|
||||
private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet,
|
||||
private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase,
|
||||
) : ViewModel(), CustomTokensClickIntents, DefaultLifecycleObserver {
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
||||
private val stateFactory = CustomTokensStateFactory(
|
||||
private val stateFactory = AddCustomTokenStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
clickIntents = this,
|
||||
)
|
||||
|
|
@ -66,7 +72,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
var uiState: AddCustomTokenState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
init {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -86,6 +92,10 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
uiState = stateFactory.getInitialState()
|
||||
}
|
||||
|
||||
private suspend fun selectSuitableWallet(suitableUserWallets: List<UserWallet>): UserWalletId? {
|
||||
val selectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
val selectedWalletId = if (walletSupportsAddingTokens(selectedWallet) && suitableUserWallets.isNotEmpty()) {
|
||||
|
|
@ -110,6 +120,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onNetworkSelected(networkItemState: NetworkItemState) {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenNetworkSelected(networkItemState.name))
|
||||
selectNetwork(networkItemState)
|
||||
router.popBackStack()
|
||||
}
|
||||
|
|
@ -128,7 +139,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onChooseNetworkClick() {
|
||||
router.openCustomTokensChooseNetwork()
|
||||
router.openCustomTokenChooseNetwork()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingNetworkClick() {
|
||||
|
|
@ -136,6 +147,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onWalletSelected(walletId: String) {
|
||||
analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.CustomToken))
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val userWalletId = UserWalletId(walletId)
|
||||
selectWalletUseCase(userWalletId)
|
||||
|
|
@ -152,7 +164,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onChooseWalletClick() {
|
||||
router.openCustomTokensChooseWallet()
|
||||
router.openCustomTokenChooseWallet()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingWalletClick() {
|
||||
|
|
@ -166,6 +178,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onContractAddressChange,
|
||||
onFocusExit = this::onContractAddressFocusExit,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -200,22 +213,34 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
networkId = networkId,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
val tokenData = ContractAddressToCustomTokenDataConverter(this@CustomTokensViewModel)
|
||||
val tokenData = ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel)
|
||||
.convert(contractAddress)
|
||||
|
||||
val isButtonEnabled = tokenData.isRequiredInformationProvided()
|
||||
uiState = uiState.copy(
|
||||
tokenData = tokenData,
|
||||
warnings = (uiState.warnings + AddCustomTokenWarning.PotentialScamToken).toPersistentSet(),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = isButtonEnabled,
|
||||
),
|
||||
)
|
||||
},
|
||||
ifRight = { token ->
|
||||
val tokenData = if (token != null) {
|
||||
FoundTokenToCustomTokenDataConverter(this@CustomTokensViewModel).convert(token)
|
||||
FoundTokenToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert(token)
|
||||
} else {
|
||||
ContractAddressToCustomTokenDataConverter(this@CustomTokensViewModel).convert(
|
||||
ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert(
|
||||
contractAddress,
|
||||
)
|
||||
}
|
||||
uiState = uiState.copy(tokenData = tokenData)
|
||||
|
||||
val isButtonEnabled = tokenData.isRequiredInformationProvided()
|
||||
uiState = uiState.copy(
|
||||
tokenData = tokenData,
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = isButtonEnabled,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -229,8 +254,12 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onTokenNameChange,
|
||||
onFocusExit = this::onTokenNameFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -241,8 +270,12 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onSymbolChange,
|
||||
onFocusExit = this::onSymbolFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -260,19 +293,45 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
isEnabled = true,
|
||||
onValueChange = this::onDecimalsChange,
|
||||
error = error,
|
||||
onFocusExit = this::onDecimalsFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onContractAddressFocusExit() {
|
||||
val error = (uiState.tokenData?.contractAddressTextField as? TextFieldState.Editable)?.error
|
||||
val validated = error !is AddCustomTokenWarning.InvalidContractAddress
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenAddress(validated = validated))
|
||||
}
|
||||
|
||||
override fun onTokenNameFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenName)
|
||||
}
|
||||
|
||||
override fun onSymbolFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenSymbol)
|
||||
}
|
||||
|
||||
override fun onDecimalsFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDecimals)
|
||||
}
|
||||
|
||||
override fun onDerivationSelected(derivation: Derivation) {
|
||||
uiState =
|
||||
uiState.copy(chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation))
|
||||
derivation.standardType?.let {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(derivation.networkName))
|
||||
}
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation),
|
||||
)
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
override fun onChooseDerivationClick() {
|
||||
router.openCustomTokensChooseDerivation()
|
||||
router.openCustomTokenChooseDerivation()
|
||||
}
|
||||
|
||||
override fun onCloseChoosingDerivationClick() {
|
||||
|
|
@ -280,6 +339,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomDerivationChange(input: String) {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value))
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
|
|
@ -332,21 +392,77 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
val cryptoCurrency = AddCustomTokenStateToCryptoCurrencyConverter(
|
||||
selectedWallet.scanResponse.derivationStyleProvider,
|
||||
).convert(uiState)
|
||||
val alreadyAdded =
|
||||
getCurrenciesUseCase(selectedWallet.walletId).getOrNull()?.any { it == cryptoCurrency }
|
||||
if (alreadyAdded == true) {
|
||||
val alreadyAdded = isCryptoCurrencyAlreadyAdded(selectedWallet, cryptoCurrency)
|
||||
if (alreadyAdded) {
|
||||
uiState = stateFactory.getStateAndTriggerEvent(
|
||||
state = uiState,
|
||||
event = Event.ShowAlert(AlertState.TokenAlreadyAdded),
|
||||
setUiState = { uiState = it },
|
||||
)
|
||||
} else {
|
||||
sendTokenAddedEvent(cryptoCurrency)
|
||||
addCryptoCurrenciesUseCase(selectedWallet.walletId, currency = cryptoCurrency)
|
||||
withContext(dispatchers.main) { router.popBackStack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun isCryptoCurrencyAlreadyAdded(
|
||||
selectedWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Boolean {
|
||||
val currenciesList = getCurrenciesUseCase(selectedWallet.walletId).getOrElse { emptyList() }
|
||||
return when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
currenciesList.any {
|
||||
it is CryptoCurrency.Coin &&
|
||||
it.id == cryptoCurrency.id &&
|
||||
it.network.derivationPath == cryptoCurrency.network.derivationPath
|
||||
}
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
currenciesList.any {
|
||||
(it as? CryptoCurrency.Token)?.let {
|
||||
it.id == cryptoCurrency.id &&
|
||||
it.contractAddress == cryptoCurrency.contractAddress &&
|
||||
it.network.id == cryptoCurrency.network.id &&
|
||||
it.network.derivationPath == cryptoCurrency.network.derivationPath
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTokenAddedEvent(cryptoCurrency: CryptoCurrency) {
|
||||
val selectedDerivation = uiState.chooseDerivationState?.selectedDerivation
|
||||
|
||||
val derivation = when {
|
||||
selectedDerivation == null -> ManageTokens.Derivation.DEFAULT.value
|
||||
selectedDerivation.networkName.isNotEmpty() -> selectedDerivation.networkName
|
||||
else -> ManageTokens.Derivation.CUSTOM.value
|
||||
}
|
||||
when (cryptoCurrency) {
|
||||
is CryptoCurrency.Token -> {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.CustomTokenWasAdded(
|
||||
derivation = derivation,
|
||||
networkId = cryptoCurrency.network.name,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
token = cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Coin -> {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.CustomTokenWasAdded(
|
||||
derivation = derivation,
|
||||
networkId = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBack() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.managetokens.presentation.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class ManageTokens(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Manage Tokens", event, params) {
|
||||
|
||||
class ScreenOpened : ManageTokens("Manage Tokens Screen Opened")
|
||||
|
||||
class TokenIsNotFound(userInput: String) : ManageTokens(
|
||||
event = "Token Is Not Found",
|
||||
params = mapOf("Input" to userInput),
|
||||
)
|
||||
|
||||
class TokenSwitcherChanged(
|
||||
token: String,
|
||||
state: AnalyticsParam.OnOffState,
|
||||
) : ManageTokens(
|
||||
event = "Token Switcher Changed",
|
||||
params = mapOf(
|
||||
"Token" to token,
|
||||
"State" to state.value,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonAdd(token: String) : ManageTokens(
|
||||
event = "Button - Add",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
class ButtonEdit(token: String) : ManageTokens(
|
||||
event = "Button - Edit",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
object ButtonChooseWallet : ManageTokens(event = "Button - Choose Wallet")
|
||||
|
||||
class WalletSelected(source: Source) : ManageTokens(
|
||||
event = "Wallet Selected",
|
||||
params = mapOf("Source" to source.name),
|
||||
) {
|
||||
|
||||
enum class Source(name: String) {
|
||||
MainToken("Main Token"),
|
||||
CustomToken("Custom Token"),
|
||||
}
|
||||
}
|
||||
|
||||
object NoticeNonNativeNetworkClicked : ManageTokens(event = "Notice - Non Native Network Clicked")
|
||||
|
||||
class ButtonGenerateAddresses(cardCount: Int) : ManageTokens(
|
||||
event = "Button - Get Addresses",
|
||||
params = mapOf("CardCount" to cardCount.toString()),
|
||||
)
|
||||
|
||||
object ButtonCustomToken : ManageTokens("Button - Custom Token")
|
||||
|
||||
class CustomTokenWasAdded(
|
||||
val derivation: String,
|
||||
val networkId: String,
|
||||
val token: String? = null,
|
||||
val contractAddress: String? = null,
|
||||
) : ManageTokens(
|
||||
event = "Custom Token Was Added",
|
||||
params = mutableMapOf(
|
||||
"Derivation" to derivation,
|
||||
"Network Id" to networkId,
|
||||
).apply {
|
||||
token?.let { put("Token", it) }
|
||||
contractAddress?.let { put("Contract Address", it) }
|
||||
},
|
||||
)
|
||||
|
||||
class CustomTokenNetworkSelected(blockchain: String) : ManageTokens(
|
||||
event = "Custom Token Network Selected",
|
||||
params = mapOf("blockchain" to blockchain),
|
||||
)
|
||||
|
||||
class CustomTokenDerivationSelected(derivation: String) : ManageTokens(
|
||||
event = "Custom Token Derivation Selected",
|
||||
params = mapOf("Derivation" to derivation),
|
||||
)
|
||||
|
||||
class CustomTokenAddress(validated: Boolean) : ManageTokens(
|
||||
"Custom Token Address",
|
||||
params = mapOf("Validation" to if (validated) "Ok" else "Error"),
|
||||
)
|
||||
|
||||
object CustomTokenName : ManageTokens("Custom Token Name")
|
||||
|
||||
object CustomTokenSymbol : ManageTokens("Custom Token Symbol")
|
||||
|
||||
object CustomTokenDecimals : ManageTokens("Custom Token Decimals")
|
||||
|
||||
enum class Derivation(val value: String) {
|
||||
DEFAULT("Default"),
|
||||
CUSTOM("Custom"),
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ internal sealed interface NetworkItemState {
|
|||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class Toggleable(
|
||||
data class Toggleable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val id: String,
|
||||
|
|
@ -72,7 +72,7 @@ internal sealed interface NetworkItemState {
|
|||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
*
|
||||
*/
|
||||
class Selectable(
|
||||
data class Selectable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
val iconResId: Int,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ internal data class ManageTokensState(
|
|||
val selectedToken: TokenItemState.Loaded? = null,
|
||||
val showChooseWalletScreen: Boolean = false,
|
||||
val event: StateEvent<Event>,
|
||||
val onEmptySearchResult: (String) -> Unit,
|
||||
)
|
||||
|
||||
data class AddCustomTokenButton(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.managetokens.presentation.common.state.*
|
|||
import com.tangem.managetokens.presentation.common.utils.CurrencyUtils
|
||||
import com.tangem.managetokens.presentation.managetokens.state.*
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensClickIntents
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensUiEvents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
internal class ManageTokensStateFactory(
|
||||
private val currentStateProvider: Provider<ManageTokensState>,
|
||||
private val clickIntents: ManageTokensClickIntents,
|
||||
private val uiIntents: ManageTokensUiEvents,
|
||||
) {
|
||||
|
||||
fun getInitialState(tokens: Flow<PagingData<TokenItemState>>): ManageTokensState {
|
||||
|
|
@ -36,6 +38,7 @@ internal class ManageTokensStateFactory(
|
|||
isLoading = false,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletState.NoSelection,
|
||||
onEmptySearchResult = uiIntents::onEmptySearchResult,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +179,7 @@ internal class ManageTokensStateFactory(
|
|||
totalNeeded = totalNeeded,
|
||||
totalWallets = totalWallets,
|
||||
walletsToDerive = walletsToDerive,
|
||||
onGenerateClick = clickIntents::onGenerateDerivationClick,
|
||||
onGenerateClick = clickIntents::onGetAddressesClick,
|
||||
)
|
||||
}
|
||||
return currentStateProvider().copy(derivationNotification = derivationNotificationState)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal val nonNativeNetworks = listOf(
|
|||
iconResId = mutableStateOf(R.drawable.img_kusama_22),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
id = "1",
|
||||
onToggleClick = { _, _ -> },
|
||||
address = "",
|
||||
decimals = 0,
|
||||
|
|
@ -48,7 +48,7 @@ internal val nonNativeNetworks = listOf(
|
|||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
id = "2",
|
||||
onToggleClick = { _, _ -> },
|
||||
address = "",
|
||||
decimals = 0,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ internal object ManageTokensStatePreviewData {
|
|||
derivationNotification = DerivationNotificationStatePreviewData.state,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletStatePreviewData.state,
|
||||
onEmptySearchResult = {},
|
||||
)
|
||||
|
||||
val loadingState: ManageTokensState
|
||||
|
|
|
|||
|
|
@ -84,29 +84,18 @@ internal fun ChooseNetworkScreen(
|
|||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
|
||||
item {
|
||||
if (networkState.nativeNetworks.isNotEmpty()) {
|
||||
NativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
if (networkState.nativeNetworks.isNotEmpty()) {
|
||||
this@LazyColumn.nativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
|
||||
if (networkState.nonNativeNetworks.isNotEmpty()) {
|
||||
item {
|
||||
NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
}
|
||||
item {
|
||||
this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
this@LazyColumn.nonNativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
Column {
|
||||
private fun LazyListScope.nativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_native_title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -119,25 +108,35 @@ private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenIt
|
|||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
}
|
||||
|
||||
networkState.nativeNetworks.forEachIndexed { index, network ->
|
||||
NetworkItem(
|
||||
state = network,
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
items(
|
||||
count = networkState.nativeNetworks.count(),
|
||||
key = { index -> networkState.nativeNetworks[index].id },
|
||||
) { index ->
|
||||
NetworkItem(
|
||||
state = networkState.nativeNetworks[index],
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
private fun LazyListScope.nonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
item {
|
||||
NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick)
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
}
|
||||
|
||||
items(
|
||||
count = networkState.nonNativeNetworks.count(),
|
||||
key = { index -> networkState.nonNativeNetworks[index].id },
|
||||
|
|
@ -153,6 +152,7 @@ private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, to
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ package com.tangem.managetokens.presentation.managetokens.ui
|
|||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -14,6 +11,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -50,6 +49,8 @@ private fun Content(state: ManageTokensState) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
) {
|
||||
|
|
@ -77,6 +78,14 @@ private fun Content(state: ManageTokensState) {
|
|||
)
|
||||
}
|
||||
val tokens = state.tokens.collectAsLazyPagingItems()
|
||||
val query = state.searchBarState.query
|
||||
|
||||
TrackPossibleEmptySearchResult(
|
||||
tokens = tokens,
|
||||
query = query,
|
||||
onEmptySearchResult = state.onEmptySearchResult,
|
||||
)
|
||||
|
||||
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
|
||||
}
|
||||
state.derivationNotification?.let {
|
||||
|
|
@ -92,6 +101,27 @@ private fun Content(state: ManageTokensState) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackPossibleEmptySearchResult(
|
||||
tokens: LazyPagingItems<TokenItemState>,
|
||||
query: String,
|
||||
onEmptySearchResult: (String) -> Unit,
|
||||
) {
|
||||
val wasLoading = remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(tokens.loadState) {
|
||||
val isLoading = tokens.loadState.refresh == LoadState.Loading
|
||||
val stoppedLoading = wasLoading.value && !isLoading
|
||||
val queryAndTokensCondition = query.isNotEmpty() && tokens.itemSnapshotList.isEmpty()
|
||||
|
||||
if (stoppedLoading && queryAndTokensCondition) {
|
||||
onEmptySearchResult(query)
|
||||
}
|
||||
|
||||
wasLoading.value = isLoading
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) {
|
||||
if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) {
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@ import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
|||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
internal interface ManageTokensClickIntents {
|
||||
|
||||
fun onAddCustomTokensButtonClick()
|
||||
|
||||
fun onSearchQueryChange(query: String)
|
||||
|
||||
fun onSearchActiveChange(active: Boolean)
|
||||
|
||||
fun onTokenItemButtonClick(token: TokenItemState.Loaded)
|
||||
|
||||
fun onGenerateDerivationClick()
|
||||
fun onGetAddressesClick()
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onCloseChooseNetworkScreen()
|
||||
|
||||
fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable)
|
||||
fun onNonNativeNetworkHintClick()
|
||||
|
||||
fun onSelectWalletsClick()
|
||||
fun onNonNativeNetworkHintClick()
|
||||
|
||||
fun onChooseWalletClick()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.viewmodels
|
||||
|
||||
internal interface ManageTokensUiEvents {
|
||||
|
||||
fun onEmptySearchResult(query: String)
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
|
|
@ -20,6 +22,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.managetokens.presentation.common.analytics.ManageTokens
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
|
|
@ -58,13 +61,15 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val checkCurrencyCompatibilityUseCase: CheckCurrencyCompatibilityUseCase,
|
||||
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
||||
) : ViewModel(), ManageTokensClickIntents, DefaultLifecycleObserver {
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), ManageTokensClickIntents, ManageTokensUiEvents, DefaultLifecycleObserver {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
||||
private val stateFactory = ManageTokensStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
clickIntents = this,
|
||||
uiIntents = this,
|
||||
)
|
||||
|
||||
var router: InnerManageTokensRouter by Delegates.notNull()
|
||||
|
|
@ -80,7 +85,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
|
||||
private var selectedWallet: UserWallet? = null
|
||||
|
||||
private var neededDerivations: Map<UserWalletId, List<CryptoCurrency>> = emptyMap()
|
||||
private var currenciesToGenerateAddresses: Map<UserWalletId, List<CryptoCurrency>> = emptyMap()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
|
|
@ -107,6 +112,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(ManageTokens.ScreenOpened())
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -151,7 +158,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
.collectLatest {
|
||||
it.onRight { mapOfMissingDerivations ->
|
||||
neededDerivations = mapOfMissingDerivations
|
||||
currenciesToGenerateAddresses = mapOfMissingDerivations
|
||||
withContext(dispatchers.main) { updateDerivation() }
|
||||
}
|
||||
}
|
||||
|
|
@ -159,8 +166,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateDerivation() {
|
||||
val totalNeeded = neededDerivations.values.sumOf { derivations -> derivations.size }
|
||||
val walletsToDerive = neededDerivations.values
|
||||
val totalNeeded = currenciesToGenerateAddresses.values.sumOf { derivations -> derivations.size }
|
||||
val walletsToDerive = currenciesToGenerateAddresses.values
|
||||
.filter { derivations -> derivations.isNotEmpty() }.size
|
||||
uiState = stateFactory.updateDerivationNotification(
|
||||
totalNeeded = totalNeeded,
|
||||
|
|
@ -182,7 +189,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAddCustomTokensButtonClick() {
|
||||
router.openCustomTokensScreen()
|
||||
analyticsEventHandler.send(ManageTokens.ButtonCustomToken)
|
||||
router.openAddCustomTokenScreen()
|
||||
}
|
||||
|
||||
override fun onSearchQueryChange(query: String) {
|
||||
|
|
@ -201,6 +209,13 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
override fun onTokenItemButtonClick(token: TokenItemState.Loaded) {
|
||||
when (token.availableAction.value) {
|
||||
TokenButtonType.ADD, TokenButtonType.EDIT -> {
|
||||
if (token.availableAction.value == TokenButtonType.ADD) {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonAdd(token.currencySymbol))
|
||||
}
|
||||
if (token.availableAction.value == TokenButtonType.EDIT) {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonEdit(token.currencySymbol))
|
||||
}
|
||||
|
||||
uiState = uiState.copy(selectedToken = token)
|
||||
val addedCurrenciesOnWallet = addedCurrenciesByWallet[selectedWallet] ?: listOf()
|
||||
stateFactory.updateTokenNetworksOnTokenSelection(token, addedCurrenciesOnWallet)
|
||||
|
|
@ -219,17 +234,21 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onGenerateDerivationClick() {
|
||||
if (neededDerivations.isNotEmpty()) {
|
||||
override fun onGetAddressesClick() {
|
||||
if (currenciesToGenerateAddresses.isNotEmpty()) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val walletId = neededDerivations.keys.firstOrNull()
|
||||
val currenciesToDerive = neededDerivations[walletId]
|
||||
if (walletId == null || currenciesToDerive.isNullOrEmpty()) return@launch
|
||||
derivePublicKeysUseCase(walletId, currenciesToDerive)
|
||||
.onRight {
|
||||
updateDerivationNotificationState()
|
||||
fetchTokenListUseCase(userWalletId = walletId)
|
||||
val cardCount = currenciesToGenerateAddresses.count { it.value.isNotEmpty() }
|
||||
analyticsEventHandler.send(ManageTokens.ButtonGenerateAddresses(cardCount))
|
||||
|
||||
currenciesToGenerateAddresses.forEach { (walletId, currenciesToDerive) ->
|
||||
if (currenciesToDerive.isNotEmpty()) {
|
||||
derivePublicKeysUseCase(walletId, currenciesToDerive)
|
||||
.onRight {
|
||||
updateDerivationNotificationState()
|
||||
fetchTokenListUseCase(userWalletId = walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,8 +266,14 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
if (!selectedWallet.isMultiCurrency || selectedWallet.isLocked) return
|
||||
|
||||
if (network.isAdded.value) {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.Off),
|
||||
)
|
||||
toggleToken(token, network, selectedWallet)
|
||||
} else {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.On),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
checkCompatibilityAndToggleToken(token, network, selectedWallet)
|
||||
}
|
||||
|
|
@ -298,7 +323,6 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
"It is only null if Blockchain is Unknown, which mustn't happen here"
|
||||
}
|
||||
if (!network.isAdded.value) {
|
||||
updateUi(token, network)
|
||||
addedCurrenciesByWallet[selectedWallet]?.add(cryptoCurrency)
|
||||
allAddedCurrencies.add(cryptoCurrency)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
|
|
@ -307,13 +331,14 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
currency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
updateUi(token, network)
|
||||
} else {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
if (canBeRemovedAndShowAlertIfNot(selectedWallet.walletId, cryptoCurrency)) {
|
||||
withContext(dispatchers.main) { updateUi(token, network) }
|
||||
addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency)
|
||||
allAddedCurrencies.remove(cryptoCurrency)
|
||||
removeCurrencyUseCase(selectedWallet.walletId, cryptoCurrency)
|
||||
withContext(dispatchers.main) { updateUi(token, network) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -348,6 +373,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onNonNativeNetworkHintClick() {
|
||||
analyticsEventHandler.send(ManageTokens.NoticeNonNativeNetworkClicked)
|
||||
uiState = stateFactory.getStateAndTriggerEvent(
|
||||
state = uiState,
|
||||
event = Event.ShowAlert(AlertState.NonNative),
|
||||
|
|
@ -355,13 +381,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onSelectWalletsClick() {
|
||||
uiState = uiState.copy(
|
||||
showChooseWalletScreen = true,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChooseWalletClick() {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonChooseWallet)
|
||||
uiState = uiState.copy(
|
||||
showChooseWalletScreen = true,
|
||||
)
|
||||
|
|
@ -374,6 +395,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onWalletSelected(walletId: String) {
|
||||
analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.MainToken))
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
selectWalletUseCase(UserWalletId(walletId))
|
||||
}
|
||||
|
|
@ -381,4 +403,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
uiState.selectedToken?.let { onTokenItemButtonClick(it) }
|
||||
uiState = stateFactory.updateSelectedWallet(selectedWalletId = selectedWallet?.walletId?.stringValue)
|
||||
}
|
||||
|
||||
override fun onEmptySearchResult(query: String) {
|
||||
analyticsEventHandler.send(ManageTokens.TokenIsNotFound(query))
|
||||
}
|
||||
}
|
||||
|
|
@ -15,11 +15,11 @@ import com.tangem.core.navigation.NavigationAction
|
|||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.managetokens.ManageTokensFragment
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.customtokens.ui.ChooseDerivationScreen
|
||||
import com.tangem.managetokens.presentation.customtokens.ui.ChooseNetworkCustomScreen
|
||||
import com.tangem.managetokens.presentation.customtokens.ui.CustomTokensChooseWalletScreen
|
||||
import com.tangem.managetokens.presentation.customtokens.ui.CustomTokensScreen
|
||||
import com.tangem.managetokens.presentation.customtokens.viewmodels.CustomTokensViewModel
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseDerivationScreen
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.ui.ChooseNetworkCustomScreen
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.ui.CustomTokensChooseWalletScreen
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenScreen
|
||||
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel
|
||||
import kotlin.properties.Delegates
|
||||
|
|
@ -46,21 +46,22 @@ internal class DefaultManageTokensRouter(
|
|||
}
|
||||
|
||||
navigation(
|
||||
startDestination = ManageTokensRoute.CustomTokens.Main.route,
|
||||
route = ManageTokensRoute.CustomTokens.route,
|
||||
startDestination = ManageTokensRoute.AddCustomToken.Main.route,
|
||||
route = ManageTokensRoute.AddCustomToken.route,
|
||||
) {
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.Main.route,
|
||||
ManageTokensRoute.AddCustomToken.Main.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
CustomTokensScreen(state = viewModel.uiState)
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
AddCustomTokenScreen(state = viewModel.uiState)
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseNetwork.route,
|
||||
ManageTokensRoute.AddCustomToken.ChooseNetwork.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
ChooseNetworkCustomScreen(
|
||||
|
|
@ -68,9 +69,9 @@ internal class DefaultManageTokensRouter(
|
|||
)
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseDerivation.route,
|
||||
ManageTokensRoute.AddCustomToken.ChooseDerivation.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
ChooseDerivationScreen(
|
||||
|
|
@ -78,9 +79,9 @@ internal class DefaultManageTokensRouter(
|
|||
)
|
||||
}
|
||||
composable(
|
||||
ManageTokensRoute.CustomTokens.ChooseWallet.route,
|
||||
ManageTokensRoute.AddCustomToken.ChooseWallet.route,
|
||||
) {
|
||||
val viewModel = hiltViewModel<CustomTokensViewModel>(viewModelStoreOwner).apply {
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>(viewModelStoreOwner).apply {
|
||||
router = this@DefaultManageTokensRouter
|
||||
}
|
||||
CustomTokensChooseWalletScreen(
|
||||
|
|
@ -103,19 +104,19 @@ internal class DefaultManageTokensRouter(
|
|||
navController.navigate(ManageTokensRoute.ManageTokens.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensScreen() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.route)
|
||||
override fun openAddCustomTokenScreen() {
|
||||
navController.navigate(ManageTokensRoute.AddCustomToken.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseNetwork() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseNetwork.route)
|
||||
override fun openCustomTokenChooseNetwork() {
|
||||
navController.navigate(ManageTokensRoute.AddCustomToken.ChooseNetwork.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseDerivation() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseDerivation.route)
|
||||
override fun openCustomTokenChooseDerivation() {
|
||||
navController.navigate(ManageTokensRoute.AddCustomToken.ChooseDerivation.route)
|
||||
}
|
||||
|
||||
override fun openCustomTokensChooseWallet() {
|
||||
navController.navigate(ManageTokensRoute.CustomTokens.ChooseWallet.route)
|
||||
override fun openCustomTokenChooseWallet() {
|
||||
navController.navigate(ManageTokensRoute.AddCustomToken.ChooseWallet.route)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.navigation.AppScreen
|
|||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
|
||||
internal interface InnerManageTokensRouter : ManageTokensRouter {
|
||||
|
||||
/**
|
||||
* Initialize router
|
||||
**/
|
||||
|
|
@ -19,12 +20,15 @@ internal interface InnerManageTokensRouter : ManageTokensRouter {
|
|||
/** Open manage tokens screen */
|
||||
fun openManageTokensScreen()
|
||||
|
||||
/** Open custom tokens screen */
|
||||
fun openCustomTokensScreen()
|
||||
/** Open add custom token screen */
|
||||
fun openAddCustomTokenScreen()
|
||||
|
||||
fun openCustomTokensChooseNetwork()
|
||||
/** Open custom token choose network screen */
|
||||
fun openCustomTokenChooseNetwork()
|
||||
|
||||
fun openCustomTokensChooseDerivation()
|
||||
/** Open custom token choose derivation screen */
|
||||
fun openCustomTokenChooseDerivation()
|
||||
|
||||
fun openCustomTokensChooseWallet()
|
||||
/** Open custom token choose wallet screen */
|
||||
fun openCustomTokenChooseWallet()
|
||||
}
|
||||
|
|
@ -10,10 +10,10 @@ internal sealed class ManageTokensRoute(val route: String) {
|
|||
|
||||
object ManageTokens : ManageTokensRoute(route = "manage_tokens")
|
||||
|
||||
object CustomTokens : ManageTokensRoute(route = "manage_tokens/custom_tokens") {
|
||||
object Main : ManageTokensRoute(CustomTokens.route + "/main")
|
||||
object ChooseNetwork : ManageTokensRoute(CustomTokens.route + "/choose_network")
|
||||
object ChooseWallet : ManageTokensRoute(CustomTokens.route + "/choose_wallet")
|
||||
object ChooseDerivation : ManageTokensRoute(CustomTokens.route + "/choose_derivation")
|
||||
object AddCustomToken : ManageTokensRoute(route = "manage_tokens/add_custom_token") {
|
||||
object Main : ManageTokensRoute(AddCustomToken.route + "/main")
|
||||
object ChooseNetwork : ManageTokensRoute(AddCustomToken.route + "/choose_network")
|
||||
object ChooseWallet : ManageTokensRoute(AddCustomToken.route + "/choose_wallet")
|
||||
object ChooseDerivation : ManageTokensRoute(AddCustomToken.route + "/choose_derivation")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,30 +4,23 @@ import arrow.core.getOrElse
|
|||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.referral.domain.converter.TokensConverter
|
||||
import com.tangem.feature.referral.domain.models.ReferralData
|
||||
import com.tangem.feature.referral.domain.models.TokenData
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import com.tangem.lib.crypto.DerivationManager
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class ReferralInteractorImpl(
|
||||
private val repository: ReferralRepository,
|
||||
private val derivationManager: DerivationManager,
|
||||
private val userWalletManager: UserWalletManager,
|
||||
private val tokensConverter: TokensConverter,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val testerFeatureToggles: TesterFeatureToggles,
|
||||
) : ReferralInteractor {
|
||||
|
||||
private val tokensForReferral = mutableListOf<TokenData>()
|
||||
|
||||
override val isDemoMode: Boolean
|
||||
get() = repository.isDemoMode
|
||||
override val isDemoMode: Boolean get() = repository.isDemoMode
|
||||
|
||||
override suspend fun getReferralStatus(): ReferralData {
|
||||
val referralData = repository.getReferralData(userWalletManager.getWalletId())
|
||||
|
|
@ -38,19 +31,9 @@ internal class ReferralInteractorImpl(
|
|||
}
|
||||
|
||||
override suspend fun startReferral(): ReferralData {
|
||||
return if (tokensForReferral.isNotEmpty()) {
|
||||
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
|
||||
startReferralNew(tokenData = tokensForReferral.first())
|
||||
} else {
|
||||
// TODO: delete [REDACTED_JIRA]
|
||||
startReferralLegacy(tokenData = tokensForReferral.first())
|
||||
}
|
||||
} else {
|
||||
error("Tokens for ref is empty")
|
||||
}
|
||||
}
|
||||
if (tokensForReferral.isEmpty()) error("Tokens for ref is empty")
|
||||
|
||||
private suspend fun startReferralNew(tokenData: TokenData): ReferralData {
|
||||
val tokenData = tokensForReferral.first()
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
|
||||
error("Failed to get selected wallet: $it")
|
||||
}
|
||||
|
|
@ -79,18 +62,6 @@ internal class ReferralInteractorImpl(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun startReferralLegacy(tokenData: TokenData): ReferralData {
|
||||
val currency = tokensConverter.convert(tokenData)
|
||||
val derivationPath = derivationManager.deriveAndAddTokens(currency)
|
||||
val publicAddress = userWalletManager.getWalletAddress(currency.networkId, derivationPath)
|
||||
return repository.startReferral(
|
||||
walletId = userWalletManager.getWalletId(),
|
||||
networkId = currency.networkId,
|
||||
tokenId = currency.id,
|
||||
address = publicAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveReferralTokens(tokens: List<TokenData>) {
|
||||
tokensForReferral.clear()
|
||||
tokensForReferral.addAll(tokens)
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.feature.referral.domain.converter
|
||||
|
||||
import com.tangem.feature.referral.domain.models.TokenData
|
||||
import com.tangem.lib.crypto.models.Currency
|
||||
import com.tangem.lib.crypto.models.Currency.NativeToken
|
||||
import com.tangem.lib.crypto.models.Currency.NonNativeToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class TokensConverter @Inject constructor() : Converter<TokenData, Currency> {
|
||||
|
||||
override fun convert(value: TokenData): Currency {
|
||||
return if (value.decimalCount != null &&
|
||||
value.contractAddress != null
|
||||
) {
|
||||
NonNativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
networkId = value.networkId,
|
||||
contractAddress = value.contractAddress,
|
||||
decimalCount = value.decimalCount,
|
||||
)
|
||||
} else {
|
||||
NativeToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
networkId = value.networkId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
|||
import com.tangem.feature.referral.domain.ReferralInteractor
|
||||
import com.tangem.feature.referral.domain.ReferralInteractorImpl
|
||||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
import com.tangem.feature.referral.domain.converter.TokensConverter
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import com.tangem.lib.crypto.DerivationManager
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,23 +21,17 @@ class ReferralDomainModule {
|
|||
@ViewModelScoped
|
||||
fun provideReferralInteractor(
|
||||
referralRepository: ReferralRepository,
|
||||
derivationManager: DerivationManager,
|
||||
userWalletManager: UserWalletManager,
|
||||
tokensConverter: TokensConverter,
|
||||
derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
testerFeatureToggles: TesterFeatureToggles,
|
||||
): ReferralInteractor {
|
||||
return ReferralInteractorImpl(
|
||||
repository = referralRepository,
|
||||
derivationManager = derivationManager,
|
||||
userWalletManager = userWalletManager,
|
||||
tokensConverter = tokensConverter,
|
||||
derivePublicKeysUseCase = derivePublicKeysUseCase,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
|
||||
testerFeatureToggles = testerFeatureToggles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,5 +9,8 @@ interface SendRouter {
|
|||
companion object {
|
||||
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
|
||||
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
|
||||
const val TRANSACTION_ID_KEY = "send_transaction_id"
|
||||
const val AMOUNT_KEY = "send_amount"
|
||||
const val DESTINATION_ADDRESS_KEY = "send_destination_address"
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,14 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
|
|
@ -59,6 +67,8 @@ dependencies {
|
|||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
@ -41,6 +42,9 @@ internal class SendFragment : ComposeFragment() {
|
|||
@Inject
|
||||
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
|
||||
|
||||
@Inject
|
||||
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
private val innerSendRouter: InnerSendRouter
|
||||
get() = requireNotNull(router as? InnerSendRouter) {
|
||||
|
|
@ -50,10 +54,14 @@ internal class SendFragment : ComposeFragment() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
|
||||
val isEditingDisabled = arguments?.getString(SendRouter.TRANSACTION_ID_KEY) != null
|
||||
viewModel.setRouter(
|
||||
innerSendRouter,
|
||||
StateRouter(
|
||||
fragmentManager = WeakReference(parentFragmentManager),
|
||||
isEditingDisabled = isEditingDisabled,
|
||||
analyticsEventsHandler = analyticsEventsHandler,
|
||||
),
|
||||
)
|
||||
listenToQrCode()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.features.send.impl.presentation.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
|
||||
|
||||
/**
|
||||
* Send screen analytics
|
||||
*/
|
||||
internal sealed class SendAnalyticEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Token / Send", event = event, params = params) {
|
||||
|
||||
/** Send screen opened */
|
||||
object SendOpened : SendAnalyticEvents(event = "Send Screen Opened")
|
||||
|
||||
/** Next button clicked */
|
||||
data class NextButtonClicked(val source: SendScreenSource) : SendAnalyticEvents(
|
||||
event = "Button - Next",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
|
||||
/** Back button clicked */
|
||||
data class BackButtonClicked(val source: SendScreenSource) : SendAnalyticEvents(
|
||||
event = "Button - Back",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
|
||||
// region Address
|
||||
/** Recipient address screen opened */
|
||||
object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
|
||||
|
||||
/** Address to send entered */
|
||||
data class AddressEntered(val source: EnterAddressSource, val isValid: Boolean) : SendAnalyticEvents(
|
||||
event = "Address Entered",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
VALIDATION to if (isValid) "Success" else "Fail",
|
||||
),
|
||||
)
|
||||
|
||||
/** Paste from clipboard button clicked */
|
||||
data class PasteButtonClicked(val type: PasteType) : SendAnalyticEvents(
|
||||
event = "Button - Paste",
|
||||
params = mapOf(
|
||||
TYPE to type.name,
|
||||
),
|
||||
)
|
||||
|
||||
/** Qr Code button clicked */
|
||||
object QrCodeButtonClicked : SendAnalyticEvents(event = "Button - QR Code")
|
||||
// endregion
|
||||
|
||||
// region Amount
|
||||
/** Amount screen opened */
|
||||
object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
|
||||
|
||||
/** Selected currency */
|
||||
data class SelectedCurrency(val type: SelectedCurrencyType) : SendAnalyticEvents(
|
||||
event = "Selected Currency",
|
||||
params = mapOf(TYPE to type.value),
|
||||
)
|
||||
|
||||
/** Currency selector button clicked */
|
||||
object SwapCurrencyButtonClicked : SendAnalyticEvents(event = "Button - Swap Currency")
|
||||
// endregion
|
||||
|
||||
// region Fee
|
||||
/** Fee screen opened */
|
||||
object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
|
||||
|
||||
/** Selected fee (send after next screen opened) */
|
||||
data class SelectedFee(val fee: String) : SendAnalyticEvents(
|
||||
event = "Fee Selected",
|
||||
params = mapOf("Commission" to fee),
|
||||
)
|
||||
|
||||
/** Custom fee selected */
|
||||
object CustomFeeButtonClicked : SendAnalyticEvents(event = "Custom Fee Clicked")
|
||||
|
||||
/** Custom fee edited */
|
||||
object GasPriceInserter : SendAnalyticEvents(event = "Gas Price Inserted")
|
||||
|
||||
/** Subtract from amount selector switched (send after next screen opened) */
|
||||
object SubtractFromAmount : SendAnalyticEvents(event = "Subtract from Amount")
|
||||
// endregion
|
||||
|
||||
// region Confirmation
|
||||
/** Confirmation screen opened */
|
||||
object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
|
||||
|
||||
/** Send transaction button clicked */
|
||||
object SendButtonClicked : SendAnalyticEvents(event = "Button - Send")
|
||||
|
||||
/** Screen reopened from confirmation screen */
|
||||
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
|
||||
event = "Screen Reopened",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
// endregion
|
||||
|
||||
// region Transaction Result
|
||||
/** Transaction send screen opened */
|
||||
object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened")
|
||||
|
||||
/** Share button clicked */
|
||||
object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
|
||||
|
||||
/** Expore button clicked */
|
||||
object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
|
||||
// endregion
|
||||
}
|
||||
|
||||
internal enum class SendScreenSource {
|
||||
Address,
|
||||
Amount,
|
||||
Fee,
|
||||
}
|
||||
|
||||
internal enum class EnterAddressSource {
|
||||
QRCode,
|
||||
PasteButton,
|
||||
RecentAddress,
|
||||
}
|
||||
|
||||
internal enum class PasteType {
|
||||
Address,
|
||||
Memo,
|
||||
}
|
||||
|
||||
internal enum class SelectedCurrencyType(val value: String) {
|
||||
Token("Token"),
|
||||
AppCurrency("App Currency"),
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.analytics.utils
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
internal class SendOnNextScreenAnalyticSender(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
fun send(prevScreen: SendUiStateType, state: SendUiState) {
|
||||
when (prevScreen) {
|
||||
SendUiStateType.Fee -> {
|
||||
val feeState = state.feeState ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
|
||||
feeSelectorState?.selectedFee?.let { selectedFee ->
|
||||
val isCustomFeeEdited = feeState.fee?.amount?.value != feeSelectorState.fees.normal.amount.value
|
||||
if (selectedFee == FeeType.Custom && isCustomFeeEdited) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.GasPriceInserter)
|
||||
}
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(selectedFee.name))
|
||||
}
|
||||
if (feeState.isSubtract) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount)
|
||||
}
|
||||
}
|
||||
SendUiStateType.Amount -> {
|
||||
val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return
|
||||
val selectedCurrency = if (isFiatSelected) {
|
||||
SelectedCurrencyType.Token
|
||||
} else {
|
||||
SelectedCurrencyType.AppCurrency
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.SelectedCurrency(selectedCurrency),
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.send.impl.presentation.analytics.utils
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.analytics.PasteType
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
|
||||
internal class SendRecipientAnalyticsSender(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
fun sendAddressAnalytics(type: EnterAddressSource?, isValidAddress: Boolean) {
|
||||
type?.let {
|
||||
if (type == EnterAddressSource.PasteButton) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.PasteButtonClicked(PasteType.Address))
|
||||
}
|
||||
analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress))
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMemoAnalytics(isPasted: Boolean) {
|
||||
if (isPasted) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.PasteButtonClicked(PasteType.Memo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ internal class SendEventStateFactory(
|
|||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) }
|
||||
private val sendTransactionErrorConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendTransactionAlertConverter(clickIntents)
|
||||
}
|
||||
|
||||
fun onConsumeEventState(): SendUiState {
|
||||
return currentStateProvider().copy(event = consumedEvent())
|
||||
|
|
@ -48,10 +50,10 @@ internal class SendEventStateFactory(
|
|||
is TransactionFee.Single -> fee.normal
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeSelector.selectedFee) {
|
||||
FeeType.SLOW -> fee.minimum
|
||||
FeeType.MARKET -> fee.normal
|
||||
FeeType.FAST -> fee.priority
|
||||
FeeType.CUSTOM -> return state
|
||||
FeeType.Slow -> fee.minimum
|
||||
FeeType.Market -> fee.normal
|
||||
FeeType.Fast -> fee.priority
|
||||
FeeType.Custom -> return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -21,7 +22,7 @@ internal class SendNotificationFactory(
|
|||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ internal class SendNotificationFactory(
|
|||
addExceedBalanceNotification(feeAmount, sendAmount)
|
||||
addInvalidAmountNotification(feeState.isSubtract, sendAmount)
|
||||
addMinimumAmountErrorNotification(feeAmount, sendAmount)
|
||||
addDustWarningNotification(feeAmount, sendAmount)
|
||||
addReserveAmountErrorNotification(recipientState.addressTextField.value)
|
||||
addTransactionLimitErrorNotification(feeAmount, sendAmount)
|
||||
// warnings
|
||||
|
|
@ -117,12 +119,12 @@ internal class SendNotificationFactory(
|
|||
private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(recipientAddress: String) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val isAccountFunded = walletManagersFacade.checkIfAccountFunded(
|
||||
val isAccountFunded = currencyChecksRepository.checkIfAccountFunded(
|
||||
userWalletId,
|
||||
cryptoCurrency.network,
|
||||
recipientAddress,
|
||||
)
|
||||
val minimumAmount = walletManagersFacade.getReserveAmount(userWalletId, cryptoCurrency.network)
|
||||
val minimumAmount = currencyChecksRepository.getReserveAmount(userWalletId, cryptoCurrency.network)
|
||||
if (!isAccountFunded && minimumAmount != null && minimumAmount > BigDecimal.ZERO) {
|
||||
add(
|
||||
SendNotification.Error.ReserveAmountError(
|
||||
|
|
@ -141,7 +143,7 @@ internal class SendNotificationFactory(
|
|||
) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val utxoLimit = walletManagersFacade.checkUtxoAmountLimit(
|
||||
val utxoLimit = currencyChecksRepository.checkUtxoAmountLimit(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
amount = receivedAmount,
|
||||
|
|
@ -173,7 +175,7 @@ internal class SendNotificationFactory(
|
|||
} else {
|
||||
feeAmount + receivedAmount
|
||||
}
|
||||
val currencyDeposit = walletManagersFacade.getExistentialDeposit(
|
||||
val currencyDeposit = currencyChecksRepository.getExistentialDeposit(
|
||||
userWalletId,
|
||||
cryptoCurrency.network,
|
||||
)
|
||||
|
|
@ -210,6 +212,29 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val dustValue = currencyChecksRepository.getDustValue(
|
||||
userWalletProvider().walletId,
|
||||
cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (dustValue != null && !balance.isNullOrZero() && receivedAmount < balance) {
|
||||
val totalAmount = feeAmount + receivedAmount
|
||||
val change = balance - totalAmount
|
||||
val isChangeLowerThanDust = change < dustValue && change != BigDecimal.ZERO
|
||||
val isShowWarning = totalAmount < dustValue || isChangeLowerThanDust
|
||||
if (isShowWarning) {
|
||||
add(
|
||||
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CARDANO_MINIMUM = "1"
|
||||
private const val DOGECOIN_MINIMUM = "0.01"
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountCurrencyConverter
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
|
|
@ -38,29 +36,17 @@ internal class SendStateFactory(
|
|||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
private val amountFieldConverter by lazy {
|
||||
|
||||
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
private val amountFieldChangeConverter by lazy {
|
||||
SendAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val amountCurrencyConverter by lazy {
|
||||
SendAmountCurrencyConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
iconStateConverter = iconStateConverter,
|
||||
|
|
@ -69,20 +55,20 @@ internal class SendStateFactory(
|
|||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val recipientStateConverter by lazy {
|
||||
private val recipientStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val feeStateConverter by lazy {
|
||||
private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendFeeStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val recipientListStateConverter by lazy {
|
||||
private val recipientListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientListConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
|
|
@ -92,24 +78,34 @@ internal class SendStateFactory(
|
|||
// region UI states
|
||||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
currentState = MutableStateFlow(SendUiStateType.None),
|
||||
event = consumedEvent(),
|
||||
isEditingDisabled = false,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
amountState = state.amountState ?: amountStateConverter.convert(Unit),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(Unit),
|
||||
amountState = state.amountState ?: amountStateConverter.convert(""),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(""),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region amount state clicks
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
fun getReadyState(amount: String, destinationAddress: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
amountState = state.amountState ?: amountStateConverter.convert(amount),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
isEditingDisabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
||||
fun getOnHideBalanceState(isBalanceHidden: Boolean): SendUiState {
|
||||
return currentStateProvider().copy(isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region recipient
|
||||
|
|
@ -125,12 +121,13 @@ internal class SendStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun onRecipientAddressValueChange(value: String): SendUiState {
|
||||
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
addressTextField = recipientState.addressTextField.copy(value = value),
|
||||
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -205,6 +202,20 @@ internal class SendStateFactory(
|
|||
isValidating = false,
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
isError = value.isNotEmpty() && !isValidMemo,
|
||||
isEnabled = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnXAddressMemoState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -26,12 +27,14 @@ import java.math.BigDecimal
|
|||
@Immutable
|
||||
internal data class SendUiState(
|
||||
val clickIntents: SendClickIntents,
|
||||
val isEditingDisabled: Boolean,
|
||||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val sendState: SendStates.SendState = SendStates.SendState(),
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
val currentState: StateFlow<SendUiStateType>,
|
||||
val isBalanceHidden: Boolean,
|
||||
val event: StateEvent<SendEvent>,
|
||||
)
|
||||
|
||||
|
|
@ -99,6 +102,7 @@ internal sealed class SendStates {
|
|||
}
|
||||
|
||||
enum class SendUiStateType {
|
||||
None,
|
||||
Amount,
|
||||
Recipient,
|
||||
Fee,
|
||||
|
|
|
|||
|
|
@ -1,60 +1,119 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
internal class StateRouter(
|
||||
private val fragmentManager: WeakReference<FragmentManager>,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val isEditingDisabled: Boolean,
|
||||
) {
|
||||
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Recipient)
|
||||
private set
|
||||
private var mutableCurrentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(
|
||||
if (isEditingDisabled) {
|
||||
SendUiStateType.None
|
||||
} else {
|
||||
SendUiStateType.Recipient
|
||||
},
|
||||
)
|
||||
|
||||
val currentState: StateFlow<SendUiStateType> = mutableCurrentState
|
||||
|
||||
fun popBackStack() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Recipient -> popBackStack()
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Fee -> showAmount()
|
||||
SendUiStateType.Send -> showFee()
|
||||
fun onBackClick(isSuccess: Boolean = false) {
|
||||
when {
|
||||
isSuccess -> popBackStack()
|
||||
isEditingDisabled -> when (currentState.value) {
|
||||
SendUiStateType.Send -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
|
||||
showFee()
|
||||
}
|
||||
else -> popBackStack()
|
||||
}
|
||||
else -> when (currentState.value) {
|
||||
SendUiStateType.Amount -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Address))
|
||||
showRecipient()
|
||||
}
|
||||
SendUiStateType.Fee -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
|
||||
showAmount()
|
||||
}
|
||||
SendUiStateType.Send -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
|
||||
showFee()
|
||||
}
|
||||
else -> popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onNextClick() {
|
||||
fun onNextClick(): SendUiStateType {
|
||||
val prevState = currentState.value
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Recipient -> showAmount()
|
||||
SendUiStateType.Amount -> showFee()
|
||||
SendUiStateType.Fee -> showSend()
|
||||
SendUiStateType.Send -> onBackClick()
|
||||
SendUiStateType.Recipient -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Amount))
|
||||
showAmount()
|
||||
}
|
||||
SendUiStateType.Amount -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
|
||||
showFee()
|
||||
}
|
||||
SendUiStateType.Fee -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee))
|
||||
showSend()
|
||||
}
|
||||
SendUiStateType.Send -> {
|
||||
onBackClick()
|
||||
}
|
||||
else -> popBackStack()
|
||||
}
|
||||
return prevState
|
||||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Recipient -> popBackStack()
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Fee -> showAmount()
|
||||
SendUiStateType.Send -> popBackStack()
|
||||
if (isEditingDisabled) {
|
||||
popBackStack()
|
||||
} else {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Amount -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount))
|
||||
showRecipient()
|
||||
}
|
||||
SendUiStateType.Fee -> {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee))
|
||||
showAmount()
|
||||
}
|
||||
else -> popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showAmount() {
|
||||
currentState.update { SendUiStateType.Amount }
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened)
|
||||
mutableCurrentState.update { SendUiStateType.Amount }
|
||||
}
|
||||
|
||||
fun showRecipient() {
|
||||
currentState.update { SendUiStateType.Recipient }
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened)
|
||||
mutableCurrentState.update { SendUiStateType.Recipient }
|
||||
}
|
||||
|
||||
fun showFee() {
|
||||
currentState.update { SendUiStateType.Fee }
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened)
|
||||
mutableCurrentState.update { SendUiStateType.Fee }
|
||||
}
|
||||
|
||||
private fun showSend() {
|
||||
currentState.update { SendUiStateType.Send }
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
|
||||
mutableCurrentState.update { SendUiStateType.Send }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
/**
|
||||
* Factory to produce amount state for [SendUiState]
|
||||
*/
|
||||
internal class AmountStateFactory(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountFieldMaxAmountConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val amountCurrencyConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountCurrencyConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnMaxAmountClick(): SendUiState {
|
||||
return amountFieldMaxAmountConverter.convert(Unit)
|
||||
}
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
||||
}
|
||||
|
|
@ -22,9 +22,9 @@ internal class SendAmountStateConverter(
|
|||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
|
||||
private val sendAmountFieldConverter: SendAmountFieldConverter,
|
||||
) : Converter<Unit, SendStates.AmountState> {
|
||||
) : Converter<String, SendStates.AmountState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.AmountState {
|
||||
override fun convert(value: String): SendStates.AmountState {
|
||||
val userWallet = userWalletProvider()
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val status = cryptoCurrencyStatusProvider()
|
||||
|
|
@ -35,7 +35,7 @@ internal class SendAmountStateConverter(
|
|||
walletName = userWallet.name,
|
||||
walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)),
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
amountTextField = sendAmountFieldConverter.convert(value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ internal class FeeConverter(
|
|||
return when (val fees = value.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (value.selectedFee) {
|
||||
FeeType.SLOW -> fees.minimum
|
||||
FeeType.MARKET -> fees.normal
|
||||
FeeType.FAST -> fees.priority
|
||||
FeeType.CUSTOM -> convertCustom(value, fees)
|
||||
FeeType.Slow -> fees.minimum
|
||||
FeeType.Market -> fees.normal
|
||||
FeeType.Fast -> fees.priority
|
||||
FeeType.Custom -> convertCustom(value, fees)
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single -> fees.normal
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ internal class FeeNotificationFactory(
|
|||
val minimumValue = multipleFees.minimum.amount.value ?: return
|
||||
val customAmount = customFee.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) {
|
||||
add(SendFeeNotification.Informational.TooLow)
|
||||
if (selectedFee == FeeType.Custom && minimumValue > customValue) {
|
||||
add(SendFeeNotification.Warning.TooLow)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ internal class FeeNotificationFactory(
|
|||
val customAmount = customFee.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = customValue / highValue
|
||||
if (selectedFee == FeeType.CUSTOM && diff > FEE_MAX_DIFF) {
|
||||
if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
|
||||
add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal sealed class FeeSelectorState {
|
|||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val selectedFee: FeeType = FeeType.MARKET,
|
||||
val selectedFee: FeeType = FeeType.Market,
|
||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
|
|
@ -21,8 +21,8 @@ internal sealed class FeeSelectorState {
|
|||
}
|
||||
|
||||
enum class FeeType {
|
||||
SLOW,
|
||||
MARKET,
|
||||
FAST,
|
||||
CUSTOM,
|
||||
Slow,
|
||||
Market,
|
||||
Fast,
|
||||
Custom,
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ internal class FeeStateFactory(
|
|||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
) {
|
||||
private val customFeeFieldConverter by lazy {
|
||||
private val customFeeFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -35,7 +35,7 @@ internal class FeeStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
val feeConverter by lazy {
|
||||
val feeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
FeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -191,7 +191,7 @@ internal class FeeStateFactory(
|
|||
val fee = feeConverter.convert(feeSelectorState)
|
||||
val feeValue = fee.amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.Custom
|
||||
val isNotEmptyCustom = if (customValue != null) {
|
||||
!customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,22 +8,6 @@ import com.tangem.features.send.impl.R
|
|||
|
||||
sealed class SendFeeNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Informational(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
),
|
||||
) {
|
||||
object TooLow : Informational(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
|
|
@ -36,6 +20,11 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
|
|||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
object TooLow : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
)
|
||||
|
||||
data class TooHigh(
|
||||
val value: String,
|
||||
) : Warning(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
|
|
@ -43,6 +44,7 @@ internal class EthereumCustomFeeConverter(
|
|||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_max_fee_footer),
|
||||
label = getFeeFormatted(value.amount.value),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = value.gasPrice.toString(),
|
||||
|
|
@ -55,6 +57,7 @@ internal class EthereumCustomFeeConverter(
|
|||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = value.gasLimit.toString(),
|
||||
|
|
@ -67,6 +70,7 @@ internal class EthereumCustomFeeConverter(
|
|||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
|
|
@ -21,20 +24,29 @@ internal class SendAmountFieldConverter(
|
|||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Unit, SendTextField.AmountField> {
|
||||
) : Converter<String, SendTextField.AmountField> {
|
||||
|
||||
override fun convert(value: Unit): SendTextField.AmountField {
|
||||
override fun convert(value: String): SendTextField.AmountField {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoDecimal = value.toBigDecimalOrDefault()
|
||||
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
|
||||
val fiatValue = if (value.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
|
||||
fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
|
||||
}
|
||||
return SendTextField.AmountField(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
value = value,
|
||||
fiatValue = fiatValue,
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }),
|
||||
isFiatValue = false,
|
||||
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency),
|
||||
cryptoAmount = cryptoAmount,
|
||||
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.swapping_insufficient_funds),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendAmountFieldMaxAmountConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendUiState> {
|
||||
|
||||
override fun convert(value: Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val feeState = state.feeState ?: return state
|
||||
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
val decimalCryptoValue = cryptoCurrencyStatus.value.amount
|
||||
val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount
|
||||
|
||||
val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty()
|
||||
val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty()
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = false,
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
),
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
isSubtract = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -21,6 +22,7 @@ internal sealed class SendTextField {
|
|||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val keyboardActions: KeyboardActions,
|
||||
val cryptoAmount: Amount,
|
||||
val fiatAmount: Amount,
|
||||
val isFiatValue: Boolean,
|
||||
|
|
@ -47,12 +49,15 @@ internal sealed class SendTextField {
|
|||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val disabledText: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : SendTextField()
|
||||
|
||||
data class CustomFee(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val keyboardActions: KeyboardActions,
|
||||
val symbol: String?,
|
||||
val decimals: Int,
|
||||
val title: TextReference,
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import com.tangem.utils.converter.Converter
|
|||
|
||||
internal class SendRecipientAddressFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, SendTextField.RecipientAddress> {
|
||||
) : Converter<String, SendTextField.RecipientAddress> {
|
||||
|
||||
override fun convert(value: Unit): SendTextField.RecipientAddress {
|
||||
override fun convert(value: String): SendTextField.RecipientAddress {
|
||||
return SendTextField.RecipientAddress(
|
||||
value = "",
|
||||
value = value,
|
||||
onValueChange = clickIntents::onRecipientAddressValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ internal class SendRecipientMemoFieldConverter(
|
|||
placeholder = resourceReference(R.string.send_optional_field),
|
||||
label = resourceReference(value),
|
||||
error = resourceReference(R.string.send_memo_destination_tag_error),
|
||||
disabledText = resourceReference(R.string.send_additional_field_already_included),
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
|
|||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.RecipientState> {
|
||||
) : Converter<String, SendStates.RecipientState> {
|
||||
|
||||
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
|
||||
private val memoFieldConverter by lazy {
|
||||
|
|
@ -19,9 +19,9 @@ internal class SendRecipientStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Unit): SendStates.RecipientState {
|
||||
override fun convert(value: String): SendStates.RecipientState {
|
||||
return SendStates.RecipientState(
|
||||
addressTextField = addressFieldConverter.convert(Unit),
|
||||
addressTextField = addressFieldConverter.convert(value),
|
||||
memoTextField = memoFieldConverter.convertOrNull(),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.extensions.shareText
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
|
|
@ -49,9 +50,10 @@ internal fun SendNavigationButtons(uiState: SendUiState) {
|
|||
@Composable
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsState()
|
||||
val isEditingDisabled = uiState.isEditingDisabled
|
||||
val isCorrectScreen = currentState.value == SendUiStateType.Amount || currentState.value == SendUiStateType.Fee
|
||||
AnimatedVisibility(
|
||||
visible = currentState.value == SendUiStateType.Amount ||
|
||||
currentState.value == SendUiStateType.Fee,
|
||||
visible = !isEditingDisabled && isCorrectScreen,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
|
@ -94,11 +96,12 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
|
|||
) { textId ->
|
||||
when {
|
||||
currentState.value == SendUiStateType.Send && !isSuccess -> {
|
||||
val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick)
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = isButtonEnabled,
|
||||
onClick = buttonClick,
|
||||
onClick = hapticFeedback,
|
||||
showProgress = isSending,
|
||||
)
|
||||
}
|
||||
|
|
@ -107,6 +110,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
|
|||
textRes = textId,
|
||||
txUrl = txUrl,
|
||||
onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) },
|
||||
onShareClick = uiState.clickIntents::onShareClick,
|
||||
onDoneClick = buttonClick,
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
@ -127,6 +131,7 @@ private fun PrimaryButtonsDone(
|
|||
@StringRes textRes: Int,
|
||||
txUrl: String,
|
||||
onExploreClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -149,6 +154,7 @@ private fun PrimaryButtonsDone(
|
|||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
context.shareText(txUrl)
|
||||
onShareClick()
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
|
@ -170,6 +176,7 @@ private fun getButtonData(
|
|||
isSuccess: Boolean,
|
||||
): Pair<Int, () -> Unit> {
|
||||
return when (currentState.value) {
|
||||
SendUiStateType.None,
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
SendUiStateType.Recipient -> R.string.send_recipient_label
|
||||
SendUiStateType.Fee -> R.string.common_fee_selector_title
|
||||
SendUiStateType.Send -> if (!isSuccess) R.string.send_confirm_label else null
|
||||
else -> null
|
||||
}
|
||||
val iconRes = if (currentState.value == SendUiStateType.Recipient) {
|
||||
R.drawable.ic_qrcode_scan_24
|
||||
|
|
@ -89,19 +90,21 @@ private fun SendScreenContent(
|
|||
) { state ->
|
||||
when (state) {
|
||||
SendUiStateType.Amount -> SendAmountContent(
|
||||
uiState.amountState,
|
||||
uiState.clickIntents,
|
||||
amountState = uiState.amountState,
|
||||
isBalanceHiding = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState.recipientState,
|
||||
uiState.clickIntents,
|
||||
recipientList,
|
||||
uiState = uiState.recipientState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
recipientList = recipientList,
|
||||
)
|
||||
SendUiStateType.Fee -> SendSpeedAndFeeContent(
|
||||
uiState.feeState,
|
||||
uiState.clickIntents,
|
||||
state = uiState.feeState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Send -> SendContent(uiState)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,10 +28,10 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean)
|
|||
sendField.value to sendField.fiatValue
|
||||
}
|
||||
|
||||
val (primaryAmount, secondaryAmount) = if (!isFiat) {
|
||||
sendField.cryptoAmount to sendField.fiatAmount
|
||||
} else {
|
||||
val (primaryAmount, secondaryAmount) = if (isFiat) {
|
||||
sendField.fiatAmount to sendField.cryptoAmount
|
||||
} else {
|
||||
sendField.cryptoAmount to sendField.fiatAmount
|
||||
}
|
||||
|
||||
AmountTextField(
|
||||
|
|
@ -40,6 +40,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean)
|
|||
symbol = primaryAmount.currencySymbol,
|
||||
onValueChange = sendField.onValueChange,
|
||||
keyboardOptions = sendField.keyboardOptions,
|
||||
keyboardActions = sendField.keyboardActions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.amount
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -11,13 +12,18 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.common.Strings.STARS
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
|
||||
internal fun AmountFieldContainer(
|
||||
amountState: SendStates.AmountState,
|
||||
isBalanceHiding: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
|
|
@ -37,14 +43,21 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier:
|
|||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing14),
|
||||
)
|
||||
Text(
|
||||
text = amountState.walletBalance.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
|
||||
val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference()
|
||||
AnimatedContent(
|
||||
targetState = balance,
|
||||
label = "Hide Balance Animation",
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
TokenIcon(
|
||||
state = amountState.tokenIconState,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -22,13 +22,17 @@ import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegment
|
|||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
@Composable
|
||||
internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) {
|
||||
internal fun SendAmountContent(
|
||||
amountState: SendStates.AmountState?,
|
||||
isBalanceHiding: Boolean,
|
||||
clickIntents: SendClickIntents,
|
||||
) {
|
||||
if (amountState == null) return
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
AmountFieldContainer(amountState = amountState)
|
||||
AmountFieldContainer(amountState = amountState, isBalanceHiding = isBalanceHiding)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ internal fun SendCustomFeeEthereum(
|
|||
selectedFee: FeeType,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) {
|
||||
if (selectedFee == FeeType.Custom && customValues.isNotEmpty()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = modifier,
|
||||
|
|
@ -38,6 +38,7 @@ internal fun SendCustomFeeEthereum(
|
|||
title = value.title,
|
||||
info = value.label,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
keyboardActions = value.keyboardActions,
|
||||
onValueChange = value.onValueChange,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
|
|
@ -54,6 +55,7 @@ internal fun SendCustomFeeEthereum(
|
|||
symbol = value.symbol,
|
||||
onValueChange = value.onValueChange,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
keyboardActions = value.keyboardActions,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ import com.tangem.core.ui.components.notifications.Notification
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
|
||||
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
|
||||
if (state == null) return
|
||||
|
|
@ -36,46 +37,92 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
) {
|
||||
SendSpeedSelector(
|
||||
state = state,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
feeSelector(state, clickIntents)
|
||||
topNotifications(notifications)
|
||||
customFee(feeSendState)
|
||||
notifications(notifications)
|
||||
subtractButton(
|
||||
receivedAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
isSubtractAvailable = state.isSubtractAvailable,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
middleNotifications(notifications)
|
||||
subtractButton(state, clickIntents)
|
||||
bottomNotifications(notifications)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotification>, modifier: Modifier = Modifier) {
|
||||
private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: SendClickIntents) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
) {
|
||||
SendSpeedSelector(
|
||||
state = state,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.topNotifications(
|
||||
configs: ImmutableList<SendFeeNotification>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
notifications(
|
||||
configs = configs.filter {
|
||||
it is SendFeeNotification.Error.ExceedsBalance ||
|
||||
it is SendFeeNotification.Warning.NetworkFeeUnreachable
|
||||
}.toImmutableList(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.middleNotifications(
|
||||
configs: ImmutableList<SendFeeNotification>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
notifications(
|
||||
configs = configs.filter {
|
||||
it is SendFeeNotification.Warning.TooLow ||
|
||||
it is SendFeeNotification.Warning.TooHigh
|
||||
}.toImmutableList(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.bottomNotifications(
|
||||
configs: ImmutableList<SendFeeNotification>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
notifications(
|
||||
configs = configs.filterIsInstance<SendFeeNotification.Warning.NetworkCoverage>().toImmutableList(),
|
||||
isLast = true,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.notifications(
|
||||
configs: ImmutableList<SendFeeNotification>,
|
||||
modifier: Modifier = Modifier,
|
||||
isLast: Boolean = false,
|
||||
) {
|
||||
items(
|
||||
items = configs,
|
||||
key = { it::class.java },
|
||||
contentType = { it::class.java },
|
||||
itemContent = {
|
||||
val bottomPadding = if (isLast) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = bottomPadding,
|
||||
)
|
||||
.animateItemPlacement(),
|
||||
containerColor = when (it) {
|
||||
is SendFeeNotification.Error.ExceedsBalance,
|
||||
is SendFeeNotification.Warning.NetworkFeeUnreachable,
|
||||
-> TangemTheme.colors.background.primary
|
||||
-> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (it) {
|
||||
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is SendFeeNotification.Error.ExceedsBalance -> {
|
||||
if (it.config.buttonsState == null) {
|
||||
TangemTheme.colors.icon.warning
|
||||
|
|
@ -116,20 +163,30 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M
|
|||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.subtractButton(
|
||||
receivedAmount: String,
|
||||
isSubtract: Boolean,
|
||||
isSubtractAvailable: Boolean,
|
||||
state: SendStates.FeeState,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val receivedAmount = state.receivedAmount
|
||||
val isSubtract = state.isSubtract
|
||||
val isSubtractAvailable = state.isSubtractAvailable
|
||||
val feeSendState = state.feeSelectorState
|
||||
if (isSubtractAvailable) {
|
||||
item {
|
||||
val feeStateContent = feeSendState as? FeeSelectorState.Content
|
||||
val isCustomAvailable = feeStateContent?.customValues.isNullOrEmpty().not()
|
||||
val isCustomSelected = feeStateContent?.selectedFee == FeeType.Custom
|
||||
val topPadding = if (isCustomSelected && isCustomAvailable) {
|
||||
TangemTheme.dimens.spacing12
|
||||
} else {
|
||||
TangemTheme.dimens.spacing20
|
||||
}
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = receivedAmount,
|
||||
isSubtract = isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing12)
|
||||
.padding(top = topPadding)
|
||||
.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -39,6 +42,7 @@ import com.tangem.features.send.impl.R
|
|||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -84,8 +88,8 @@ internal fun SendSpeedSelector(
|
|||
amount = getCryptoReference(minimumAmount, state.isFeeApproximate),
|
||||
fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency),
|
||||
symbolLength = minimumAmount.currencySymbol.length,
|
||||
isSelected = isSelected == FeeType.SLOW,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) },
|
||||
isSelected = isSelected == FeeType.Slow,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
|
||||
)
|
||||
val normalAmount = fees.normal.amount
|
||||
SendSpeedSelectorItem(
|
||||
|
|
@ -94,8 +98,8 @@ internal fun SendSpeedSelector(
|
|||
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
|
||||
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
|
||||
symbolLength = normalAmount.currencySymbol.length,
|
||||
isSelected = isSelected == FeeType.MARKET,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
isSelected = isSelected == FeeType.Market,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
|
||||
)
|
||||
val priorityAmount = fees.priority.amount
|
||||
SendSpeedSelectorItem(
|
||||
|
|
@ -104,20 +108,25 @@ internal fun SendSpeedSelector(
|
|||
amount = getCryptoReference(priorityAmount, state.isFeeApproximate),
|
||||
fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency),
|
||||
symbolLength = priorityAmount.currencySymbol.length,
|
||||
isSelected = isSelected == FeeType.FAST,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
|
||||
isSelected = isSelected == FeeType.Fast,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
|
||||
showDivider = fees.normal is Fee.Ethereum,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = fees.normal is Fee.Ethereum,
|
||||
label = "Custom fee appearance animation",
|
||||
) {
|
||||
val showWarning = state.notifications.any {
|
||||
it is SendFeeNotification.Warning.TooHigh ||
|
||||
it is SendFeeNotification.Warning.TooLow
|
||||
}
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
isSelected = isSelected == FeeType.CUSTOM,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) },
|
||||
isSelected = isSelected == FeeType.Custom,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
|
||||
showDivider = fees.normal !is Fee.Ethereum,
|
||||
showWarning = showWarning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +139,7 @@ internal fun SendSpeedSelector(
|
|||
amount = getCryptoReference(normalAmount, state.isFeeApproximate),
|
||||
fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency),
|
||||
symbolLength = normalAmount.currencySymbol.length,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) },
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
|
||||
showDivider = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -224,6 +233,7 @@ private fun SendSpeedSelectorItem(
|
|||
symbolLength: Int? = null,
|
||||
isSelected: Boolean = false,
|
||||
showDivider: Boolean = true,
|
||||
showWarning: Boolean = false,
|
||||
) {
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
|
|
@ -259,7 +269,10 @@ private fun SendSpeedSelectorItem(
|
|||
symbolLength = symbolLength,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
} else {
|
||||
SpacerWMax()
|
||||
}
|
||||
WarningIcon(showWarning = showWarning)
|
||||
}
|
||||
if (showDivider) {
|
||||
Box(
|
||||
|
|
@ -340,6 +353,26 @@ private fun RowScope.SelectorValueContent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningIcon(showWarning: Boolean = false) {
|
||||
AnimatedVisibility(
|
||||
visible = showWarning,
|
||||
label = "Custom fee warning indicator",
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_alert_triangle_20),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.InputRowRecipient
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
|
|
@ -58,7 +59,7 @@ internal fun SendRecipientContent(
|
|||
title = address.label,
|
||||
placeholder = address.placeholder,
|
||||
onValueChange = address.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientAddressValueChange,
|
||||
onPasteClick = { clickIntents.onRecipientAddressValueChange(it, EnterAddressSource.PasteButton) },
|
||||
isError = isError,
|
||||
isLoading = isValidating,
|
||||
error = address.error,
|
||||
|
|
@ -73,16 +74,18 @@ internal fun SendRecipientContent(
|
|||
}
|
||||
uiState.memoTextField?.let { memoField ->
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText
|
||||
TextFieldWithPaste(
|
||||
value = memoField.value,
|
||||
label = memoField.label,
|
||||
placeholder = memoField.placeholder,
|
||||
placeholder = placeholder,
|
||||
footer = stringResource(R.string.send_recipient_memo_footer),
|
||||
onValueChange = memoField.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientMemoValueChange,
|
||||
onPasteClick = { clickIntents.onRecipientMemoValueChange(it, isPasted = true) },
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
isReadOnly = !memoField.isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +166,9 @@ private fun LazyListScope.recipientListItem(
|
|||
},
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
onClick = {
|
||||
clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +206,7 @@ private fun RecipientWalletListItem(
|
|||
ListItemWithIcon(
|
||||
title = wallet.title.resolveReference(),
|
||||
subtitle = wallet.subtitle.resolveReference(),
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title) },
|
||||
onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) },
|
||||
)
|
||||
}
|
||||
if (!item.isWalletsOnly) {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@ internal fun TextFieldWithPaste(
|
|||
footer: String? = null,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
isReadOnly: Boolean = false,
|
||||
) {
|
||||
val (title, color) = if (isError && error != null) {
|
||||
error to TangemTheme.colors.text.warning
|
||||
} else {
|
||||
label to TangemTheme.colors.text.secondary
|
||||
val (title, color) = when {
|
||||
isError && error != null -> error to TangemTheme.colors.text.warning
|
||||
isReadOnly -> label to TangemTheme.colors.text.disabled
|
||||
else -> label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
FooterContainer(modifier, footer) {
|
||||
Row(
|
||||
|
|
@ -55,18 +56,21 @@ internal fun TextFieldWithPaste(
|
|||
value = value,
|
||||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
readOnly = isReadOnly,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
if (!isReadOnly) {
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
modifier = Modifier
|
||||
.align(CenterVertically)
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -17,6 +18,7 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.common.Strings
|
||||
import com.tangem.core.ui.components.inputrow.InputRowDefault
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImage
|
||||
import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault
|
||||
|
|
@ -65,18 +67,21 @@ internal fun SendContent(uiState: SendUiState) {
|
|||
FromWallet(
|
||||
walletName = amountState.walletName,
|
||||
walletBalance = amountState.walletBalance.resolveReference(),
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isSuccess = isSuccess,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
RecipientBlock(
|
||||
recipientState = recipientState,
|
||||
isSuccess = isSuccess,
|
||||
isEditingDisabled = uiState.isEditingDisabled,
|
||||
onClick = uiState.clickIntents::showRecipient,
|
||||
)
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isSuccess = isSuccess,
|
||||
isEditingDisabled = uiState.isEditingDisabled,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
FeeBlock(
|
||||
feeState = feeState,
|
||||
isSuccess = isSuccess,
|
||||
|
|
@ -89,7 +94,7 @@ internal fun SendContent(uiState: SendUiState) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun FromWallet(walletName: String, walletBalance: String) {
|
||||
private fun FromWallet(walletName: String, walletBalance: String, isBalanceHidden: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -108,20 +113,31 @@ private fun FromWallet(walletName: String, walletBalance: String) {
|
|||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = walletBalance,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
val balance = if (isBalanceHidden) Strings.STARS else walletBalance
|
||||
AnimatedContent(
|
||||
targetState = balance,
|
||||
label = "Hide Balance Animation",
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
private fun AmountBlock(
|
||||
amountState: SendStates.AmountState,
|
||||
isSuccess: Boolean,
|
||||
isEditingDisabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val amount = amountState.amountTextField
|
||||
|
||||
val cryptoAmount = formatCryptoAmount(
|
||||
|
|
@ -134,6 +150,11 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
|
|||
fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code,
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
)
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
TangemTheme.colors.background.action
|
||||
}
|
||||
InputRowImage(
|
||||
title = TextReference.Res(R.string.send_amount_label),
|
||||
subtitle = TextReference.Str(cryptoAmount),
|
||||
|
|
@ -142,21 +163,31 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
|
|||
showNetworkIcon = true,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess) { onClick() },
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||
private fun RecipientBlock(
|
||||
recipientState: SendStates.RecipientState,
|
||||
isSuccess: Boolean,
|
||||
isEditingDisabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val address = recipientState.addressTextField
|
||||
val memo = recipientState.memoTextField
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
TangemTheme.colors.background.action
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isSuccess) { onClick() },
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() },
|
||||
) {
|
||||
val showMemo = memo != null && memo.value.isNotBlank()
|
||||
InputRowRecipientDefault(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package com.tangem.features.send.impl.presentation.viewmodel
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface SendClickIntents {
|
||||
internal interface SendClickIntents {
|
||||
|
||||
fun popBackStack()
|
||||
|
||||
|
|
@ -30,9 +31,9 @@ interface SendClickIntents {
|
|||
// endregion
|
||||
|
||||
// region Recipient
|
||||
fun onRecipientAddressValueChange(value: String)
|
||||
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String)
|
||||
fun onRecipientMemoValueChange(value: String, isPasted: Boolean = false)
|
||||
// endregion
|
||||
|
||||
// region Fee
|
||||
|
|
@ -56,6 +57,8 @@ interface SendClickIntents {
|
|||
|
||||
fun onExploreClick(txUrl: String)
|
||||
|
||||
fun onShareClick()
|
||||
|
||||
fun onAmountReduceClick(reducedAmount: String)
|
||||
|
||||
fun onAmountReduceIgnoreClick()
|
||||
|
|
|
|||
|
|
@ -7,19 +7,18 @@ import androidx.lifecycle.*
|
|||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
|
|
@ -36,12 +35,19 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
||||
import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender
|
||||
import com.tangem.features.send.impl.presentation.analytics.utils.SendRecipientAnalyticsSender
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.*
|
||||
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -74,6 +80,9 @@ internal class SendViewModel @Inject constructor(
|
|||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
|
|
@ -88,6 +97,10 @@ internal class SendViewModel @Inject constructor(
|
|||
private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
|
||||
?: error("This screen can't open without `CryptoCurrency`")
|
||||
|
||||
private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY]
|
||||
private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY]
|
||||
private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY]
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private var innerRouter: InnerSendRouter by Delegates.notNull()
|
||||
|
|
@ -103,6 +116,11 @@ internal class SendViewModel @Inject constructor(
|
|||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
||||
private val amountStateFactory = AmountStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
|
|
@ -132,10 +150,16 @@ internal class SendViewModel @Inject constructor(
|
|||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
private val sendOnNextScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendOnNextScreenAnalyticSender(analyticsEventHandler)
|
||||
}
|
||||
private val sendRecipientAnalyticsSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientAnalyticsSender(analyticsEventHandler)
|
||||
}
|
||||
// todo convert to StateFlow
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
|
@ -158,6 +182,8 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
onStateActive()
|
||||
subscribeOnBalanceHidden(owner)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SendOpened)
|
||||
}
|
||||
|
||||
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
|
||||
|
|
@ -184,6 +210,17 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnBalanceHidden(owner: LifecycleOwner) {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun getCurrenciesStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
|
||||
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
|
||||
|
||||
|
|
@ -192,11 +229,10 @@ internal class SendViewModel @Inject constructor(
|
|||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { currencyStatus ->
|
||||
currencyStatus.onRight {
|
||||
cryptoCurrencyStatus = it
|
||||
coinCryptoCurrencyStatus = it
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
updateNotifications()
|
||||
onDataLoaded(
|
||||
currencyStatus = it,
|
||||
coinCurrencyStatus = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
|
|
@ -208,10 +244,10 @@ internal class SendViewModel @Inject constructor(
|
|||
flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
|
||||
) { coinStatus, currencyStatus ->
|
||||
if (coinStatus.isRight() && currencyStatus.isRight()) {
|
||||
coinStatus.onRight { coinCryptoCurrencyStatus = it }
|
||||
currencyStatus.onRight { cryptoCurrencyStatus = it }
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
onDataLoaded(
|
||||
currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") },
|
||||
coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") },
|
||||
)
|
||||
}
|
||||
}.flowWithLifecycle(owner.lifecycle)
|
||||
.flowOn(dispatchers.main)
|
||||
|
|
@ -245,6 +281,21 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) {
|
||||
cryptoCurrencyStatus = currencyStatus
|
||||
coinCryptoCurrencyStatus = coinCurrencyStatus
|
||||
|
||||
if (transactionId != null && amount != null && destinationAddress != null) {
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress)
|
||||
stateRouter.showFee()
|
||||
} else {
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
stateRouter.showRecipient()
|
||||
}
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getUserWallets().conflate(),
|
||||
|
|
@ -281,10 +332,12 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletId = wallet.walletId,
|
||||
network = walletCurrency.network,
|
||||
)
|
||||
return@fold AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = addresses.first().value,
|
||||
)
|
||||
return@fold addresses.firstOrNull()?.let {
|
||||
AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = it.value,
|
||||
)
|
||||
}
|
||||
},
|
||||
ifLeft = { null },
|
||||
)
|
||||
|
|
@ -352,11 +405,18 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region screen state navigation
|
||||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() = stateRouter.onBackClick()
|
||||
override fun onNextClick() = stateRouter.onNextClick()
|
||||
override fun onBackClick() = stateRouter.onBackClick(uiState.sendState.isSuccess)
|
||||
override fun onNextClick() {
|
||||
val prevScreen = stateRouter.onNextClick()
|
||||
sendOnNextScreenAnalyticSender.send(prevScreen, uiState)
|
||||
}
|
||||
|
||||
override fun onPrevClick() = stateRouter.onPrevClick()
|
||||
|
||||
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
|
||||
override fun onQrCodeScanClick() {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.QrCodeButtonClicked)
|
||||
innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
|
||||
}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage))
|
||||
|
|
@ -368,24 +428,16 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
uiState = stateFactory.getOnCurrencyChangedState(isFiat)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SwapCurrencyButtonClicked)
|
||||
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(value)
|
||||
uiState = amountStateFactory.getOnAmountValueChange(value)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState.amountState ?: return
|
||||
val amountTextField = amountState.amountTextField
|
||||
val (amount, decimals) = if (amountTextField.isFiatValue) {
|
||||
cryptoCurrencyStatus.value.fiatAmount to amountTextField.fiatAmount.decimals
|
||||
} else {
|
||||
cryptoCurrencyStatus.value.amount to amountTextField.cryptoAmount.decimals
|
||||
}
|
||||
if (amount != null && !amount.isZero()) {
|
||||
onAmountValueChange(amount.parseBigDecimal(decimals))
|
||||
}
|
||||
uiState = amountStateFactory.getOnMaxAmountClick()
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -394,7 +446,7 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address)
|
||||
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
|
||||
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
|
|
@ -405,24 +457,26 @@ internal class SendViewModel @Inject constructor(
|
|||
}.saveIn(qrScannerJobHolder)
|
||||
}
|
||||
|
||||
override fun onRecipientAddressValueChange(value: String) {
|
||||
uiState = stateFactory.onRecipientAddressValueChange(value)
|
||||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = stateFactory.getOnRecipientAddressValidationStarted()
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.onRecipientAddressValueChange(value)
|
||||
uiState = stateFactory.getOnRecipientAddressValidationStarted()
|
||||
val isValidAddress = validateAddress(value)
|
||||
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
sendRecipientAnalyticsSender.sendAddressAnalytics(type, isValidAddress)
|
||||
}
|
||||
}.saveIn(addressValidationJobHolder)
|
||||
}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String) {
|
||||
uiState = stateFactory.getOnRecipientMemoValueChange(value)
|
||||
override fun onRecipientMemoValueChange(value: String, isPasted: Boolean) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = stateFactory.getOnRecipientAddressValidationStarted()
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = stateFactory.getOnRecipientMemoValueChange(value)
|
||||
uiState = stateFactory.getOnRecipientAddressValidationStarted()
|
||||
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
|
||||
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
|
||||
sendRecipientAnalyticsSender.sendMemoAnalytics(isPasted)
|
||||
}
|
||||
}.saveIn(addressValidationJobHolder)
|
||||
}
|
||||
|
|
@ -435,16 +489,14 @@ internal class SendViewModel @Inject constructor(
|
|||
).getOrElse { false }
|
||||
}
|
||||
|
||||
private fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val result = XrpAddressService.decodeXAddress(value)
|
||||
onRecipientAddressValueChange(result?.address.orEmpty())
|
||||
onRecipientMemoValueChange(result?.destinationTag.toString())
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
|
||||
uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true)
|
||||
uiState = stateFactory.getOnXAddressMemoState()
|
||||
val isValidAddress = validateAddress(decodedAddress.address)
|
||||
uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -454,6 +506,9 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||
updateFeeNotifications()
|
||||
if (feeType == FeeType.Custom) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
|
|
@ -514,18 +569,35 @@ internal class SendViewModel @Inject constructor(
|
|||
onCheckFeeUpdate()
|
||||
}
|
||||
sendIdleTimer = System.currentTimeMillis()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SendButtonClicked)
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount()
|
||||
override fun showAmount() {
|
||||
stateRouter.showAmount()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
||||
}
|
||||
|
||||
override fun showRecipient() = stateRouter.showRecipient()
|
||||
override fun showRecipient() {
|
||||
stateRouter.showRecipient()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
||||
}
|
||||
|
||||
override fun showFee() = stateRouter.showFee()
|
||||
override fun showFee() {
|
||||
stateRouter.showFee()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
||||
}
|
||||
|
||||
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
|
||||
override fun onExploreClick(txUrl: String) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
||||
innerRouter.openUrl(txUrl)
|
||||
}
|
||||
|
||||
override fun onShareClick() {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
|
||||
}
|
||||
|
||||
override fun onAmountReduceClick(reducedAmount: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(reducedAmount)
|
||||
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount)
|
||||
uiState = sendNotificationFactory.dismissHighFeeWarningState()
|
||||
loadFee()
|
||||
}
|
||||
|
|
@ -588,6 +660,7 @@ internal class SendViewModel @Inject constructor(
|
|||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
scheduleBalanceUpdate()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -645,7 +718,6 @@ internal class SendViewModel @Inject constructor(
|
|||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val CHECK_FEE_UPDATE_DELAY = 60_000L
|
||||
private const val BALANCE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import com.tangem.datasource.crypto.DataSignatureVerifier
|
|||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -298,14 +297,6 @@ internal class DefaultSwapRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? {
|
||||
return walletManagersFacade.getExistentialDeposit(userWalletId, network)
|
||||
}
|
||||
|
||||
override suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal? {
|
||||
return walletManagersFacade.getDustValue(userWalletId, network)
|
||||
}
|
||||
|
||||
private fun parseTxDetails(txDetailsJson: String): TxDetails? {
|
||||
return try {
|
||||
txDetailsMoshiAdapter.fromJson(txDetailsJson)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.feature.swap.domain.api
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
|
|
@ -69,8 +68,4 @@ interface SwapRepository {
|
|||
): Either<DataError, SwapDataModel>
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
|
||||
|
||||
suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal?
|
||||
|
||||
suspend fun getDustValue(userWalletId: UserWalletId, network: Network): BigDecimal?
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
|||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
|
|
@ -58,6 +59,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val quotesRepository: QuotesRepository,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
private val appCurrencyRepository: AppCurrencyRepository,
|
||||
private val initialToCurrencyResolver: InitialToCurrencyResolver,
|
||||
) : SwapInteractor {
|
||||
|
|
@ -380,7 +382,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount: SwapAmount,
|
||||
fromToken: CryptoCurrency,
|
||||
) {
|
||||
val existentialDeposit = repository.getExistentialDeposit(userWalletId, fromToken.network)
|
||||
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, fromToken.network)
|
||||
if (existentialDeposit != null) {
|
||||
val nativeBalance = userWalletManager.getNativeTokenBalance(
|
||||
fromToken.network.backendId,
|
||||
|
|
@ -404,7 +406,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue
|
||||
is TxFeeState.SingleFeeState -> feeState.fee.feeValue
|
||||
}
|
||||
val dust = repository.getDustValue(userWalletId, fromTokenStatus.currency.network)
|
||||
val dust = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network)
|
||||
val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO
|
||||
if (dust != null &&
|
||||
!balance.isNullOrZero() &&
|
||||
|
|
@ -664,6 +666,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
Fee.Aptos(
|
||||
amount = feeAmount,
|
||||
gasUnitPrice = fee.feeValue.toLong() / fee.gasLimit,
|
||||
gasLimit = fee.gasLimit.toLong(),
|
||||
)
|
||||
}
|
||||
else -> Fee.Common(feeAmount)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.demo.IsDemoCardUseCase
|
|||
import com.tangem.domain.tokens.GetCardTokensListUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -42,6 +43,7 @@ class SwapDomainModule {
|
|||
quotesRepository: QuotesRepository,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
appCurrencyRepository: AppCurrencyRepository,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
initialToCurrencyResolver: InitialToCurrencyResolver,
|
||||
|
|
@ -59,6 +61,7 @@ class SwapDomainModule {
|
|||
dispatcher = coroutineDispatcherProvider,
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
appCurrencyRepository = appCurrencyRepository,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
initialToCurrencyResolver = initialToCurrencyResolver,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.tester.api
|
||||
|
||||
/**
|
||||
* Tester feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TesterFeatureToggles {
|
||||
|
||||
val isDerivePublicKeysRefactoringEnabled: Boolean
|
||||
}
|
||||
|
|
@ -27,17 +27,18 @@ dependencies {
|
|||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(project(":core:featuretoggles"))
|
||||
implementation(project(":core:ui"))
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(project(":features:tester:api"))
|
||||
|
||||
/** Other modules */
|
||||
implementation(project(":libs:crypto"))
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tester.api)
|
||||
|
||||
/** Other modules */
|
||||
implementation(projects.libs.crypto)
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.feature.tester.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.feature.tester.featuretoggles.DefaultTesterFeatureToggles
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TesterFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTesterFeatureToggles(featureTogglesManager: FeatureTogglesManager): TesterFeatureToggles {
|
||||
return DefaultTesterFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.feature.tester.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.tester.api.TesterFeatureToggles
|
||||
|
||||
/**
|
||||
* Default implementation of Tester feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTesterFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TesterFeatureToggles {
|
||||
|
||||
override val isDerivePublicKeysRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "DERIVE_PUBLIC_KEYS_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -4,24 +4,29 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
|
||||
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel for screen with list of feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@HiltViewModel
|
||||
internal class FeatureTogglesViewModel @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
/** Current ui state */
|
||||
|
|
@ -47,9 +52,11 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onToggleValueChange(name: String, isEnabled: Boolean) {
|
||||
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
|
||||
|
||||
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
|
||||
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List<TesterFeatureToggle> {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsPendingTxToTransactionStateConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -29,7 +29,7 @@ internal class TokenDetailsLoadedBalanceConverter(
|
|||
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
|
||||
|
||||
private val txHistoryItemConverter by lazy {
|
||||
TokenDetailsPendingTxToTransactionStateConverter(symbol, decimals, clickIntents)
|
||||
TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents)
|
||||
}
|
||||
|
||||
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toBriefAddressFormat
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
||||
// FIXME: Refactoring needed
|
||||
/** Same as [TokenDetailsTxHistoryTransactionStateConverter] but with other timestamp format */
|
||||
internal class TokenDetailsPendingTxToTransactionStateConverter(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Converter<TxHistoryItem, TransactionState> {
|
||||
|
||||
override fun convert(value: TxHistoryItem): TransactionState {
|
||||
return createTransactionStateItem(item = value)
|
||||
}
|
||||
|
||||
private fun createTransactionStateItem(item: TxHistoryItem): TransactionState {
|
||||
return TransactionState.Content(
|
||||
txHash = item.txHash,
|
||||
amount = item.getAmount(),
|
||||
timestamp = item.timestampInMillis.toTimeFormat(),
|
||||
status = item.status.tiUiStatus(),
|
||||
direction = item.extractDirection(),
|
||||
iconRes = item.extractIcon(),
|
||||
title = item.extractTitle(),
|
||||
subtitle = item.extractSubtitle(),
|
||||
onClick = { clickIntents.onTransactionClick(item.txHash) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) {
|
||||
R.drawable.ic_close_24
|
||||
} else {
|
||||
when (type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TxHistoryItem.TransactionType.Operation,
|
||||
is TxHistoryItem.TransactionType.Swap,
|
||||
is TxHistoryItem.TransactionType.Transfer,
|
||||
is TxHistoryItem.TransactionType.UnknownOperation,
|
||||
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) {
|
||||
is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval)
|
||||
is TxHistoryItem.TransactionType.Operation -> stringReference(type.name)
|
||||
is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap)
|
||||
is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer)
|
||||
is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractSubtitle(): TextReference =
|
||||
when (val interactionAddress = interactionAddressType) {
|
||||
is TxHistoryItem.InteractionAddressType.Contract -> resourceReference(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)),
|
||||
)
|
||||
is TxHistoryItem.InteractionAddressType.User -> resourceReference(
|
||||
id = if (isOutgoing) {
|
||||
R.string.transaction_history_transaction_to_address
|
||||
} else {
|
||||
R.string.transaction_history_transaction_from_address
|
||||
},
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) {
|
||||
TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
|
||||
TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
|
||||
TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed
|
||||
}
|
||||
|
||||
private fun Long.toTimeFormat(): String {
|
||||
return DateTimeFormatters.formatTime(time = DateTime(this, DateTimeZone.getDefault()))
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() =
|
||||
if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
val prefix = when (status) {
|
||||
TxHistoryItem.TransactionStatus.Failed -> ""
|
||||
else -> if (isOutgoing) "-" else "+"
|
||||
}
|
||||
return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
|
|||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
|
|
@ -52,8 +51,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE,
|
||||
item = TxHistoryItemState.Title(clickIntents::onExploreClick),
|
||||
)
|
||||
.insertGroupTitle() // method uses the raw timestamp
|
||||
.formatTransactionsTimestamp() // method formats the timestamp
|
||||
.insertGroupTitle()
|
||||
}
|
||||
}
|
||||
.launchIn(CoroutineScope(Dispatchers.IO))
|
||||
|
|
@ -94,28 +92,10 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the [PagingData] to format the [TxHistoryItemState] timestamp
|
||||
*/
|
||||
private fun PagingData<TxHistoryItemState>.formatTransactionsTimestamp(): PagingData<TxHistoryItemState> {
|
||||
return map { txHistoryItemState ->
|
||||
if (txHistoryItemState is TxHistoryItemState.Transaction &&
|
||||
txHistoryItemState.state is TransactionState.Content
|
||||
) {
|
||||
val txContent = txHistoryItemState.state as TransactionState.Content
|
||||
txHistoryItemState.copy(
|
||||
state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()),
|
||||
)
|
||||
} else {
|
||||
txHistoryItemState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItemState?.getTimestamp(): Long? {
|
||||
return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) {
|
||||
val txContent = this.state as TransactionState.Content
|
||||
requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" }
|
||||
txContent.timestamp
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
|
@ -13,8 +14,6 @@ import com.tangem.utils.converter.Converter
|
|||
import com.tangem.utils.toBriefAddressFormat
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
|
||||
// FIXME: Refactoring needed
|
||||
/** Same as [TokenDetailsPendingTxToTransactionStateConverter] but with other timestamp format */
|
||||
internal class TokenDetailsTxHistoryTransactionStateConverter(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
|
|
@ -30,12 +29,13 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
return TransactionState.Content(
|
||||
txHash = item.txHash,
|
||||
amount = item.getAmount(),
|
||||
timestamp = item.getRawTimestamp(),
|
||||
time = item.timestampInMillis.toTimeFormat(),
|
||||
status = item.status.tiUiStatus(),
|
||||
direction = item.extractDirection(),
|
||||
iconRes = item.extractIcon(),
|
||||
title = item.extractTitle(),
|
||||
subtitle = item.extractSubtitle(),
|
||||
timestamp = item.timestampInMillis,
|
||||
onClick = { clickIntents.onTransactionClick(item.txHash) },
|
||||
)
|
||||
}
|
||||
|
|
@ -87,14 +87,6 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
|
||||
private fun TxHistoryItem.extractDirection() = if (isOutgoing) Direction.OUTGOING else Direction.INCOMING
|
||||
|
||||
/**
|
||||
* Get timestamp without formatting.
|
||||
* It's life hack that help us to add transaction's group title to flow.
|
||||
*
|
||||
* @see [convert]
|
||||
*/
|
||||
private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString()
|
||||
|
||||
private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) {
|
||||
TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed
|
||||
TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.wallet.featuretoggles
|
||||
|
||||
/**
|
||||
* Wallet feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface WalletFeatureToggles {
|
||||
|
||||
val isWalletsScrollingPreviewEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.feature.wallet.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.feature.wallet.featuretoggles.DefaultWalletFeatureToggles
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object WalletFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletFeatureToggles {
|
||||
return DefaultWalletFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.feature.wallet.di
|
|||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import com.tangem.features.wallet.navigation.WalletRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -16,10 +15,7 @@ internal object WalletRouterModule {
|
|||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideWalletRouter(
|
||||
reduxNavController: ReduxNavController,
|
||||
walletFeatureToggles: WalletFeatureToggles,
|
||||
): WalletRouter {
|
||||
return DefaultWalletRouter(reduxNavController = reduxNavController, walletFeatureToggles = walletFeatureToggles)
|
||||
fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter {
|
||||
return DefaultWalletRouter(reduxNavController = reduxNavController)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.feature.wallet.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
|
||||
/**
|
||||
* Default implementation of Wallet feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultWalletFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : WalletFeatureToggles {
|
||||
|
||||
override val isWalletsScrollingPreviewEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "WALLETS_SCROLLING_PREVIEW_ENABLED")
|
||||
}
|
||||
|
|
@ -1,17 +1,10 @@
|
|||
package com.tangem.feature.wallet.presentation.common
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
|
@ -19,16 +12,9 @@ import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.*
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("LargeClass")
|
||||
|
|
@ -80,14 +66,6 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
val walletListConfig by lazy {
|
||||
WalletsListConfig(
|
||||
selectedWalletIndex = 0,
|
||||
wallets = wallets.values.toPersistentList(),
|
||||
onWalletChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
val coinIconState
|
||||
get() = TokenIconState.CoinIcon(
|
||||
url = null,
|
||||
|
|
@ -320,144 +298,4 @@ internal object WalletPreviewData {
|
|||
),
|
||||
).toImmutableList(),
|
||||
)
|
||||
|
||||
private val manageButtons by lazy {
|
||||
persistentListOf(
|
||||
WalletManageButton.Buy(enabled = true, onClick = {}),
|
||||
WalletManageButton.Send(enabled = true, onClick = {}),
|
||||
WalletManageButton.Receive(enabled = true, onClick = {}),
|
||||
WalletManageButton.Sell(enabled = true, onClick = {}),
|
||||
WalletManageButton.Swap(enabled = true, onClick = {}),
|
||||
)
|
||||
}
|
||||
|
||||
val multicurrencyWalletScreenState by lazy {
|
||||
WalletMultiCurrencyState.Content(
|
||||
onBackClick = {},
|
||||
topBarConfig = topBarConfig,
|
||||
walletsListConfig = walletListConfig,
|
||||
tokensListState = WalletTokensListState.Content(
|
||||
persistentListOf(
|
||||
TokensListItemState.NetworkGroupTitle(id = 0, stringReference("Bitcoin")),
|
||||
TokensListItemState.Token(
|
||||
tokenItemVisibleState.copy(
|
||||
id = "token_1",
|
||||
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
|
||||
),
|
||||
),
|
||||
TokensListItemState.Token(
|
||||
tokenItemVisibleState.copy(
|
||||
id = "token_2",
|
||||
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
|
||||
),
|
||||
),
|
||||
TokensListItemState.Token(
|
||||
tokenItemVisibleState.copy(
|
||||
id = "token_3",
|
||||
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
|
||||
),
|
||||
),
|
||||
TokensListItemState.Token(
|
||||
tokenItemVisibleState.copy(
|
||||
id = "token_4",
|
||||
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
|
||||
),
|
||||
),
|
||||
TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")),
|
||||
TokensListItemState.Token(
|
||||
tokenItemVisibleState.copy(
|
||||
id = "token_5",
|
||||
titleState = TokenItemState.TitleState.Content(text = "Ethereum"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"),
|
||||
),
|
||||
),
|
||||
),
|
||||
organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Visible(isEnabled = true, {}),
|
||||
),
|
||||
pullToRefreshConfig = WalletPullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
onRefresh = {},
|
||||
),
|
||||
notifications = persistentListOf(
|
||||
WalletNotification.Critical.DevCard,
|
||||
WalletNotification.Informational.MissingAddresses(missingAddressesCount = 0, onGenerateClick = {}),
|
||||
WalletNotification.Warning.NetworksUnreachable,
|
||||
),
|
||||
bottomSheetConfig = bottomSheet,
|
||||
onManageTokensClick = {},
|
||||
event = consumedEvent(),
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
|
||||
val singleWalletScreenState by lazy {
|
||||
WalletSingleCurrencyState.Content(
|
||||
onBackClick = {},
|
||||
topBarConfig = topBarConfig,
|
||||
walletsListConfig = walletListConfig,
|
||||
pullToRefreshConfig = WalletPullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
onRefresh = {},
|
||||
),
|
||||
notifications = persistentListOf(WalletNotification.Warning.NetworksUnreachable),
|
||||
buttons = manageButtons,
|
||||
bottomSheetConfig = bottomSheet,
|
||||
marketPriceBlockState = MarketPriceBlockState.Content(
|
||||
currencySymbol = "BTC",
|
||||
price = "98900.12$",
|
||||
priceChangeConfig = PriceChangeState.Content(
|
||||
valueInPercent = "5.16%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
),
|
||||
txHistoryState = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
PagingData.from(
|
||||
listOf(
|
||||
TxHistoryState.TxHistoryItemState.GroupTitle(
|
||||
title = "Today",
|
||||
itemKey = UUID.randomUUID().toString(),
|
||||
),
|
||||
TxHistoryState.TxHistoryItemState.Transaction(
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
status = TransactionState.Content.Status.Unconfirmed,
|
||||
direction = TransactionState.Content.Direction.OUTGOING,
|
||||
iconRes = com.tangem.core.ui.R.drawable.ic_arrow_up_24,
|
||||
title = resourceReference(com.tangem.core.ui.R.string.common_transfer),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
TxHistoryState.TxHistoryItemState.GroupTitle(
|
||||
title = "Yesterday",
|
||||
itemKey = UUID.randomUUID().toString(),
|
||||
),
|
||||
TxHistoryState.TxHistoryItemState.Transaction(
|
||||
TransactionState.Content(
|
||||
txHash = UUID.randomUUID().toString(),
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
status = TransactionState.Content.Status.Confirmed,
|
||||
direction = TransactionState.Content.Direction.OUTGOING,
|
||||
iconRes = com.tangem.core.ui.R.drawable.ic_arrow_up_24,
|
||||
title = resourceReference(com.tangem.core.ui.R.string.common_transfer),
|
||||
subtitle = TextReference.Str("33BddS...ga2B"),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
event = consumedEvent(),
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,17 +26,13 @@ import com.tangem.feature.wallet.presentation.WalletFragment
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreenV2
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModelV2
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/** Default implementation of wallet feature router */
|
||||
internal class DefaultWalletRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
private val walletFeatureToggles: WalletFeatureToggles,
|
||||
) : InnerWalletRouter {
|
||||
|
||||
private var navController: NavHostController by Delegates.notNull()
|
||||
|
|
@ -53,21 +49,12 @@ internal class DefaultWalletRouter(
|
|||
startDestination = WalletRoute.Wallet.route,
|
||||
) {
|
||||
composable(WalletRoute.Wallet.route) {
|
||||
if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) {
|
||||
val viewModel = hiltViewModel<WalletViewModelV2>().apply {
|
||||
setWalletRouter(router = this@DefaultWalletRouter)
|
||||
subscribeToLifecycle(LocalLifecycleOwner.current)
|
||||
}
|
||||
|
||||
WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value)
|
||||
} else {
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply {
|
||||
router = this@DefaultWalletRouter
|
||||
}
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
WalletScreen(state = viewModel.uiState)
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply {
|
||||
setWalletRouter(router = this@DefaultWalletRouter)
|
||||
subscribeToLifecycle(LocalLifecycleOwner.current)
|
||||
}
|
||||
|
||||
WalletScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value)
|
||||
}
|
||||
|
||||
composable(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue