Updated on 2026-08-14
This commit is contained in:
commit
b76e1bbc45
219 changed files with 4585 additions and 5271 deletions
|
|
@ -2,6 +2,7 @@ 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.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
|
@ -15,6 +16,7 @@ import kotlinx.collections.immutable.persistentSetOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
||||
@Suppress("LargeClass")
|
||||
internal class AddCustomTokenStateFactory(
|
||||
private val currentStateProvider: Provider<AddCustomTokenState>,
|
||||
private val clickIntents: AddCustomTokenClickIntents,
|
||||
|
|
@ -72,8 +74,11 @@ internal class AddCustomTokenStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getListOfDerivations(supportedNetworks: List<Network>): List<Derivation> {
|
||||
return supportedNetworks.mapNotNull { network ->
|
||||
private fun getListOfDerivations(
|
||||
networksListToGenerateDerivations: List<Network>,
|
||||
filterOnlyHardenedDerivations: Boolean = false,
|
||||
): List<Derivation> {
|
||||
return networksListToGenerateDerivations.mapNotNull { network ->
|
||||
network.derivationPath.value?.let { rawPath ->
|
||||
Derivation(
|
||||
networkName = network.name,
|
||||
|
|
@ -82,6 +87,12 @@ internal class AddCustomTokenStateFactory(
|
|||
networkId = network.backendId,
|
||||
onDerivationSelected = clickIntents::onDerivationSelected,
|
||||
)
|
||||
}.takeIf { derivation ->
|
||||
if (filterOnlyHardenedDerivations) {
|
||||
derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -218,41 +229,52 @@ internal class AddCustomTokenStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState {
|
||||
fun updateStateOnNetworkSelected(
|
||||
networkItemState: NetworkItemState,
|
||||
supportsTokens: Boolean,
|
||||
networks: List<Network>,
|
||||
requiresHardenedDerivationOnly: Boolean,
|
||||
): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
val tokenData = if (supportsTokens) {
|
||||
uiState.tokenData
|
||||
?: CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
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,
|
||||
),
|
||||
)
|
||||
uiState.tokenData ?: CustomTokenData(
|
||||
contractAddressTextField = TextFieldState.Editable(
|
||||
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 {
|
||||
null
|
||||
}
|
||||
|
||||
val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly)
|
||||
val chooseDerivationState = createChooseDerivationState(derivations)
|
||||
|
||||
return uiState.copy(
|
||||
chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState),
|
||||
chooseNetworkState = uiState.chooseNetworkState.copy(
|
||||
selectedNetwork = networkItemState,
|
||||
),
|
||||
chooseDerivationState = chooseDerivationState,
|
||||
tokenData = tokenData,
|
||||
addTokenButton = uiState.addTokenButton.copy(isEnabled = true),
|
||||
)
|
||||
|
|
@ -291,6 +313,47 @@ internal class AddCustomTokenStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState {
|
||||
val uiState = currentStateProvider()
|
||||
val path = createDerivationPathOrNull(input)
|
||||
val isWrongDerivationForWallet2 = isWrongDerivationForWallet2(
|
||||
requiresHardenedDerivationOnly = requiresHardenedDerivationOnly,
|
||||
derivationPath = path,
|
||||
)
|
||||
val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
confirmButtonEnabled = path != null && !isWrongDerivationForWallet2,
|
||||
derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2,
|
||||
)
|
||||
return uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = enterDerivationState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isWrongDerivationForWallet2(
|
||||
requiresHardenedDerivationOnly: Boolean,
|
||||
derivationPath: DerivationPath?,
|
||||
): Boolean {
|
||||
return if (requiresHardenedDerivationOnly) {
|
||||
!allNodesHardened(derivationPath)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun allNodesHardened(derivationPath: DerivationPath?): Boolean {
|
||||
return derivationPath?.nodes?.all { it.isHardened } ?: false
|
||||
}
|
||||
|
||||
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
|
||||
return try {
|
||||
DerivationPath(rawPath)
|
||||
} catch (error: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState {
|
||||
return currentStateProvider().copy(
|
||||
tokenData = CustomTokenData(
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ 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
|
||||
|
|
@ -56,6 +54,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private val validateContractAddressUseCase: ValidateContractAddressUseCase,
|
||||
private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet,
|
||||
private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase,
|
||||
private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver {
|
||||
|
||||
|
|
@ -135,13 +134,25 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
private fun selectNetwork(networkItemState: NetworkItemState) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch
|
||||
val supportsTokens = areTokensSupportedByNetworkUseCase(
|
||||
networkId = networkItemState.id,
|
||||
userWalletId = selectedWalletId,
|
||||
).getOrNull() ?: false
|
||||
|
||||
val networksForDerivations = getSupportedNetworks(selectedWalletId)
|
||||
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens)
|
||||
uiState = stateFactory.updateStateOnNetworkSelected(
|
||||
networkItemState = networkItemState,
|
||||
supportsTokens = supportsTokens,
|
||||
networks = networksForDerivations,
|
||||
requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(
|
||||
networkId = networkItemState.id,
|
||||
userWalletId = selectedWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -347,6 +358,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomDerivationChange(input: String) {
|
||||
val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return
|
||||
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value))
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
|
|
@ -356,25 +369,19 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) {
|
||||
val path = createDerivationPathOrNull(input)
|
||||
val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
confirmButtonEnabled = path != null,
|
||||
derivationIncorrect = input.isNotBlank() && path == null,
|
||||
)
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = enterDerivationState,
|
||||
),
|
||||
val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce
|
||||
uiState = stateFactory.updateOnCustomDerivationEntered(
|
||||
input = input,
|
||||
requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
|
||||
return try {
|
||||
DerivationPath(rawPath)
|
||||
} catch (error: HDWalletError) {
|
||||
null
|
||||
}
|
||||
private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean {
|
||||
return requiresHardenedDerivationOnlyUseCase.invoke(
|
||||
networkId = networkId,
|
||||
userWalletId = userWalletId,
|
||||
).getOrElse { false }
|
||||
}
|
||||
|
||||
override fun onCustomDerivationSelected() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ internal fun TokensList(
|
|||
LazyColumn(modifier = modifier) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_title),
|
||||
text = stringResource(id = R.string.manage_tokens_list_header_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
|||
import com.tangem.core.ui.components.TangemTextFieldsDefault
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.onboarding.R
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
|
||||
|
|
@ -67,7 +69,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo
|
|||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
|
||||
text = stringResource(id = R.string.common_import),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = state.buttonCreateWallet.enabled,
|
||||
showProgress = state.buttonCreateWallet.showProgress,
|
||||
|
|
@ -196,6 +198,28 @@ private fun SuggestionsBlockPreview_Dark(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ImportSeedPhraseScreenPreview_Light(
|
||||
@PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList<String>,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
ImportSeedPhraseScreen(
|
||||
ImportSeedPhraseState(
|
||||
tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
|
||||
onSuggestedPhraseClick = {},
|
||||
buttonCreateWallet = ButtonState(
|
||||
enabled = true,
|
||||
isClickable = true,
|
||||
showProgress = false,
|
||||
onClick = {},
|
||||
),
|
||||
suggestionsList = suggestions,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider<ImmutableList<String>>(
|
||||
collection = listOf(
|
||||
persistentListOf(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.onboarding.presentation.wallet2.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
|
|
@ -8,16 +9,19 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerW32
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.onboarding.R
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -88,4 +92,25 @@ private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList<MnemonicGridItem>)
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun YourSeedPhraseScreenPreview_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
YourSeedPhraseScreen(
|
||||
state = YourSeedPhraseState(
|
||||
mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(),
|
||||
buttonContinue = ButtonState(
|
||||
enabled = true,
|
||||
isClickable = true,
|
||||
showProgress = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.feature.qrscanning
|
||||
|
||||
enum class SourceType {
|
||||
WALLET_CONNECT,
|
||||
SEND,
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ android {
|
|||
namespace = "com.tangem.feature.qrscanning.impl"
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Core */
|
||||
|
|
@ -40,8 +39,13 @@ dependencies {
|
|||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Api */
|
||||
implementation(projects.features.qrScanning.api)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.qrScanning)
|
||||
implementation(projects.domain.qrScanning.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.di
|
||||
|
||||
import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository
|
||||
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
|
||||
import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
|
||||
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 QrScanningModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQrScannedEventsRepository(): QrScanningEventsRepository {
|
||||
return DefaultQrScanningEventsRepository()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase {
|
||||
return EmitQrScannedEventUseCase(repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.feature.qrscanning.presentation.transformers
|
|||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.repo
|
||||
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
||||
|
||||
private data class QrScanningEvent(val type: SourceType, val qrCode: String)
|
||||
|
||||
private val scannedEvents = MutableSharedFlow<QrScanningEvent>()
|
||||
|
||||
override suspend fun emitResult(type: SourceType, qrCode: String) {
|
||||
scannedEvents.emit(QrScanningEvent(type, qrCode))
|
||||
}
|
||||
|
||||
override fun subscribeToScanningResults(type: SourceType) = scannedEvents
|
||||
.filter { it.type == type }
|
||||
.map { it.qrCode }
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.repo
|
||||
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface QrScanningEventsRepository {
|
||||
|
||||
suspend fun emitResult(type: SourceType, qrCode: String)
|
||||
|
||||
fun subscribeToScanningResults(type: SourceType): Flow<String>
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
|
||||
|
||||
internal class EmitQrScannedEventUseCase(
|
||||
private val repository: QrScanningEventsRepository,
|
||||
) {
|
||||
suspend operator fun invoke(type: SourceType, qrCode: String): Either<Exception, Unit> {
|
||||
return try {
|
||||
repository.emitResult(type, qrCode)
|
||||
Unit.right()
|
||||
} catch (e: Exception) {
|
||||
e.left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.feature.qrscanning.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.lang.Exception
|
||||
|
||||
class ListenToQrScanningUseCase(
|
||||
val repository: QrScanningEventsRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(type: SourceType): Either<Exception, Flow<String>> {
|
||||
return try {
|
||||
repository.subscribeToScanningResults(type).right()
|
||||
} catch (e: Exception) {
|
||||
e.left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.feature.qrscanning.viewmodel
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlin.properties.Delegates
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.feature.qrscanning.viewmodel
|
||||
|
||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
|
||||
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
|
||||
import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import androidx.compose.ui.res.pluralStringResource
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -113,14 +115,22 @@ private fun Awards(expectedAwards: ExpectedAwards) {
|
|||
thickness = TangemTheme.dimens.size0_5,
|
||||
)
|
||||
AwardText(
|
||||
startText = stringResource(id = R.string.referral_expected_awards),
|
||||
startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
|
||||
stringResource(id = R.string.referral_expected_awards)
|
||||
} else {
|
||||
stringResource(id = R.string.referral_no_expected_awards)
|
||||
},
|
||||
startTextColor = TangemTheme.colors.text.tertiary,
|
||||
startTextStyle = TangemTheme.typography.subtitle2,
|
||||
endText = pluralStringResource(
|
||||
id = R.plurals.referral_number_of_wallets,
|
||||
count = expectedAwards.numberOfWallets,
|
||||
expectedAwards.numberOfWallets,
|
||||
),
|
||||
endText = if (expectedAwards.expectedAwards.isNotEmpty()) {
|
||||
pluralStringResource(
|
||||
id = R.plurals.referral_number_of_wallets,
|
||||
count = expectedAwards.numberOfWallets,
|
||||
expectedAwards.numberOfWallets,
|
||||
)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
endTextColor = TangemTheme.colors.text.tertiary,
|
||||
endTextStyle = TangemTheme.typography.body2,
|
||||
cornersToRound = CornersToRound.ZERO,
|
||||
|
|
@ -309,34 +319,20 @@ private fun Context.shareText(text: String) {
|
|||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_InLightTheme() {
|
||||
private fun ParticipateBottomBlockPreview_Light(
|
||||
@PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = ExpectedAwards(
|
||||
numberOfWallets = 3,
|
||||
expectedAwards = listOf(
|
||||
ExpectedAward(
|
||||
amount = "10 USDT",
|
||||
paymentDate = "Today",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "20 USDT",
|
||||
paymentDate = "6 Aug 2023",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "30 USDT",
|
||||
paymentDate = "10 Aug 2023",
|
||||
),
|
||||
),
|
||||
),
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
purchasedWalletCount = data.purchasedWalletCount,
|
||||
code = data.code,
|
||||
shareLink = data.shareLink,
|
||||
expectedAwards = data.expectedAwards,
|
||||
onAgreementClick = data.onAgreementClick,
|
||||
onShowCopySnackbar = data.onShowCopySnackbar,
|
||||
onCopyClick = data.onCopyClick,
|
||||
onShareClick = data.onShareClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -344,18 +340,20 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() {
|
|||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
private fun ParticipateBottomBlockPreview_Dark(
|
||||
@PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
purchasedWalletCount = state.purchasedWalletCount,
|
||||
code = state.code,
|
||||
shareLink = state.shareLink,
|
||||
expectedAwards = state.expectedAwards,
|
||||
onAgreementClick = state.onAgreementClick,
|
||||
onShowCopySnackbar = state.onShowCopySnackbar,
|
||||
onCopyClick = state.onCopyClick,
|
||||
onShareClick = state.onShareClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -363,26 +361,7 @@ private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() {
|
|||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 0,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun LessMoreButton_White() {
|
||||
private fun LessMoreButton_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
LessMoreButton(
|
||||
isExpanded = remember {
|
||||
|
|
@ -394,19 +373,64 @@ private fun LessMoreButton_White() {
|
|||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() {
|
||||
private fun LessMoreButton_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
ParticipateBottomBlock(
|
||||
purchasedWalletCount = 3,
|
||||
code = "x4JdK",
|
||||
shareLink = "",
|
||||
expectedAwards = null,
|
||||
onAgreementClick = {},
|
||||
onShowCopySnackbar = {},
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
}
|
||||
LessMoreButton(
|
||||
isExpanded = remember {
|
||||
mutableStateOf(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterProvider<ParticipateBottomBlockData>(
|
||||
collection = listOf(
|
||||
ParticipateBottomBlockData(
|
||||
purchasedWalletCount = 3,
|
||||
expectedAwards = ExpectedAwards(
|
||||
numberOfWallets = 3,
|
||||
expectedAwards = listOf(
|
||||
ExpectedAward(
|
||||
amount = "10 USDT",
|
||||
paymentDate = "Today",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "20 USDT",
|
||||
paymentDate = "6 Aug 2023",
|
||||
),
|
||||
ExpectedAward(
|
||||
amount = "30 USDT",
|
||||
paymentDate = "10 Aug 2023",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ParticipateBottomBlockData(
|
||||
purchasedWalletCount = 3,
|
||||
expectedAwards = ExpectedAwards(
|
||||
numberOfWallets = 3,
|
||||
expectedAwards = emptyList(),
|
||||
),
|
||||
),
|
||||
ParticipateBottomBlockData(
|
||||
purchasedWalletCount = 3,
|
||||
expectedAwards = null,
|
||||
),
|
||||
ParticipateBottomBlockData(
|
||||
purchasedWalletCount = 0,
|
||||
expectedAwards = null,
|
||||
),
|
||||
|
||||
),
|
||||
)
|
||||
|
||||
private data class ParticipateBottomBlockData(
|
||||
val purchasedWalletCount: Int,
|
||||
val expectedAwards: ExpectedAwards?,
|
||||
val code: String = "x4JDK",
|
||||
val shareLink: String = "",
|
||||
val onAgreementClick: () -> Unit = {},
|
||||
val onShowCopySnackbar: () -> Unit = {},
|
||||
val onCopyClick: () -> Unit = {},
|
||||
val onShareClick: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -11,6 +11,7 @@ interface SendRouter {
|
|||
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 TAG_KEY = "send_tag"
|
||||
const val DESTINATION_ADDRESS_KEY = "send_destination_address"
|
||||
}
|
||||
}
|
||||
|
|
@ -69,12 +69,13 @@ dependencies {
|
|||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.qrScanning)
|
||||
implementation(projects.domain.qrScanning.models)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.qrScanning.api)
|
||||
implementation(projects.features.qrScanning.impl)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import androidx.fragment.app.Fragment
|
|||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.feature.qrscanning.SourceType
|
||||
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
|||
),
|
||||
) {
|
||||
|
||||
object TotalExceedsBalance : Error(
|
||||
data object TotalExceedsBalance : Error(
|
||||
title = resourceReference(R.string.send_notification_exceed_balance_title),
|
||||
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
|
||||
)
|
||||
|
||||
object InvalidAmount : Error(
|
||||
data object InvalidAmount : Error(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
|
||||
)
|
||||
|
|
@ -49,7 +49,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
|||
val amountLimit: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
) : Error(
|
||||
title = resourceReference(R.string.send_notifiaction_transaction_limit_title),
|
||||
title = resourceReference(R.string.send_notification_transaction_limit_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_transaction_limit_text,
|
||||
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
|
||||
|
|
@ -94,17 +94,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
|||
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
|
||||
)
|
||||
|
||||
data class NetworkCoverage(
|
||||
val amountReducedBy: String,
|
||||
val amountReduced: String,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.send_network_fee_warning_content,
|
||||
formatArgs = wrappedList(amountReducedBy, amountReduced),
|
||||
),
|
||||
)
|
||||
|
||||
data object FeeTooLow : Warning(
|
||||
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
|
||||
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ internal class SendNotificationFactory(
|
|||
addDustWarningNotification(feeAmount, sendAmount)
|
||||
addTransactionLimitErrorNotification(feeAmount, sendAmount)
|
||||
// warnings
|
||||
addFeeCoverageNotification(sendState.isSubtract, sendAmount)
|
||||
addExistentialWarningNotification(feeAmount, sendAmount)
|
||||
addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce)
|
||||
addTooLowNotification(feeState)
|
||||
|
|
@ -257,29 +256,6 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeCoverageNotification(
|
||||
isSubtract: Boolean,
|
||||
amountValue: BigDecimal,
|
||||
) {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
|
||||
val amountReducedValue = amountValue.minus(feeAmount)
|
||||
val amountReducedByValue = amountValue.minus(amountReducedValue)
|
||||
val amountReducedBy = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = amountReducedByValue,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
val amountReduced = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = amountReducedValue,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
if (isSubtract) {
|
||||
add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addTooLowNotification(feeState: SendStates.FeeState) {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
|
@ -31,6 +32,7 @@ internal class SendStateFactory(
|
|||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
) {
|
||||
|
|
@ -62,7 +64,7 @@ internal class SendStateFactory(
|
|||
private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendFeeStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -85,16 +87,18 @@ internal class SendStateFactory(
|
|||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
amountState = state.amountState ?: amountStateConverter.convert(""),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(""),
|
||||
recipientState = state.recipientState
|
||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadyState(amount: String, destinationAddress: String): SendUiState {
|
||||
fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
amountState = state.amountState ?: amountStateConverter.convert(amount),
|
||||
recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress),
|
||||
recipientState = state.recipientState
|
||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
isEditingDisabled = true,
|
||||
)
|
||||
|
|
@ -214,9 +218,20 @@ internal class SendStateFactory(
|
|||
//endregion
|
||||
|
||||
//region send
|
||||
fun onSubtractSelect(isSubtract: Boolean): SendUiState {
|
||||
fun onSubtractSelect(isSubtract: Boolean, isAmountSubtractAvailable: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val fee = state.feeState?.fee ?: return state
|
||||
val amountState = state.amountState ?: return state
|
||||
val amount = amountState.amountTextField.cryptoAmount
|
||||
val amountValue = amount.value ?: return state
|
||||
val amountToSend = if (isSubtract && isAmountSubtractAvailable) {
|
||||
val feeValue = fee.amount.value ?: return state
|
||||
amountValue.minus(feeValue)
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
return state.copy(
|
||||
amountState = amountStateConverter.convert(amountToSend.parseBigDecimal(amount.decimals)),
|
||||
sendState = state.sendState.copy(isSubtract = isSubtract),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates
|
|||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendAmountStateConverter(
|
||||
|
|
@ -37,18 +38,22 @@ internal class SendAmountStateConverter(
|
|||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = sendAmountFieldConverter.convert(value),
|
||||
isPrimaryButtonEnabled = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconUrl = appCurrency.iconSmallUrl,
|
||||
isFiat = true,
|
||||
),
|
||||
),
|
||||
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(status.currency.symbol),
|
||||
iconState = iconStateConverter.convert(status),
|
||||
isFiat = false,
|
||||
),
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
title = stringReference(appCurrency.code),
|
||||
iconUrl = appCurrency.iconSmallUrl,
|
||||
isFiat = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,14 +13,14 @@ import com.tangem.utils.converter.Converter
|
|||
internal class FeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<FeeSelectorState.Content, Fee> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal class FeeStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
) {
|
||||
|
|
@ -28,7 +28,7 @@ internal class FeeStateFactory(
|
|||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ internal class FeeStateFactory(
|
|||
FeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ internal class FeeStateFactory(
|
|||
}
|
||||
|
||||
private fun isFeeApproximate(fee: Fee): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
|
||||
return isFeeApproximateUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
amountType = fee.amount.type,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal class SendFeeCustomFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,16 +9,15 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
|
||||
internal class SendFeeStateConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.FeeState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
return SendStates.FeeState(
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
fee = null,
|
||||
notifications = persistentListOf(),
|
||||
rate = cryptoCurrencyStatus.value.fiatRate,
|
||||
rate = feeCryptoCurrencyStatusProvider().value.fiatRate,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
isFeeApproximate = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import java.math.RoundingMode
|
|||
internal class EthereumCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Fee.Ethereum, ImmutableList<SendTextField.CustomFee>> {
|
||||
|
||||
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
|
||||
|
|
@ -152,7 +152,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val rate = cryptoCurrencyStatusProvider().value.fiatRate
|
||||
val rate = feeCryptoCurrencyStatusProvider().value.fiatRate
|
||||
val fiatFee = rate?.let { fee?.multiply(it) }
|
||||
return stringReference(
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
|
|
@ -164,7 +164,7 @@ internal class EthereumCustomFeeConverter(
|
|||
}
|
||||
|
||||
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ internal class SendAmountFieldConverter(
|
|||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoDecimal = value.toBigDecimalOrDefault()
|
||||
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val fiatValue = if (value.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
|
||||
val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
|
||||
fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
|
||||
}
|
||||
val isDoneActionEnabled = !cryptoDecimal.isZero()
|
||||
|
|
@ -52,6 +53,7 @@ internal class SendAmountFieldConverter(
|
|||
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.swapping_insufficient_funds),
|
||||
isFiatUnavailable = fiatRate == null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal sealed class SendTextField {
|
|||
val fiatAmount: Amount,
|
||||
val isFiatValue: Boolean,
|
||||
val fiatValue: String,
|
||||
val isFiatUnavailable: Boolean,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
) : SendTextField()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
|
|
@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter
|
|||
internal class SendRecipientMemoFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Int, SendTextField.RecipientMemo> {
|
||||
) : Converter<SendRecipientMemoFieldConverter.Data, SendTextField.RecipientMemo> {
|
||||
|
||||
fun convertOrNull(): SendTextField.RecipientMemo? {
|
||||
fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? {
|
||||
val cryptoCurrency = cryptoCurrencyStatus().currency
|
||||
val memo = memoValue ?: ""
|
||||
|
||||
return when (cryptoCurrency.network.id.value) {
|
||||
Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
|
||||
Blockchain.XRP.id -> {
|
||||
convert(
|
||||
value = Data(memo = memo, label = R.string.send_destination_tag_field),
|
||||
)
|
||||
}
|
||||
Blockchain.Binance.id,
|
||||
Blockchain.TON.id,
|
||||
Blockchain.Cosmos.id,
|
||||
|
|
@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter(
|
|||
Blockchain.Stellar.id,
|
||||
Blockchain.Hedera.id,
|
||||
Blockchain.Algorand.id,
|
||||
-> convert(R.string.send_extras_hint_memo)
|
||||
-> {
|
||||
convert(
|
||||
value = Data(memo = memo, label = R.string.send_extras_hint_memo),
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(value: Int): SendTextField.RecipientMemo {
|
||||
override fun convert(value: Data): SendTextField.RecipientMemo {
|
||||
return SendTextField.RecipientMemo(
|
||||
value = "",
|
||||
value = value.memo,
|
||||
onValueChange = clickIntents::onRecipientMemoValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = resourceReference(R.string.send_optional_field),
|
||||
label = resourceReference(value),
|
||||
label = resourceReference(value.label),
|
||||
error = resourceReference(R.string.send_memo_destination_tag_error),
|
||||
disabledText = resourceReference(R.string.send_additional_field_already_included),
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
data class Data(val memo: String, @StringRes val label: Int)
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<String, SendStates.RecipientState> {
|
||||
) : Converter<SendRecipientStateConverter.Data, SendStates.RecipientState> {
|
||||
|
||||
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
|
||||
private val memoFieldConverter by lazy {
|
||||
|
|
@ -20,14 +20,16 @@ internal class SendRecipientStateConverter(
|
|||
)
|
||||
}
|
||||
|
||||
override fun convert(value: String): SendStates.RecipientState {
|
||||
override fun convert(value: Data): SendStates.RecipientState {
|
||||
return SendStates.RecipientState(
|
||||
addressTextField = addressFieldConverter.convert(value),
|
||||
memoTextField = memoFieldConverter.convertOrNull(),
|
||||
addressTextField = addressFieldConverter.convert(value.address),
|
||||
memoTextField = memoFieldConverter.convertOrNull(value.memo),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
wallets = persistentListOf(),
|
||||
recent = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
data class Data(val address: String, val memo: String? = null)
|
||||
}
|
||||
|
|
@ -11,10 +11,12 @@ import androidx.compose.ui.Alignment.Companion.BottomCenter
|
|||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
|
|
@ -62,10 +64,14 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean)
|
|||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
|
||||
val text = if (sendField.isFiatUnavailable) {
|
||||
BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
} else {
|
||||
"${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2,
|
||||
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
|
||||
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.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
|
|
@ -28,6 +30,7 @@ internal fun SendAmountContent(
|
|||
clickIntents: SendClickIntents,
|
||||
) {
|
||||
if (amountState == null) return
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
|
|
@ -41,19 +44,29 @@ internal fun SendAmountContent(
|
|||
end = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
showIndication = false,
|
||||
onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
if (amountState.segmentedButtonConfig.isNotEmpty()) {
|
||||
SegmentedButtons(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(TangemTheme.dimens.size40),
|
||||
config = amountState.segmentedButtonConfig,
|
||||
showIndication = false,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clickIntents.onCurrencyChangeClick(it.isFiat)
|
||||
},
|
||||
) {
|
||||
SendAmountCurrencyButton(it)
|
||||
}
|
||||
} else {
|
||||
SpacerWMax()
|
||||
}
|
||||
SecondaryButton(
|
||||
text = stringResource(R.string.send_max_amount),
|
||||
onClick = clickIntents::onMaxValueClick,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clickIntents.onMaxValueClick()
|
||||
},
|
||||
size = TangemButtonSize.Text,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
|||
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"
|
||||
|
|
@ -37,9 +36,8 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
),
|
||||
) {
|
||||
feeSelector(state, clickIntents)
|
||||
topNotifications(notifications)
|
||||
customFee(feeSendState)
|
||||
middleNotifications(notifications)
|
||||
notifications(notifications)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,29 +54,6 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents:
|
|||
}
|
||||
}
|
||||
|
||||
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.filterIsInstance<SendFeeNotification.Warning.TooHigh>().toImmutableList(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.notifications(
|
||||
configs: ImmutableList<SendFeeNotification>,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ 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.common.util.cardTypesResolver
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
|
|
@ -30,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
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
|
||||
|
|
@ -66,17 +72,19 @@ internal class SendViewModel @Inject constructor(
|
|||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
currencyChecksRepository: CurrencyChecksRepository,
|
||||
isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
|
|
@ -95,6 +103,7 @@ internal class SendViewModel @Inject constructor(
|
|||
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 memo: String? = savedStateHandle[SendRouter.TAG_KEY]
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
|
|
@ -108,6 +117,7 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
|
@ -120,7 +130,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||
)
|
||||
|
|
@ -165,12 +175,14 @@ internal class SendViewModel @Inject constructor(
|
|||
private var isAmountSubtractAvailable: Boolean = false
|
||||
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
private var balanceHidingJobHolder = JobHolder()
|
||||
private var recipientsJobHolder = JobHolder()
|
||||
private var feeJobHolder = JobHolder()
|
||||
private var addressValidationJobHolder = JobHolder()
|
||||
private var memoValidationJobHolder = JobHolder()
|
||||
private var sendNotificationsJobHolder = JobHolder()
|
||||
private var feeNotificationsJobHolder = JobHolder()
|
||||
private var qrScannerJobHolder = JobHolder()
|
||||
|
|
@ -204,7 +216,13 @@ internal class SendViewModel @Inject constructor(
|
|||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
checkIfSubtractAvailable()
|
||||
getCurrenciesStatusUpdates(wallet)
|
||||
|
||||
val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
|
||||
val isMultiCurrency = wallet.isMultiCurrency
|
||||
getCurrenciesStatusUpdates(
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
|
|
@ -227,31 +245,38 @@ internal class SendViewModel @Inject constructor(
|
|||
.saveIn(balanceHidingJobHolder)
|
||||
}
|
||||
|
||||
private fun getCurrenciesStatusUpdates(wallet: UserWallet) {
|
||||
val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
|
||||
|
||||
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
|
||||
if (cryptoCurrency is CryptoCurrency.Coin) {
|
||||
getCurrencyStatusUpdates(isSingleWallet = isSingleWallet)
|
||||
.onEach { currencyStatus ->
|
||||
currencyStatus.onRight {
|
||||
onDataLoaded(
|
||||
currencyStatus = it,
|
||||
coinCurrencyStatus = it,
|
||||
)
|
||||
}
|
||||
getCurrencyStatusUpdates(
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
).onEach { currencyStatus ->
|
||||
currencyStatus.onRight {
|
||||
onDataLoaded(
|
||||
currencyStatus = it,
|
||||
coinCurrencyStatus = it,
|
||||
feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency),
|
||||
)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceJobHolder)
|
||||
} else {
|
||||
combine(
|
||||
flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
|
||||
flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
|
||||
) { coinStatus, currencyStatus ->
|
||||
if (coinStatus.isRight() && currencyStatus.isRight()) {
|
||||
flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken),
|
||||
flow2 = getCurrencyStatusUpdates(
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
),
|
||||
) { maybeCoinStatus, maybeCurrencyStatus ->
|
||||
if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) {
|
||||
val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") }
|
||||
val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") }
|
||||
onDataLoaded(
|
||||
currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") },
|
||||
coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") },
|
||||
currencyStatus = currencyStatus,
|
||||
coinCurrencyStatus = coinStatus,
|
||||
feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -261,18 +286,41 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase(
|
||||
private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
isSingleWalletWithTokens = isSingleWalletWithToken,
|
||||
).conflate().distinctUntilChanged()
|
||||
|
||||
private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = isSingleWallet,
|
||||
).conflate().distinctUntilChanged()
|
||||
private fun getCurrencyStatusUpdates(
|
||||
isSingleWalletWithToken: Boolean,
|
||||
isMultiCurrency: Boolean,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return if (isMultiCurrency) {
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = isSingleWalletWithToken,
|
||||
).conflate().distinctUntilChanged()
|
||||
} else {
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFeeCurrencyStatusSync(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
isMultiCurrency: Boolean,
|
||||
): CryptoCurrencyStatus {
|
||||
return if (isMultiCurrency) {
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
).getOrNull() ?: error("Fee currency is unreachable")
|
||||
} else {
|
||||
cryptoCurrencyStatus
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
|
|
@ -286,19 +334,31 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) {
|
||||
private fun onDataLoaded(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
coinCurrencyStatus: CryptoCurrencyStatus,
|
||||
feeCurrencyStatus: CryptoCurrencyStatus,
|
||||
) {
|
||||
cryptoCurrencyStatus = currencyStatus
|
||||
coinCryptoCurrencyStatus = coinCurrencyStatus
|
||||
feeCryptoCurrencyStatus = feeCurrencyStatus
|
||||
|
||||
if (transactionId != null && amount != null && destinationAddress != null) {
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress)
|
||||
stateRouter.showFee()
|
||||
} else {
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
stateRouter.showRecipient()
|
||||
when {
|
||||
uiState.sendState.isSuccess -> {
|
||||
stateRouter.showSend()
|
||||
}
|
||||
transactionId != null && amount != null && destinationAddress != null -> {
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
stateRouter.showFee()
|
||||
updateNotifications()
|
||||
}
|
||||
else -> {
|
||||
getWalletsAndRecent()
|
||||
uiState = stateFactory.getReadyState()
|
||||
stateRouter.showRecipient()
|
||||
updateNotifications()
|
||||
}
|
||||
}
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
private fun getWalletsAndRecent() {
|
||||
|
|
@ -406,7 +466,7 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
return
|
||||
} else {
|
||||
uiState = stateFactory.onSubtractSelect(false)
|
||||
uiState = stateFactory.onSubtractSelect(false, isAmountSubtractAvailable)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
|
||||
}
|
||||
if (checkIfFeeTooLow(uiState)) {
|
||||
|
|
@ -454,13 +514,14 @@ internal class SendViewModel @Inject constructor(
|
|||
// region recipient state clicks
|
||||
fun onRecipientAddressScanned(address: String) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
|
||||
parseQrCodeUseCase(address, cryptoCurrency).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
|
||||
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
ifLeft = {
|
||||
onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
|
||||
Timber.w(it)
|
||||
},
|
||||
)
|
||||
|
|
@ -475,6 +536,7 @@ internal class SendViewModel @Inject constructor(
|
|||
val isValidAddress = validateAddress(value)
|
||||
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
|
||||
autoNextFromRecipient(type, isValidAddress)
|
||||
}
|
||||
}.saveIn(addressValidationJobHolder)
|
||||
}
|
||||
|
|
@ -487,7 +549,7 @@ internal class SendViewModel @Inject constructor(
|
|||
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
|
||||
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
|
||||
}
|
||||
}.saveIn(addressValidationJobHolder)
|
||||
}.saveIn(memoValidationJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun validateAddress(value: String): Boolean {
|
||||
|
|
@ -519,6 +581,12 @@ internal class SendViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
|
||||
val isRecent = type == EnterAddressSource.RecentAddress
|
||||
val isAddressOnly = uiState.recipientState?.memoTextField == null
|
||||
if (isRecent && isAddressOnly && isValidAddress) onNextClick()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region fee
|
||||
|
|
@ -538,7 +606,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSubtractSelect() {
|
||||
uiState = stateFactory.onSubtractSelect(true)
|
||||
uiState = stateFactory.onSubtractSelect(true, isAmountSubtractAvailable)
|
||||
stateRouter.showSend()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
|
||||
}
|
||||
|
|
@ -645,16 +713,10 @@ internal class SendViewModel @Inject constructor(
|
|||
val fee = feeState.fee ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) {
|
||||
val feeValue = fee.amount.value ?: return
|
||||
amountValue.minus(feeValue)
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
createTransactionUseCase(
|
||||
amount = amountToSend.convertToAmount(cryptoCurrency),
|
||||
amount = amountValue.convertToAmount(cryptoCurrency),
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = recipient,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ sealed class DataError {
|
|||
|
||||
data class InvalidPayoutAddressError(override val code: Int = 992) : DataError()
|
||||
|
||||
object UnknownError : DataError() {
|
||||
data object UnknownError : DataError() {
|
||||
override val code: Int = -1
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,6 @@ sealed class Warning {
|
|||
data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning()
|
||||
|
||||
data class MinAmountWarning(val dustValue: BigDecimal) : Warning()
|
||||
|
||||
data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning()
|
||||
}
|
||||
|
|
@ -79,8 +79,6 @@ interface SwapInteractor {
|
|||
*/
|
||||
fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount
|
||||
|
||||
fun isAvailableToSwap(networkId: String): Boolean
|
||||
|
||||
fun getSelectedWallet(): UserWallet?
|
||||
|
||||
suspend fun selectInitialCurrencyToSwap(
|
||||
|
|
|
|||
|
|
@ -197,7 +197,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
return repository.getPairs(initialCurrency, currenciesList)
|
||||
}
|
||||
|
||||
@Deprecated("used in old swap mechanism")
|
||||
override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState {
|
||||
val derivationPath = permissionOptions.fromToken.network.derivationPath.value
|
||||
val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) {
|
||||
|
|
@ -241,7 +240,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("used in old swap mechanism")
|
||||
override suspend fun findBestQuote(
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
|
|
@ -376,6 +374,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val warnings = mutableListOf<Warning>()
|
||||
manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken)
|
||||
manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount)
|
||||
manageReduceAmountWarning(warnings, fromTokenStatus, amount)
|
||||
return warnings
|
||||
}
|
||||
|
||||
|
|
@ -424,6 +423,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun manageReduceAmountWarning(
|
||||
warnings: MutableList<Warning>,
|
||||
fromTokenStatus: CryptoCurrencyStatus,
|
||||
amount: SwapAmount,
|
||||
) {
|
||||
val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id
|
||||
if (isTezos && amount.value == fromTokenStatus.value.amount) {
|
||||
warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onSwap(
|
||||
swapProvider: SwapProvider,
|
||||
swapData: SwapDataModel?,
|
||||
|
|
@ -729,16 +739,10 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Deprecated("used in old swap mechanism")
|
||||
override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount {
|
||||
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals)
|
||||
}
|
||||
|
||||
@Deprecated("used in old swap mechanism")
|
||||
override fun isAvailableToSwap(networkId: String): Boolean {
|
||||
return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId)
|
||||
}
|
||||
|
||||
override suspend fun selectInitialCurrencyToSwap(
|
||||
initialCryptoCurrency: CryptoCurrency,
|
||||
state: TokensDataStateExpress,
|
||||
|
|
@ -1584,16 +1588,6 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
|
||||
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
||||
private const val INFINITY_SYMBOL = "∞"
|
||||
|
||||
private val ONE_INCH_SUPPORTED_NETWORKS = listOf(
|
||||
"ethereum",
|
||||
"binance-smart-chain",
|
||||
"polygon-pos",
|
||||
"optimistic-ethereum",
|
||||
"arbitrum-one",
|
||||
"xdai",
|
||||
"avalanche",
|
||||
"fantom",
|
||||
)
|
||||
private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01")
|
||||
}
|
||||
}
|
||||
|
|
@ -67,4 +67,5 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ data class SwapStateHolder(
|
|||
val alert: SwapWarning.GenericWarning? = null,
|
||||
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
val providerState: ProviderState,
|
||||
val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01
|
||||
|
||||
val fee: FeeItemState = FeeItemState.Empty,
|
||||
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
|
||||
|
|
@ -108,7 +109,6 @@ sealed interface SwapWarning {
|
|||
val title: TextReference? = null,
|
||||
val message: TextReference? = null,
|
||||
val type: GenericWarningType = GenericWarningType.OTHER,
|
||||
val shouldWrapMessage: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
) : SwapWarning
|
||||
data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
|
|
@ -116,6 +116,8 @@ sealed interface SwapWarning {
|
|||
data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning
|
||||
data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
}
|
||||
|
||||
enum class GenericWarningType {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.TxFee
|
||||
|
||||
data class UiActions(
|
||||
|
|
@ -13,6 +14,8 @@ data class UiActions(
|
|||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onMaxAmountSelected: () -> Unit,
|
||||
val onReduceAmount: (SwapAmount) -> Unit,
|
||||
val onReduceAmountIgnoreClick: () -> Unit,
|
||||
val openPermissionBottomSheet: () -> Unit,
|
||||
val onChangeApproveType: (ApproveType) -> Unit,
|
||||
// region new actions
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ internal class StateBuilder(
|
|||
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
|
||||
providerState = ProviderState.Empty(),
|
||||
shouldShowMaxAmount = false,
|
||||
reduceAmountIgnore = false,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -213,7 +214,11 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
|
||||
val warnings = getWarningsForSuccessState(
|
||||
quoteModel = quoteModel,
|
||||
fromToken = fromToken,
|
||||
ignoreAmountReduce = uiStateHolder.reduceAmountIgnore,
|
||||
)
|
||||
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
|
||||
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
|
||||
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
|
||||
|
|
@ -311,41 +316,28 @@ internal class StateBuilder(
|
|||
private fun getWarningsForSuccessState(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
ignoreAmountReduce: Boolean,
|
||||
): List<SwapWarning> {
|
||||
val warnings = mutableListOf<SwapWarning>()
|
||||
addDomainWarnings(quoteModel, warnings)
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
|
||||
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
|
||||
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
|
||||
) {
|
||||
warnings.add(
|
||||
SwapWarning.PermissionNeeded(
|
||||
createPermissionNotificationConfig(fromToken.symbol),
|
||||
),
|
||||
)
|
||||
}
|
||||
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
||||
is IncludeFeeInAmount.Included ->
|
||||
warnings.add(
|
||||
SwapWarning.GeneralWarning(
|
||||
createNetworkFeeCoverageNotificationConfig(),
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
addUnableCoverFeeWarning(quoteModel, fromToken, warnings)
|
||||
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
|
||||
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
|
||||
) {
|
||||
warnings.add(SwapWarning.InsufficientFunds)
|
||||
}
|
||||
maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce)
|
||||
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
|
||||
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
|
||||
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings)
|
||||
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
|
||||
maybeAddInsufficientFundsWarning(quoteModel, warnings)
|
||||
maybeAddTransactionInProgressWarning(quoteModel, warnings)
|
||||
return warnings
|
||||
}
|
||||
|
||||
private fun maybeAddTransactionInProgressWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
if (quoteModel.permissionState is PermissionDataState.PermissionLoading) {
|
||||
warnings.add(
|
||||
SwapWarning.TransactionInProgressWarning(
|
||||
title = resourceReference(R.string.swapping_pending_transaction_title),
|
||||
description = resourceReference(R.string.swapping_pending_transaction_subtitle),
|
||||
title = resourceReference(R.string.warning_express_approval_in_progress_title),
|
||||
description = resourceReference(R.string.warning_express_approval_in_progress_message),
|
||||
),
|
||||
)
|
||||
} else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) {
|
||||
|
|
@ -361,10 +353,13 @@ internal class StateBuilder(
|
|||
),
|
||||
)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList<SwapWarning>) {
|
||||
private fun maybeAddDomainWarnings(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
quoteModel.warnings.forEach {
|
||||
when (it) {
|
||||
is Warning.ExistentialDepositWarning -> {
|
||||
|
|
@ -390,7 +385,7 @@ internal class StateBuilder(
|
|||
NotificationConfig(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_invalid_minimum_amount_text,
|
||||
R.string.warning_express_dust_message,
|
||||
wrappedList(
|
||||
it.dustValue.toPlainString(),
|
||||
it.dustValue.toPlainString(),
|
||||
|
|
@ -401,11 +396,84 @@ internal class StateBuilder(
|
|||
),
|
||||
)
|
||||
}
|
||||
is Warning.ReduceAmountWarning -> {
|
||||
if (!ignoreAmountReduce) {
|
||||
warnings.add(
|
||||
SwapWarning.ReduceAmount(
|
||||
notificationConfig = createReduceAmountNotificationConfig(
|
||||
amount = it.tezosFeeThreshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val fromAmount = quoteModel.fromTokenInfo.tokenAmount
|
||||
val patchedAmount = fromAmount.copy(
|
||||
value = fromAmount.value - it.tezosFeeThreshold,
|
||||
)
|
||||
actions.onReduceAmount(patchedAmount)
|
||||
},
|
||||
onDismissClick = actions.onReduceAmountIgnoreClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addUnableCoverFeeWarning(
|
||||
private fun maybeAddNeedReserveToCreateAccountWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value
|
||||
if (status is CryptoCurrencyStatus.NoAccount) {
|
||||
val amount = quoteModel.toTokenInfo.tokenAmount.value
|
||||
val amountToCreateAccount = status.amountToCreateAccount
|
||||
|
||||
if (amount < amountToCreateAccount) {
|
||||
warnings.add(
|
||||
SwapWarning.NeedReserveToCreateAccount(
|
||||
notificationConfig = createActivateAccountNotificationConfig(
|
||||
status.amountToCreateAccount,
|
||||
quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddPermissionNeededWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
fromToken: CryptoCurrency,
|
||||
) {
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
|
||||
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
|
||||
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
|
||||
) {
|
||||
warnings.add(
|
||||
SwapWarning.PermissionNeeded(
|
||||
createPermissionNotificationConfig(fromToken.symbol),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddNetworkFeeCoverageWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
||||
is IncludeFeeInAmount.Included ->
|
||||
warnings.add(
|
||||
SwapWarning.GeneralWarning(
|
||||
createNetworkFeeCoverageNotificationConfig(),
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddUnableCoverFeeWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
|
|
@ -428,7 +496,29 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun maybeAddInsufficientFundsWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
|
||||
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
|
||||
) {
|
||||
warnings.add(SwapWarning.InsufficientFunds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean {
|
||||
val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value
|
||||
if (status is CryptoCurrencyStatus.NoAccount) {
|
||||
val amount = quoteModel.toTokenInfo.tokenAmount.value
|
||||
val amountToCreateAccount = status.amountToCreateAccount
|
||||
|
||||
if (amount < amountToCreateAccount) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val preparedSwapConfigState = quoteModel.preparedSwapConfigState
|
||||
// check has has outgoing transaction
|
||||
if (preparedSwapConfigState.hasOutgoingTransaction) return false
|
||||
|
|
@ -587,7 +677,7 @@ internal class StateBuilder(
|
|||
subtitle = if (dataError is DataError.UnknownError) {
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
} else {
|
||||
resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString()))
|
||||
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||
},
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
|
|
@ -774,7 +864,7 @@ internal class StateBuilder(
|
|||
SwapWarning.GeneralWarning(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = TextReference.Res(R.string.warning_express_refresh_required_title),
|
||||
subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)),
|
||||
subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = TextReference.Res(R.string.warning_button_refresh),
|
||||
|
|
@ -823,8 +913,8 @@ internal class StateBuilder(
|
|||
warnings.add(
|
||||
0,
|
||||
SwapWarning.TransactionInProgressWarning(
|
||||
title = resourceReference(R.string.swapping_pending_transaction_title),
|
||||
description = resourceReference(R.string.swapping_pending_transaction_subtitle),
|
||||
title = resourceReference(R.string.warning_express_approval_in_progress_title),
|
||||
description = resourceReference(R.string.warning_express_approval_in_progress_message),
|
||||
),
|
||||
)
|
||||
return uiState.copy(
|
||||
|
|
@ -928,17 +1018,11 @@ internal class StateBuilder(
|
|||
|
||||
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null)
|
||||
|
||||
fun addWarning(
|
||||
uiState: SwapStateHolder,
|
||||
message: TextReference?,
|
||||
shouldWrapMessage: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder {
|
||||
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
|
||||
renewWarnings.add(
|
||||
SwapWarning.GenericWarning(
|
||||
message = message,
|
||||
shouldWrapMessage = shouldWrapMessage,
|
||||
onClick = onClick,
|
||||
),
|
||||
)
|
||||
|
|
@ -1220,6 +1304,35 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig {
|
||||
return NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.send_notification_invalid_reserve_amount_title,
|
||||
formatArgs = wrappedList("$amount $token"),
|
||||
),
|
||||
subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createReduceAmountNotificationConfig(
|
||||
amount: String,
|
||||
onConfirmClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
): NotificationConfig {
|
||||
return NotificationConfig(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
|
||||
primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)),
|
||||
onPrimaryClick = onConfirmClick,
|
||||
secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore),
|
||||
onSecondaryClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUnableToCoverFeeNotificationConfig(
|
||||
fromToken: CryptoCurrency,
|
||||
feeCurrency: CryptoCurrency?,
|
||||
|
|
|
|||
|
|
@ -337,14 +337,9 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
)
|
||||
}
|
||||
is SwapWarning.GenericWarning -> {
|
||||
val message = warning.message?.let {
|
||||
if (warning.shouldWrapMessage) {
|
||||
String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference())
|
||||
} else {
|
||||
it.resolveReference()
|
||||
}
|
||||
} ?: stringResource(id = R.string.common_unknown_error)
|
||||
RefreshableWaringCard(
|
||||
val message = warning.message?.resolveReference()
|
||||
?: stringResource(id = R.string.common_unknown_error)
|
||||
RefreshableWarningCard(
|
||||
title = stringResource(id = R.string.common_warning),
|
||||
description = message,
|
||||
onClick = warning.onClick,
|
||||
|
|
@ -377,6 +372,16 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
iconTint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
is SwapWarning.NeedReserveToCreateAccount -> {
|
||||
Notification(
|
||||
config = warning.notificationConfig,
|
||||
)
|
||||
}
|
||||
is SwapWarning.ReduceAmount -> {
|
||||
Notification(
|
||||
config = warning.notificationConfig,
|
||||
)
|
||||
}
|
||||
is SwapWarning.TransactionInProgressWarning -> {
|
||||
CardWithIcon(
|
||||
title = warning.title.resolveReference(),
|
||||
|
|
@ -499,6 +504,7 @@ private val state = SwapStateHolder(
|
|||
providerState = ProviderState.Loading(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
shouldShowMaxAmount = true,
|
||||
reduceAmountIgnore = false,
|
||||
tosState = TosState(
|
||||
tosLink = LegalState(
|
||||
title = stringReference("Terms of Use"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor
|
|||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
|
|
@ -812,6 +813,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onReduceAmountClicked(newAmount: SwapAmount) {
|
||||
onAmountChanged(newAmount.formatToUIRepresentation())
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun onAmountSelected(selected: Boolean) {
|
||||
if (selected) {
|
||||
|
|
@ -874,7 +879,14 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
onSearchEntered("")
|
||||
},
|
||||
onMaxAmountSelected = { onMaxAmountClicked() },
|
||||
onMaxAmountSelected = ::onMaxAmountClicked,
|
||||
onReduceAmount = ::onReduceAmountClicked,
|
||||
onReduceAmountIgnoreClick = {
|
||||
uiState = uiState.copy(
|
||||
reduceAmountIgnore = true,
|
||||
warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount },
|
||||
)
|
||||
},
|
||||
openPermissionBottomSheet = {
|
||||
singleTaskScheduler.cancelTask()
|
||||
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)
|
||||
|
|
@ -1153,11 +1165,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val loggingTag = "SwapViewModel"
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
private const val UPDATE_DELAY = 10000L
|
||||
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
private companion object {
|
||||
const val loggingTag = "SwapViewModel"
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
|
||||
object TopUpWithoutReserve : Informational(
|
||||
title = resourceReference(id = R.string.warning_no_account_title),
|
||||
subtitle = resourceReference(id = R.string.no_account_bnb),
|
||||
subtitle = resourceReference(id = R.string.no_account_send_to_create),
|
||||
)
|
||||
|
||||
class HasPendingTransactions(val coinSymbol: String) : Informational(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.inputrow.InputRowBestRate
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
|
|
@ -67,7 +68,7 @@ internal fun ExchangeProvider(
|
|||
)
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "ID: $providerTxId",
|
||||
text = stringResource(R.string.express_transaction_id, wrappedList(providerTxId)),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -174,9 +174,10 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
status = cryptoCurrencyStatus ?: return,
|
||||
transactionInfo = data.let {
|
||||
TransactionInfo(
|
||||
amount = it.baseCurrencyAmount,
|
||||
transactionId = it.transactionId,
|
||||
destinationAddress = it.depositWalletAddress,
|
||||
amount = it.baseCurrencyAmount,
|
||||
tag = it.depositWalletAddressTag,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
|||
amount = it.baseCurrencyAmount,
|
||||
destinationAddress = it.depositWalletAddress,
|
||||
transactionId = it.transactionId,
|
||||
tag = it.depositWalletAddressTag,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
override val oneTimeEventId: String = id + userWalletId.stringValue
|
||||
}
|
||||
|
||||
object WalletOpened : Basic(event = "Wallet Opened")
|
||||
data object WalletOpened : Basic(event = "Wallet Opened")
|
||||
|
||||
class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
|
|
@ -53,8 +53,8 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
|
||||
|
||||
object ScreenOpened : MainScreen(event = "Screen opened")
|
||||
object WalletSwipe : MainScreen(event = "Wallet Swipe")
|
||||
data object ScreenOpened : MainScreen(event = "Screen opened")
|
||||
data object WalletSwipe : MainScreen(event = "Wallet Swipe")
|
||||
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
|
||||
event = "Enable Biometric",
|
||||
|
|
@ -66,37 +66,39 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
params = mapOf("Result" to result.value),
|
||||
)
|
||||
|
||||
object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
|
||||
object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
|
||||
object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
|
||||
object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
|
||||
data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
|
||||
data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
|
||||
data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
|
||||
data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
|
||||
|
||||
object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
|
||||
data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
|
||||
|
||||
object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
|
||||
data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
|
||||
|
||||
object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
|
||||
data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
|
||||
|
||||
object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
|
||||
data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
|
||||
|
||||
object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
|
||||
data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
|
||||
|
||||
object TestnetCard : MainScreen(event = "Notice - Testnet Card")
|
||||
data object TestnetCard : MainScreen(event = "Notice - Testnet Card")
|
||||
|
||||
object DemoCard : MainScreen(event = "Notice - Demo Card")
|
||||
data object DemoCard : MainScreen(event = "Notice - Demo Card")
|
||||
|
||||
object DevelopmentCard : MainScreen(event = "Notice - Development Card")
|
||||
data object DevelopmentCard : MainScreen(event = "Notice - Development Card")
|
||||
|
||||
object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
|
||||
data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
|
||||
|
||||
object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
|
||||
data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
|
||||
|
||||
object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
|
||||
data object BackupError : MainScreen(event = "Notice - Backup Error")
|
||||
|
||||
object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
|
||||
data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
|
||||
|
||||
object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
||||
data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
|
||||
|
||||
object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
|
||||
|
||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard
|
||||
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||
is WalletNotification.Informational.NoAccount,
|
||||
is WalletNotification.Warning.LowSignatures,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.domain
|
||||
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackupValidator @Inject constructor() {
|
||||
|
||||
fun isValid(cardDTO: CardDTO): Boolean {
|
||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
||||
}
|
||||
|
||||
private fun validateCurves(cardDTO: CardDTO): Boolean {
|
||||
val config = CardConfig.createConfig(cardDTO)
|
||||
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
|
||||
// we cannot determine whether this curve is missing due to an error or because the user
|
||||
// did not want to recreate the wallet.
|
||||
val expectedCurves = config.mandatoryCurves
|
||||
.filterNot { it == EllipticCurve.Bls12381G2Aug }
|
||||
val curves = cardDTO.wallets.map { it.curve }
|
||||
for (expectedCurve in expectedCurves) {
|
||||
val cardCurvesCount = curves.count { it == expectedCurve }
|
||||
|
||||
// missing curve
|
||||
if (cardCurvesCount == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// duplicated curve
|
||||
if (cardCurvesCount > 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
|
||||
val backupStatus = cardDTO.backupStatus
|
||||
return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
private val promoRepository: PromoRepository,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val backupValidator: BackupValidator,
|
||||
) {
|
||||
|
||||
private var readyForRateAppNotification = false
|
||||
|
|
@ -57,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
buildList {
|
||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
addCriticalNotifications(userWallet)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
||||
|
||||
|
|
@ -85,7 +86,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(userWallet: UserWallet) {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
addIf(
|
||||
element = WalletNotification.Critical.BackupError,
|
||||
condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
||||
)
|
||||
|
||||
addIf(
|
||||
element = WalletNotification.Critical.DevCard,
|
||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ sealed class WalletBottomSheetConfig(
|
|||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
primaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(id = R.string.user_wallet_list_unlock_all),
|
||||
text = resourceReference(
|
||||
id = R.string.user_wallet_list_unlock_all_with,
|
||||
formatArgs = wrappedList(resourceReference(R.string.common_biometrics)),
|
||||
),
|
||||
onClick = onUnlockClick,
|
||||
),
|
||||
secondaryButtonConfig = ButtonConfig(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
title = resourceReference(id = R.string.warning_failed_to_verify_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message),
|
||||
)
|
||||
|
||||
data object BackupError : Critical(
|
||||
title = resourceReference(R.string.warning_backup_errors_title),
|
||||
subtitle = resourceReference(R.string.warning_backup_errors_message),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Warning(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue