Updated on 2026-08-14
This commit is contained in:
commit
a56bcff7dd
438 changed files with 9017 additions and 4452 deletions
|
|
@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.block.model.BlockUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import com.tangem.features.details.impl.BuildConfig
|
||||
import com.tangem.features.details.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
|
|||
onClick = { router.push(AppRoute.AppSettings) },
|
||||
),
|
||||
).let(::add)
|
||||
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = "tester_menu",
|
||||
block = BlockUM(
|
||||
text = stringReference(value = "Tester menu"),
|
||||
iconRes = R.drawable.ic_alert_24,
|
||||
onClick = { router.push(AppRoute.TesterMenu) },
|
||||
),
|
||||
).let(::add)
|
||||
}
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -26,6 +27,10 @@ dependencies {
|
|||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
|
@ -36,6 +41,8 @@ dependencies {
|
|||
implementation(tangemDeps.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(tangemDeps.hot.android)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import com.arkivanov.decompose.router.stack.push
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -20,6 +22,8 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
|
||||
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
|
||||
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
|
||||
val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks()
|
||||
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
|
||||
|
||||
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
|
||||
|
||||
|
|
@ -38,4 +42,20 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
inner class PushNotificationsComponentModelCallbacks : PushNotificationsComponent.ModelCallbacks {
|
||||
override fun onResult() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks {
|
||||
override fun onBackClick() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
override fun onAccessCodeSet() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.root.di
|
|||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.AddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
|
||||
|
|
@ -42,4 +43,9 @@ internal interface AddExistingWalletModuleBinds {
|
|||
@IntoMap
|
||||
@ClassKey(AddExistingWalletImportModel::class)
|
||||
fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SetAccessCodeModel::class)
|
||||
fun bindAccessCodeModel(model: SetAccessCodeModel): Model
|
||||
}
|
||||
|
|
@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing
|
|||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddExistingWalletChildFactory @Inject constructor() {
|
||||
internal class AddExistingWalletChildFactory @Inject constructor(
|
||||
private val pushNotificationsComponent: PushNotificationsComponent.Factory,
|
||||
) {
|
||||
|
||||
fun createChild(
|
||||
route: AddExistingWalletRoute,
|
||||
|
|
@ -27,6 +31,18 @@ internal class AddExistingWalletChildFactory @Inject constructor() {
|
|||
callbacks = model.addExistingWalletImportModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create(
|
||||
context = childContext,
|
||||
params = PushNotificationsComponent.Params.Callbacks(
|
||||
callbacks = model.pushNotificationsComponentModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent(
|
||||
context = childContext,
|
||||
params = SetAccessCodeComponent.Params(
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,10 @@ internal sealed class AddExistingWalletRoute : Route {
|
|||
|
||||
@Serializable
|
||||
object Import : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object PushNotifications : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object AccessCode : AddExistingWalletRoute()
|
||||
}
|
||||
|
|
@ -1,18 +1,30 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.MnemonicType
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class CreateMobileWalletModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
private val router: Router,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
|
|
@ -20,10 +32,30 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
CreateMobileWalletUM(
|
||||
onBackClick = { router.pop() },
|
||||
onCreateClick = ::onCreateClick,
|
||||
createButtonLoading = false,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onCreateClick() {
|
||||
// TODO create a wallet
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = true)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
saveUserWalletUseCase(
|
||||
hotUserWalletBuilder.build(),
|
||||
)
|
||||
router.push(AppRoute.Wallet)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Unit,
|
||||
) : CreateMobileWalletComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CreateMobileWalletModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -10,18 +10,12 @@ import dagger.hilt.InstallIn
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CreateMobileWalletModule
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CreateMobileWalletModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCreateMobileWalletComponentFactory(
|
||||
impl: DefaultCreateMobileWalletComponent.Factory,
|
||||
): CreateMobileWalletComponent.Factory
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet.entity
|
||||
|
||||
internal data class CreateMobileWalletUM(
|
||||
val createButtonLoading: Boolean,
|
||||
val onBackClick: () -> Unit,
|
||||
val onCreateClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -85,7 +85,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
|
|||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
text = stringResourceSafe(R.string.common_create),
|
||||
showProgress = false,
|
||||
showProgress = state.createButtonLoading,
|
||||
enabled = true,
|
||||
onClick = state.onCreateClick,
|
||||
)
|
||||
|
|
@ -133,6 +133,7 @@ private fun PreviewCreateWalletContent() {
|
|||
CreateMobileWalletContent(
|
||||
state = CreateMobileWalletUM(
|
||||
onBackClick = {},
|
||||
createButtonLoading = false,
|
||||
onCreateClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
|
||||
import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class SetAccessCodeComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val model: SetAccessCodeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SetAccessCodeContent(
|
||||
state = state,
|
||||
onBack = { model.onBack() },
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
DisableScreenshotsDisposableEffect()
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onBackClick()
|
||||
fun onAccessCodeSet()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SetAccessCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SetAccessCodeComponent.Params>()
|
||||
|
||||
internal val uiState: StateFlow<SetAccessCodeUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
fun onBack() {
|
||||
when (uiState.value.step) {
|
||||
SetAccessCodeUM.Step.AccessCode -> {
|
||||
params.callbacks.onBackClick()
|
||||
}
|
||||
SetAccessCodeUM.Step.ConfirmAccessCode -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeSecond = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState() = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
|
||||
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
|
||||
buttonEnabled = false,
|
||||
onContinue = ::onContinue,
|
||||
)
|
||||
|
||||
private fun onAccessCodeFirstChange(value: String) {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCodeFirst = value,
|
||||
buttonEnabled = value.length == uiState.value.accessCodeLength,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAccessCodeSecondChange(value: String) {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCodeSecond = value,
|
||||
buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinue() {
|
||||
when (uiState.value.step) {
|
||||
SetAccessCodeUM.Step.AccessCode -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
step = SetAccessCodeUM.Step.ConfirmAccessCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
SetAccessCodeUM.Step.ConfirmAccessCode -> {
|
||||
params.callbacks.onAccessCodeSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.entity
|
||||
|
||||
internal data class SetAccessCodeUM(
|
||||
val step: Step,
|
||||
val accessCodeFirst: String,
|
||||
val accessCodeSecond: String,
|
||||
val onAccessCodeFirstChange: (String) -> Unit,
|
||||
val onAccessCodeSecondChange: (String) -> Unit,
|
||||
val buttonEnabled: Boolean,
|
||||
val onContinue: () -> Unit,
|
||||
) {
|
||||
val accessCodeLength: Int = ACCESS_CODE_LENGTH
|
||||
|
||||
enum class Step {
|
||||
AccessCode,
|
||||
ConfirmAccessCode,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCESS_CODE_LENGTH = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.*
|
||||
import com.tangem.core.res.R
|
||||
|
||||
@Composable
|
||||
internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
AnimatedContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
targetState = state.step,
|
||||
transitionSpec = TangemAnimations.AnimatedContent
|
||||
.slide { initial, target -> target.ordinal > initial.ordinal },
|
||||
label = "AnimatedContent",
|
||||
) { step ->
|
||||
when (step) {
|
||||
AccessCode -> SetAccessCodeEnter(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
state = state,
|
||||
reEnterAccessCodeState = false,
|
||||
)
|
||||
ConfirmAccessCode -> SetAccessCodeEnter(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
state = state,
|
||||
reEnterAccessCodeState = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.imePadding(),
|
||||
text = if (state.step == ConfirmAccessCode) {
|
||||
stringResourceSafe(R.string.common_confirm)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
},
|
||||
onClick = state.onContinue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.components.fields.PinTextField
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
|
||||
|
||||
@Composable
|
||||
internal fun SetAccessCodeEnter(
|
||||
state: SetAccessCodeUM,
|
||||
reEnterAccessCodeState: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 56.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (reEnterAccessCodeState) {
|
||||
stringResourceSafe(R.string.access_code_confirm_title)
|
||||
} else {
|
||||
stringResourceSafe(R.string.access_code_create_title)
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (reEnterAccessCodeState) {
|
||||
stringResourceSafe(R.string.access_code_confirm_description)
|
||||
} else {
|
||||
stringResourceSafe(
|
||||
R.string.access_code_create_description,
|
||||
state.accessCodeLength,
|
||||
)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
PinTextField(
|
||||
length = state.accessCodeLength,
|
||||
isPasswordVisual = true,
|
||||
value = if (reEnterAccessCodeState) {
|
||||
state.accessCodeSecond
|
||||
} else {
|
||||
state.accessCodeFirst
|
||||
},
|
||||
onValueChange = if (reEnterAccessCodeState) {
|
||||
state.onAccessCodeSecondChange
|
||||
} else {
|
||||
state.onAccessCodeFirstChange
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
SetAccessCodeEnter(
|
||||
reEnterAccessCodeState = false,
|
||||
state = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.AccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = {},
|
||||
onAccessCodeSecondChange = {},
|
||||
buttonEnabled = false,
|
||||
onContinue = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview2() {
|
||||
TangemThemePreview {
|
||||
SetAccessCodeEnter(
|
||||
reEnterAccessCodeState = true,
|
||||
state = SetAccessCodeUM(
|
||||
step = SetAccessCodeUM.Step.ConfirmAccessCode,
|
||||
accessCodeFirst = "",
|
||||
accessCodeSecond = "",
|
||||
onAccessCodeFirstChange = {},
|
||||
onAccessCodeSecondChange = {},
|
||||
buttonEnabled = false,
|
||||
onContinue = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,6 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.markets.deeplink
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
|
@ -194,8 +193,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private fun loadArtworks(wallets: List<UserWallet>) {
|
||||
modelScope.launch {
|
||||
loadArtworksMutex.withLock {
|
||||
wallets.forEach { wallet ->
|
||||
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
|
||||
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
|
||||
if (!loadedArtworks.containsKey(wallet.walletId)) {
|
||||
val artwork = getCardImageUseCase(
|
||||
cardId = wallet.cardId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.features.nft
|
||||
|
||||
interface NFTFeatureToggles {
|
||||
val isNFTEnabled: Boolean
|
||||
val isNFTEVMEnabled: Boolean
|
||||
val isNFTSolanaEnabled: Boolean
|
||||
|
||||
val isNFTMediaContentEnabled: Boolean
|
||||
}
|
||||
|
|
@ -5,14 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
|||
internal class DefaultNFTFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : NFTFeatureToggles {
|
||||
override val isNFTEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_ENABLED")
|
||||
|
||||
override val isNFTEVMEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_EVM_ENABLED")
|
||||
|
||||
override val isNFTSolanaEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_SOLANA_ENABLED")
|
||||
|
||||
override val isNFTMediaContentEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
|
|
@ -17,31 +17,28 @@ import com.tangem.features.nft.impl.R
|
|||
internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.nft_collections_title),
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
TangemPullToRefreshContainer(
|
||||
config = state.pullToRefreshConfig,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
when (val content = state.content) {
|
||||
is NFTCollectionsUM.Content -> NFTCollectionsContent(content)
|
||||
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content)
|
||||
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content)
|
||||
is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content)
|
||||
}
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier,
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.nft_collections_title),
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
|
||||
TangemPullToRefreshContainer(
|
||||
config = state.pullToRefreshConfig,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
when (val content = state.content) {
|
||||
is NFTCollectionsUM.Content -> NFTCollectionsContent(content)
|
||||
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content)
|
||||
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content)
|
||||
is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,14 +52,14 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi
|
|||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing48)
|
||||
.widthIn(min = TangemTheme.dimens.size158),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = state.onReceiveClick,
|
||||
)
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = state.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import com.tangem.features.nft.common.NFTRoute
|
|||
@Composable
|
||||
internal fun NFTContent(stackState: ChildStack<NFTRoute, ComposableContentComponent>) {
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.nft.details.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
|
|
@ -8,6 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.nft.models.NFTSalePrice
|
||||
import com.tangem.features.nft.details.entity.NFTAssetUM
|
||||
import com.tangem.features.nft.details.entity.NFTDetailsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class NFTPriceChangeTransformer(
|
||||
|
|
@ -18,35 +20,50 @@ internal class NFTPriceChangeTransformer(
|
|||
override fun transform(prevState: NFTDetailsUM): NFTDetailsUM {
|
||||
val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState
|
||||
|
||||
return prevState.copy(
|
||||
nftAsset = prevState.nftAsset.copy(
|
||||
topInfo = topInfo.copy(
|
||||
salePrice = when (nftSalePrice) {
|
||||
is NFTSalePrice.Empty,
|
||||
is NFTSalePrice.Error,
|
||||
-> NFTAssetUM.SalePrice.Empty
|
||||
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
|
||||
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
|
||||
isFlickering = false,
|
||||
cryptoPrice = stringReference(
|
||||
nftSalePrice.value.format {
|
||||
crypto(
|
||||
symbol = nftSalePrice.symbol,
|
||||
decimals = nftSalePrice.decimals,
|
||||
)
|
||||
},
|
||||
),
|
||||
fiatPrice = stringReference(
|
||||
nftSalePrice.fiatValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
),
|
||||
val salePrice = when (nftSalePrice) {
|
||||
is NFTSalePrice.Empty,
|
||||
is NFTSalePrice.Error,
|
||||
-> NFTAssetUM.SalePrice.Empty
|
||||
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
|
||||
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
|
||||
isFlickering = false,
|
||||
cryptoPrice = stringReference(
|
||||
nftSalePrice.value.format {
|
||||
crypto(
|
||||
symbol = nftSalePrice.symbol,
|
||||
decimals = nftSalePrice.decimals,
|
||||
)
|
||||
},
|
||||
),
|
||||
fiatPrice = stringReference(
|
||||
nftSalePrice.fiatValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val hasSalePrice = salePrice !is NFTAssetUM.SalePrice.Empty
|
||||
|
||||
val newTopInfo = if (
|
||||
topInfo.rarity is NFTAssetUM.Rarity.Empty &&
|
||||
topInfo.description.isNullOrEmpty() &&
|
||||
!hasSalePrice
|
||||
) {
|
||||
NFTAssetUM.TopInfo.Empty
|
||||
} else {
|
||||
topInfo.copy(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price).takeIf { hasSalePrice },
|
||||
salePrice = salePrice,
|
||||
)
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
nftAsset = prevState.nftAsset.copy(
|
||||
topInfo = newTopInfo,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
|
|
@ -22,25 +19,25 @@ import com.tangem.features.nft.impl.R
|
|||
internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
modifier = Modifier,
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = state.nftAsset.name,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
|
||||
TangemPullToRefreshContainer(
|
||||
config = state.pullToRefreshConfig,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
NFTDetailsAsset(
|
||||
state = state.nftAsset,
|
||||
|
|
@ -49,16 +46,19 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
|
|||
onExploreClick = state.onExploreClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_send),
|
||||
onClick = state.onSendClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_send),
|
||||
onClick = state.onSendClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,8 +173,7 @@ internal fun NFTDetailsGroupBlock(
|
|||
} else {
|
||||
onBlockClick?.invoke()
|
||||
}
|
||||
}
|
||||
.padding(top = TangemTheme.dimens.spacing4),
|
||||
},
|
||||
text = value.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.nft.receive.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
|
|
@ -22,41 +22,33 @@ import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
|||
internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = stringResourceSafe(id = R.string.nft_receive_title),
|
||||
subtitle = state.appBarSubtitle.resolveReference(),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier,
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = stringResourceSafe(id = R.string.nft_receive_title),
|
||||
subtitle = state.appBarSubtitle.resolveReference(),
|
||||
)
|
||||
|
||||
when (val networks = state.networks) {
|
||||
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks)
|
||||
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
|
||||
when (val networks = state.networks) {
|
||||
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks)
|
||||
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
ShowBottomSheet(state.bottomSheetConfig)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.features.nft.traits.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
|
|
@ -17,25 +16,21 @@ import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
|
|||
internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = stringResourceSafe(R.string.nft_traits_title),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
NFTAssetTraitsContent(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding),
|
||||
state = state,
|
||||
)
|
||||
},
|
||||
)
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier,
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = stringResourceSafe(R.string.nft_traits_title),
|
||||
)
|
||||
|
||||
NFTAssetTraitsContent(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -71,13 +71,11 @@ internal fun MultiWalletAccessCodeEnter(
|
|||
state.onAccessCodeFirstChange
|
||||
},
|
||||
label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third),
|
||||
isError = state.codesNotMatchError || state.atLeast4CharError,
|
||||
isError = state.codesNotMatchError,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
caption = when {
|
||||
state.codesNotMatchError && reEnterAccessCodeState ->
|
||||
stringResourceSafe(R.string.onboarding_access_codes_doesnt_match)
|
||||
state.atLeast4CharError && !reEnterAccessCodeState ->
|
||||
stringResourceSafe(R.string.onboarding_access_code_too_short)
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -204,12 +204,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
|
||||
modelScope.launch {
|
||||
val availableForBuy = rampStateManager.availableForBuy(
|
||||
scanResponse = scanResponse,
|
||||
userWalletId = userWalletId,
|
||||
userWallet = userWallet ?: return@launch,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
_uiState.update {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ dependencies {
|
|||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.onramp.models)
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Deprecated("Use OnrampDeepLinkHandler")
|
||||
abstract class OnrampDeepLink : DeepLink() {
|
||||
override val uri = "tangem://onramp"
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): OnrampDeepLink
|
||||
}
|
||||
}
|
||||
|
||||
interface OnrampDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): OnrampDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface OnrampDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): OnrampDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultOnrampDeepLink @AssistedInject constructor(
|
||||
private val appRouter: AppRouter,
|
||||
private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger,
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
) : OnrampDeepLink() {
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
val txId = params[TX_ID_KEY]
|
||||
val result = OnrampRedirectResult.getResult(params[RESULT_KEY])
|
||||
|
||||
when {
|
||||
!txId.isNullOrEmpty() -> {
|
||||
// finish current onramp flow and show onramp success screen
|
||||
val replaceOnrampScreens = appRouter.stack
|
||||
.filterNot { it is AppRoute.Onramp }
|
||||
.toMutableList()
|
||||
replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId))
|
||||
appRouter.replaceAll(*replaceOnrampScreens.toTypedArray())
|
||||
}
|
||||
result != OnrampRedirectResult.Unknown -> {
|
||||
scope.launch {
|
||||
onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnrampDeepLink.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultOnrampDeepLink
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TX_ID_KEY = "tx_id"
|
||||
const val RESULT_KEY = "result"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,10 +11,6 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal interface OnrampDeeplinkModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
fun onHotTokenClick(status: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
val unavailabilityReason = rampStateManager.availableForBuy(
|
||||
scanResponse = selectedUserWallet.scanResponse,
|
||||
userWalletId = params.userWalletId,
|
||||
userWallet = selectedUserWallet,
|
||||
cryptoCurrency = status.currency,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
private fun Lce<TokenListError, TokenList>.isInsufficientBalanceForSell(): Boolean {
|
||||
return if (params.filterOperation == OnrampOperation.SELL) {
|
||||
isContent {
|
||||
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() ?: false
|
||||
(it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
@ -229,13 +229,16 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
return when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> {
|
||||
rampStateManager.availableForBuy(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
userWalletId = params.userWalletId,
|
||||
userWallet = userWallet,
|
||||
cryptoCurrency = status.currency,
|
||||
).isAvailable()
|
||||
}
|
||||
OnrampOperation.SELL -> {
|
||||
rampStateManager.availableForSell(userWallet = userWallet, status = status).isRight()
|
||||
rampStateManager.availableForSell(
|
||||
userWalletId = userWallet.walletId,
|
||||
status = status,
|
||||
sendUnavailabilityReason = null,
|
||||
).isRight()
|
||||
}
|
||||
OnrampOperation.SWAP -> {
|
||||
val isAvailable = rampStateManager.availableForSwap(
|
||||
|
|
|
|||
|
|
@ -16,4 +16,7 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
}
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
package com.tangem.features.pushnotifications.api
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface PushNotificationsComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory : ComponentFactory<Unit, PushNotificationsComponent>
|
||||
interface Factory : ComponentFactory<Params, PushNotificationsComponent>
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onResult()
|
||||
}
|
||||
|
||||
sealed class Params {
|
||||
data class Callbacks(val callbacks: ModelCallbacks) : Params()
|
||||
data class Route(val route: AppRoute) : Params()
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
internal class DefaultPushNotificationsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
@Assisted params: PushNotificationsComponent.Params,
|
||||
) : PushNotificationsComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: PushNotificationsModel = getOrCreateModel(params)
|
||||
|
|
@ -41,6 +41,9 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory : PushNotificationsComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultPushNotificationsComponent
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: PushNotificationsComponent.Params,
|
||||
): DefaultPushNotificationsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.features.pushnotifications.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM
|
||||
|
|
@ -21,15 +22,19 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class PushNotificationsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
|
||||
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
private val analyticHandler: AnalyticsEventHandler,
|
||||
private val notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
notificationsFeatureToggles: NotificationsFeatureToggles,
|
||||
) : Model(), PushNotificationsClickIntents {
|
||||
|
||||
private val params: PushNotificationsComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
PushNotificationsUM(
|
||||
showInfoAboutNotifications = notificationsFeatureToggles.isNotificationsEnabled,
|
||||
|
|
@ -51,7 +56,7 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +67,7 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +78,18 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
appRouter.push(AppRoute.Home)
|
||||
onResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResult() {
|
||||
when (params) {
|
||||
is PushNotificationsComponent.Params.Callbacks -> {
|
||||
params.callbacks.onResult()
|
||||
}
|
||||
is PushNotificationsComponent.Params.Route -> {
|
||||
appRouter.push(params.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import com.google.mlkit.vision.common.InputImage
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import com.tangem.feature.qrscanning.model.QrScanningModel
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||
|
|
@ -41,10 +42,10 @@ class DefaultQrScanningComponent @AssistedInject constructor(
|
|||
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
|
||||
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
|
||||
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(model::onQrScanned)
|
||||
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.CAMERA) }
|
||||
}
|
||||
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(model::onQrScanned)
|
||||
MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.GALLERY) }
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.qrscanning.model
|
||||
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
internal interface QrScanningClickIntents {
|
||||
|
|
@ -8,7 +9,7 @@ internal interface QrScanningClickIntents {
|
|||
|
||||
fun onBackClick()
|
||||
|
||||
fun onQrScanned(qrCode: String)
|
||||
fun onQrScanned(qrCode: String, source: QrResultSource)
|
||||
|
||||
fun onGalleryClicked()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.data.card.sdk.CardSdkProvider
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.domain.qrscanning.models.RawQrResult
|
||||
import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
|
||||
import com.tangem.feature.qrscanning.QrScanningComponent
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
|
|
@ -71,10 +73,11 @@ internal class QrScanningModel @Inject constructor(
|
|||
|
||||
override fun onBackClick() = appRouter.pop()
|
||||
|
||||
override fun onQrScanned(qrCode: String) {
|
||||
override fun onQrScanned(qrCode: String, source: QrResultSource) {
|
||||
if (qrCode.isNotBlank()) {
|
||||
modelScope.launch(dispatchers.mainImmediate) {
|
||||
emitQrScannedEventUseCase.invoke(params.source, qrCode)
|
||||
val qrCode = RawQrResult(qrCode, source, params.source)
|
||||
emitQrScannedEventUseCase.invoke(qrCode)
|
||||
}
|
||||
if (!isScanned) {
|
||||
appRouter.pop()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.qrscanning.models.QrResultSource
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.impl.R
|
||||
import com.tangem.feature.qrscanning.model.QrScanningClickIntents
|
||||
|
|
@ -28,7 +29,7 @@ internal class InitializeQrScanningStateTransformer(
|
|||
topBarConfig = constructTopBarConfig(),
|
||||
message = message,
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onQrScanned = clickIntents::onQrScanned,
|
||||
onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) },
|
||||
onGalleryClick = clickIntents::onGalleryClicked,
|
||||
pasteAction = constructPasteAction(),
|
||||
)
|
||||
|
|
@ -50,7 +51,7 @@ internal class InitializeQrScanningStateTransformer(
|
|||
private fun constructPasteAction(): PasteAction {
|
||||
val uri = clipboardManager.getText()
|
||||
return if (uri != null) {
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri) }
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) }
|
||||
} else {
|
||||
PasteAction.None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.StartReferralBody
|
|||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.feature.referral.converters.ReferralConverter
|
||||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
import com.tangem.feature.referral.domain.models.ReferralData
|
||||
|
|
@ -97,13 +96,13 @@ internal class ReferralRepositoryImpl @Inject constructor(
|
|||
sdkToken = sdkToken,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
} else {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ android {
|
|||
namespace = "com.tangem.features.send.v2.api"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
@ -34,4 +38,14 @@ dependencies {
|
|||
/** Other */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.domain.staking.models)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -21,11 +21,9 @@ sealed class FeeSelectorUM {
|
|||
val fees: TransactionFee,
|
||||
val feeItems: ImmutableList<FeeItem>,
|
||||
val selectedFeeItem: FeeItem,
|
||||
val isFeeApproximate: Boolean,
|
||||
val feeExtraInfo: FeeExtraInfo,
|
||||
val feeFiatRateUM: FeeFiatRateUM?,
|
||||
val displayNonceInput: Boolean,
|
||||
val nonce: BigInteger?,
|
||||
val onNonceChange: (String) -> Unit,
|
||||
val feeNonce: FeeNonce,
|
||||
) : FeeSelectorUM()
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +33,21 @@ data class FeeFiatRateUM(
|
|||
val appCurrency: AppCurrency,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class FeeExtraInfo(
|
||||
val isFeeApproximate: Boolean,
|
||||
val isFeeConvertibleToFiat: Boolean,
|
||||
val isTronToken: Boolean,
|
||||
)
|
||||
|
||||
sealed class FeeNonce {
|
||||
data object None : FeeNonce()
|
||||
data class Nonce(
|
||||
val nonce: BigInteger?,
|
||||
val onNonceChange: (String) -> Unit,
|
||||
) : FeeNonce()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class FeeItem {
|
||||
abstract val fee: Fee
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ sealed class FeeSelectorParams {
|
|||
abstract val state: FeeSelectorUM
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val suggestedFeeState: SuggestedFeeState
|
||||
abstract val feeDisplaySource: FeeDisplaySource
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ sealed class FeeSelectorParams {
|
|||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
) : FeeSelectorParams()
|
||||
|
|
@ -28,6 +30,7 @@ sealed class FeeSelectorParams {
|
|||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
val callback: FeeSelectorModelCallback,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
|
|||
|
||||
interface SendDestinationBlockComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
object FeeCalculationUtils {
|
||||
|
||||
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||
|
||||
/**
|
||||
* Check and calculates subtracted amount
|
||||
*/
|
||||
fun checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
return if (isFeeCoverage) {
|
||||
balance.minus(reduceAmountBy).minus(feeValue)
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too high
|
||||
*/
|
||||
fun checkIfCustomFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content): Pair<Boolean, String> {
|
||||
val defaultResult = false to ""
|
||||
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return defaultResult
|
||||
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return defaultResult
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return defaultResult
|
||||
val highValue = multipleFees.priority.amount.value ?: return defaultResult
|
||||
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = if (highValue > BigDecimal.ZERO) {
|
||||
customValue / highValue
|
||||
} else {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
val isFeeTooHigh = diff > FEE_MAX_DIFF
|
||||
return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too low
|
||||
*/
|
||||
fun checkIfCustomFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean {
|
||||
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return false
|
||||
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return false
|
||||
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
|
||||
return minimumValue > customValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sending amount with fee is greater than balance
|
||||
*/
|
||||
fun checkFeeCoverage(
|
||||
isSubtractAvailable: Boolean,
|
||||
balance: BigDecimal,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
): Boolean {
|
||||
if (!isSubtractAvailable) return false
|
||||
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
|
||||
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if fee exceeds fee paid currency balance
|
||||
*/
|
||||
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
|
||||
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SendNotificationsUpdateListener {
|
||||
/** Flow triggers notifications update */
|
||||
val updateTriggerFlow: Flow<NotificationData>
|
||||
|
||||
/** Flow returns whether there is error notifications */
|
||||
val hasErrorFlow: Flow<Boolean>
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
|
||||
interface SendNotificationsUpdateTrigger {
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackHasError(hasError: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerUpdate(data: NotificationData)
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class FeeCalculationUtilsTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract available and fee coverage needed WHEN checkAndCalculateSubtractedAmount THEN returns subtracted amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("6"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(BigDecimal("3"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN amount subtract not available WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = false
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN sufficient balance for amount and fee WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no balance WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
|
||||
// GIVEN
|
||||
val isAmountSubtractAvailable = true
|
||||
val cryptoCurrencyStatus = createCryptoCurrencyStatus(null)
|
||||
val amountValue = BigDecimal("5")
|
||||
val feeValue = BigDecimal("2")
|
||||
val reduceAmountBy = BigDecimal("1")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(amountValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee exceeds balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("5")
|
||||
val feeAmount = BigDecimal("10")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee within balance WHEN checkExceedBalance THEN returns false`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount: BigDecimal? = null
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee balance WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance: BigDecimal? = null
|
||||
val feeAmount = BigDecimal("5")
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero fee amount WHEN checkExceedBalance THEN returns true`() {
|
||||
// GIVEN
|
||||
val feeBalance = BigDecimal("10")
|
||||
val feeAmount = BigDecimal.ZERO
|
||||
|
||||
// WHEN
|
||||
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
private fun createCryptoCurrencyStatus(amount: BigDecimal?): CryptoCurrencyStatus {
|
||||
val value = if (amount != null) {
|
||||
CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
yieldBalance = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = mockk(relaxed = true),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyStatus.NoAmount(
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
)
|
||||
}
|
||||
return CryptoCurrencyStatus(
|
||||
currency = mockk(relaxed = true),
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ android {
|
|||
namespace = "com.tangem.features.send.v2.impl"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.sendV2.api)
|
||||
|
|
@ -79,4 +83,13 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -48,10 +48,14 @@ internal sealed class CommonSendAnalyticEvents(
|
|||
data class TransactionError(
|
||||
val categoryName: String,
|
||||
val token: String,
|
||||
val blockchain: String,
|
||||
) : CommonSendAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Error - Transaction Rejected",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
/** Close button clicked */
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
params = FeeSelectorParams.FeeSelectorDetailsParams(
|
||||
state = model.uiState.value,
|
||||
onLoadFee = params.onLoadFee,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import com.tangem.features.send.v2.api.entity.FeeItem
|
|||
internal interface FeeSelectorIntents {
|
||||
fun onFeeItemSelected(feeItem: FeeItem)
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
fun onNonceChange(value: String)
|
||||
fun onDoneClick()
|
||||
}
|
||||
|
||||
internal class StubFeeSelectorIntents : FeeSelectorIntents {
|
||||
override fun onFeeItemSelected(feeItem: FeeItem) {}
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
||||
override fun onNonceChange(value: String) {}
|
||||
override fun onDoneClick() {}
|
||||
}
|
||||
|
|
@ -15,10 +15,7 @@ import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
|||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorCustomValueChangedTransformer
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer
|
||||
import com.tangem.features.send.v2.feeselector.model.transformers.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -67,6 +64,7 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
uiState.update(
|
||||
FeeSelectorLoadedTransformer(
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
fees = fee,
|
||||
suggestedFeeState = params.suggestedFeeState,
|
||||
|
|
@ -80,7 +78,7 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isFeeApproximate(amountType: AmountType): Boolean {
|
||||
val networkId = params.cryptoCurrencyStatus.currency.network.id
|
||||
val networkId = params.feeCryptoCurrencyStatus.currency.network.id
|
||||
return isFeeApproximateUseCase(networkId = networkId, amountType = amountType)
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +93,15 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
value = value,
|
||||
intents = this,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNonceChange(value: String) {
|
||||
uiState.update(FeeSelectorNonceChangeTransformer(value = value))
|
||||
}
|
||||
|
||||
override fun onDoneClick() {
|
||||
(params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
package com.tangem.features.send.v2.feeselector.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeSelectorLoadedTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val fees: TransactionFee,
|
||||
private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState,
|
||||
|
|
@ -26,7 +28,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
normalFee = fees.normal,
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
|
|
@ -44,20 +46,32 @@ internal class FeeSelectorLoadedTransformer(
|
|||
-> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market }
|
||||
}
|
||||
|
||||
val nonce = ((prevState as? FeeSelectorUM.Content)?.feeNonce as? FeeNonce.Nonce)?.nonce
|
||||
|
||||
return FeeSelectorUM.Content(
|
||||
fees = fees,
|
||||
feeItems = feeItems,
|
||||
selectedFeeItem = selectedFee,
|
||||
isFeeApproximate = isFeeApproximate,
|
||||
feeFiatRateUM = cryptoCurrencyStatus.value.fiatRate?.let { rate ->
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = isFeeApproximate,
|
||||
isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate,
|
||||
isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token &&
|
||||
isTron(cryptoCurrencyStatus.currency.network.rawId),
|
||||
),
|
||||
feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
|
||||
FeeFiatRateUM(
|
||||
rate = rate,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
},
|
||||
displayNonceInput = false,
|
||||
nonce = null,
|
||||
onNonceChange = {},
|
||||
feeNonce = if (fees.normal is Fee.Ethereum) {
|
||||
FeeNonce.Nonce(
|
||||
nonce = nonce,
|
||||
onNonceChange = feeSelectorIntents::onNonceChange,
|
||||
)
|
||||
} else {
|
||||
FeeNonce.None
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.send.v2.feeselector.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.api.entity.FeeNonce
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class FeeSelectorNonceChangeTransformer(
|
||||
private val value: String,
|
||||
) : Transformer<FeeSelectorUM> {
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
val state = prevState as? FeeSelectorUM.Content ?: return prevState
|
||||
val feeNonce = state.feeNonce as? FeeNonce.Nonce ?: return prevState
|
||||
if (value.isEmpty()) {
|
||||
return state.copy(feeNonce = feeNonce.copy(null))
|
||||
}
|
||||
|
||||
val nonce = value.toBigIntegerOrNull() ?: return prevState
|
||||
|
||||
return state.copy(feeNonce = feeNonce.copy(nonce = nonce))
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,17 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -29,9 +31,8 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -50,21 +51,14 @@ internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier =
|
|||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
text = stringResourceSafe(R.string.common_network_fee_title),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing6)
|
||||
.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = R.drawable.ic_token_info_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
SpacerWMax()
|
||||
FeeSelectorDescription(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeSelectorDescription(state: FeeSelectorUM, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
FeeSelectorStaticPart(modifier = Modifier.weight(1f))
|
||||
when (state) {
|
||||
is FeeSelectorUM.Content -> FeeContent(state)
|
||||
is FeeSelectorUM.Loading -> FeeLoading()
|
||||
|
|
@ -74,7 +68,31 @@ internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier =
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeError() {
|
||||
private fun FeeSelectorStaticPart(modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4)
|
||||
.weight(1f, fill = false),
|
||||
text = stringResourceSafe(R.string.common_network_fee_title),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing6)
|
||||
.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = R.drawable.ic_token_info_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeError() {
|
||||
Text(
|
||||
text = EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -83,7 +101,7 @@ private fun RowScope.FeeError() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeLoading() {
|
||||
private fun FeeLoading() {
|
||||
TextShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.body1,
|
||||
|
|
@ -92,62 +110,87 @@ private fun RowScope.FeeLoading() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeContent(state: FeeSelectorUM.Content) {
|
||||
private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) {
|
||||
val fiatRate = state.feeFiatRateUM
|
||||
EllipsisText(
|
||||
text = if (fiatRate != null) {
|
||||
getFiatString(
|
||||
value = state.selectedFeeItem.fee.amount.value,
|
||||
rate = fiatRate.rate,
|
||||
appCurrency = fiatRate.appCurrency,
|
||||
approximate = state.isFeeApproximate,
|
||||
)
|
||||
} else {
|
||||
state.selectedFeeItem.fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = state.selectedFeeItem.fee.amount.currencySymbol,
|
||||
decimals = state.selectedFeeItem.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(width = 18.dp, height = 24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_select_18_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
EllipsisText(
|
||||
text = if (fiatRate != null) {
|
||||
getFiatString(
|
||||
value = state.selectedFeeItem.fee.amount.value,
|
||||
rate = fiatRate.rate,
|
||||
appCurrency = fiatRate.appCurrency,
|
||||
approximate = state.feeExtraInfo.isFeeApproximate,
|
||||
)
|
||||
} else {
|
||||
state.selectedFeeItem.fee.amount.value.format {
|
||||
crypto(
|
||||
symbol = state.selectedFeeItem.fee.amount.currencySymbol,
|
||||
decimals = state.selectedFeeItem.fee.amount.decimals,
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(width = 18.dp, height = 24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_select_18_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
|
||||
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun FeeSelectorBlockContent_Preview() {
|
||||
private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvider::class) state: FeeSelectorUM) {
|
||||
TangemThemePreview {
|
||||
val feeItem = FeeItem.Market(
|
||||
Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)),
|
||||
)
|
||||
FeeSelectorBlockContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = FeeSelectorUM.Content(
|
||||
feeItems = persistentListOf(feeItem),
|
||||
selectedFeeItem = feeItem,
|
||||
isFeeApproximate = false,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("2500"),
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
displayNonceInput = false,
|
||||
nonce = null,
|
||||
onNonceChange = {},
|
||||
fees = TransactionFee.Single(feeItem.fee),
|
||||
),
|
||||
)
|
||||
FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth(), state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
|
||||
private val maxFeeItem = FeeItem.Market(
|
||||
fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Ethereum)),
|
||||
)
|
||||
private val lowFeeItem =
|
||||
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
|
||||
|
||||
override val values: Sequence<FeeSelectorUM> = sequenceOf(
|
||||
FeeSelectorUM.Content(
|
||||
feeItems = persistentListOf(lowFeeItem),
|
||||
selectedFeeItem = lowFeeItem,
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("2500"),
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
fees = TransactionFee.Single(lowFeeItem.fee),
|
||||
),
|
||||
FeeSelectorUM.Content(
|
||||
feeItems = persistentListOf(maxFeeItem),
|
||||
selectedFeeItem = maxFeeItem,
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeNonce = FeeNonce.None,
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("2500000000000"),
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
fees = TransactionFee.Single(maxFeeItem.fee),
|
||||
),
|
||||
FeeSelectorUM.Error(GetFeeError.UnknownError),
|
||||
FeeSelectorUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnter
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
|
|
@ -45,10 +46,7 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents
|
||||
|
|
@ -57,7 +55,6 @@ import com.tangem.utils.StringsSigns
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@Composable
|
||||
internal fun FeeSelectorModalBottomSheet(
|
||||
|
|
@ -158,7 +155,7 @@ private fun FeeSelectorItems(
|
|||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeFiatRateUM != null) {
|
||||
|
|
@ -184,7 +181,7 @@ private fun FeeSelectorItems(
|
|||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeFiatRateUM != null) {
|
||||
|
|
@ -210,7 +207,7 @@ private fun FeeSelectorItems(
|
|||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeFiatRateUM != null) {
|
||||
|
|
@ -236,7 +233,7 @@ private fun FeeSelectorItems(
|
|||
crypto(
|
||||
symbol = item.fee.amount.currencySymbol,
|
||||
decimals = item.fee.amount.decimals,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
).fee(canBeLower = state.feeExtraInfo.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeFiatRateUM != null) {
|
||||
|
|
@ -258,9 +255,7 @@ private fun FeeSelectorItems(
|
|||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
onValueChange = feeSelectorIntents::onCustomFeeValueChange,
|
||||
displayNonceInput = state.displayNonceInput,
|
||||
nonce = state.nonce,
|
||||
onNonceChange = state.onNonceChange,
|
||||
nonce = state.feeNonce,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -275,9 +270,7 @@ private fun CustomFeeBlock(
|
|||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
displayNonceInput: Boolean,
|
||||
nonce: BigInteger?,
|
||||
onNonceChange: (String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -310,9 +303,7 @@ private fun CustomFeeBlock(
|
|||
ExpandedCustomFeeItems(
|
||||
customFeeFields = customFee.customValues,
|
||||
onValueChange = onValueChange,
|
||||
displayNonceInput = displayNonceInput,
|
||||
nonce = nonce,
|
||||
onNonceChange = onNonceChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -322,14 +313,12 @@ private fun CustomFeeBlock(
|
|||
private fun ExpandedCustomFeeItems(
|
||||
customFeeFields: ImmutableList<CustomFeeFieldUM>,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
displayNonceInput: Boolean,
|
||||
nonce: BigInteger?,
|
||||
onNonceChange: (String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
customFeeFields.fastForEachIndexed { index, field ->
|
||||
val showDivider = index != customFeeFields.size - 1 || displayNonceInput
|
||||
val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce
|
||||
if (field.label != null) {
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = field.value,
|
||||
|
|
@ -338,6 +327,7 @@ private fun ExpandedCustomFeeItems(
|
|||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
info = field.label,
|
||||
description = field.footer,
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
|
|
@ -351,6 +341,7 @@ private fun ExpandedCustomFeeItems(
|
|||
title = field.title,
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
symbol = field.symbol,
|
||||
description = field.footer,
|
||||
onValueChange = { onValueChange(index, it) },
|
||||
keyboardOptions = field.keyboardOptions,
|
||||
keyboardActions = field.keyboardActions,
|
||||
|
|
@ -359,18 +350,21 @@ private fun ExpandedCustomFeeItems(
|
|||
}
|
||||
}
|
||||
|
||||
if (displayNonceInput) {
|
||||
// TODO implement v2 input without binding to amount
|
||||
InputRowEnterInfoAmountV2(
|
||||
text = nonce?.toString() ?: "",
|
||||
decimals = 0,
|
||||
if (nonce is FeeNonce.Nonce) {
|
||||
InputRowEnter(
|
||||
text = nonce.nonce?.toString().orEmpty(),
|
||||
title = resourceReference(R.string.send_nonce),
|
||||
titleColor = TangemTheme.colors.text.tertiary,
|
||||
symbol = null,
|
||||
onValueChange = onNonceChange,
|
||||
description = resourceReference(R.string.send_nonce_footer),
|
||||
onValueChange = nonce.onNonceChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
keyboardActions = KeyboardActions(),
|
||||
placeholder = resourceReference(R.string.send_nonce_hint),
|
||||
titleColor = TangemTheme.colors.text.secondary,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -504,14 +498,16 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<
|
|||
// amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum),
|
||||
// ),
|
||||
selectedFeeItem = customFeeItem,
|
||||
isFeeApproximate = true,
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = true,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal.TEN,
|
||||
appCurrency = AppCurrency.Default,
|
||||
),
|
||||
displayNonceInput = true,
|
||||
onNonceChange = {},
|
||||
nonce = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
fees = TransactionFee.Single(customFeeItem.fee),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
||||
|
|
@ -53,6 +54,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendComponent.Params,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : SendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<CommonSendRoute>()
|
||||
|
|
@ -298,6 +300,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess)
|
||||
},
|
||||
),
|
||||
feeSelectorComponentFactory = feeSelectorComponentFactory,
|
||||
)
|
||||
} else {
|
||||
model.showAlertError()
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.v2.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
|
|
@ -35,6 +37,7 @@ import kotlinx.coroutines.flow.*
|
|||
internal class SendConfirmComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmModel = getOrCreateModel(params = params)
|
||||
|
|
@ -92,6 +95,19 @@ internal class SendConfirmComponent(
|
|||
onClick = model::showEditFee,
|
||||
)
|
||||
|
||||
private val feeSelectorBlockComponent = feeSelectorComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = model.uiState.value.feeSelectorUM,
|
||||
onLoadFee = params.onLoadFee,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
suggestedFeeState = model.suggestedFeeState,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
||||
private val notificationsComponent = DefaultSendNotificationsComponent(
|
||||
appComponentContext = child("sendConfirmNotifications"),
|
||||
params = SendNotificationsComponent.Params(
|
||||
|
|
@ -136,6 +152,7 @@ internal class SendConfirmComponent(
|
|||
destinationBlockComponent = destinationBlockComponent,
|
||||
amountBlockComponent = amountBlockComponent,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
notificationsComponent = notificationsComponent,
|
||||
notificationsUM = notificationState,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,13 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
|||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeNonce
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
|
||||
|
|
@ -49,13 +55,13 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmIn
|
|||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
|
|
@ -86,13 +92,14 @@ internal class SendConfirmModel @Inject constructor(
|
|||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
|
||||
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
|
||||
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
|
||||
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
|
||||
private val notificationsUpdateListener: SendNotificationsUpdateListener,
|
||||
private val alertFactory: SendConfirmAlertFactory,
|
||||
private val sendAnalyticHelper: SendAnalyticHelper,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
|
||||
) : Model(), SendConfirmClickIntents {
|
||||
) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback {
|
||||
|
||||
private val params: SendConfirmComponent.Params = paramsContainer.require()
|
||||
|
||||
|
|
@ -115,6 +122,8 @@ internal class SendConfirmModel @Inject constructor(
|
|||
get() = uiState.value.feeUM as? FeeUM.Content
|
||||
private val feeSelectorUM
|
||||
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
|
||||
private val feeUMV2
|
||||
get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content
|
||||
|
||||
val confirmData: ConfirmData
|
||||
get() = ConfirmData(
|
||||
|
|
@ -129,6 +138,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
private var sendIdleTimer: Long = 0L
|
||||
private var isAmountSubtractAvailable = false
|
||||
internal var suggestedFeeState: FeeSelectorParams.SuggestedFeeState = FeeSelectorParams.SuggestedFeeState.None
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
|
|
@ -300,16 +310,25 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun subscribeOnNotificationsUpdateTrigger() {
|
||||
notificationsUpdateTrigger.hasErrorFlow
|
||||
notificationsUpdateListener.hasErrorFlow
|
||||
.onEach { hasError ->
|
||||
_uiState.update {
|
||||
val feeUM = it.feeUM as? FeeUM.Content
|
||||
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
|
||||
it.copy(
|
||||
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
|
||||
) ?: it.confirmUM,
|
||||
)
|
||||
if (_uiState.value.isRedesignEnabled) {
|
||||
val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content
|
||||
it.copy(
|
||||
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeUM != null,
|
||||
) ?: it.confirmUM,
|
||||
)
|
||||
} else {
|
||||
val feeUM = it.feeUM as? FeeUM.Content
|
||||
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
|
||||
it.copy(
|
||||
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
|
||||
) ?: it.confirmUM,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -319,9 +338,18 @@ internal class SendConfirmModel @Inject constructor(
|
|||
val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val destination = destinationUM?.addressTextField?.actualAddress ?: return
|
||||
val memo = destinationUM?.memoTextField?.value
|
||||
val fee = feeSelectorUM?.selectedFee
|
||||
val isRedesignEnabled = uiState.value.isRedesignEnabled
|
||||
val fee = if (isRedesignEnabled) {
|
||||
feeUMV2?.selectedFeeItem?.fee
|
||||
} else {
|
||||
feeSelectorUM?.selectedFee
|
||||
}
|
||||
val nonce = if (isRedesignEnabled) {
|
||||
(feeUMV2?.feeNonce as? FeeNonce.Nonce)?.nonce
|
||||
} else {
|
||||
feeSelectorUM?.nonce
|
||||
}
|
||||
val feeValue = fee?.amount?.value ?: return
|
||||
val nonce = feeSelectorUM?.nonce
|
||||
|
||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
|
|
@ -377,6 +405,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
CommonSendAnalyticEvents.TransactionError(
|
||||
categoryName = analyticsCategoryName,
|
||||
token = cryptoCurrency.symbol,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -448,14 +477,25 @@ internal class SendConfirmModel @Inject constructor(
|
|||
)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
confirmUM = SendConfirmationNotificationsTransformer(
|
||||
feeUM = uiState.value.feeUM,
|
||||
amountUM = uiState.value.amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
).transform(uiState.value.confirmUM),
|
||||
confirmUM = if (uiState.value.isRedesignEnabled) {
|
||||
SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = uiState.value.feeSelectorUM,
|
||||
amountUM = uiState.value.amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
).transform(uiState.value.confirmUM)
|
||||
} else {
|
||||
SendConfirmationNotificationsTransformer(
|
||||
feeUM = uiState.value.feeUM,
|
||||
amountUM = uiState.value.amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
).transform(uiState.value.confirmUM)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -556,6 +596,12 @@ internal class SendConfirmModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) {
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
_uiState.update { it.copy(feeSelectorUM = feeSelectorUM) }
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CHECK_FEE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.common.utils.formatFooterFiatFee
|
||||
import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendConfirmationNotificationsTransformerV2(
|
||||
private val feeSelectorUM: FeeSelectorUM,
|
||||
private val amountUM: AmountState,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val analyticsCategoryName: String,
|
||||
) : Transformer<ConfirmUM> {
|
||||
override fun transform(prevState: ConfirmUM): ConfirmUM {
|
||||
val state = prevState as? ConfirmUM.Content ?: return prevState
|
||||
val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content ?: return prevState
|
||||
return state.copy(
|
||||
sendingFooter = getSendingFooterText(),
|
||||
notifications = buildList {
|
||||
addTooHighNotification(feeSelectorUM)
|
||||
addTooLowNotification(feeSelectorUM)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) {
|
||||
if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) {
|
||||
add(NotificationUM.Warning.FeeTooLow)
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.NoticeTransactionDelays(
|
||||
categoryName = analyticsCategoryName,
|
||||
token = cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) {
|
||||
val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM)
|
||||
if (isFeeTooHigh) {
|
||||
add(NotificationUM.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSendingFooterText(): TextReference {
|
||||
val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content
|
||||
val amountUM = amountUM as? AmountState.Data
|
||||
val fee = feeSelectorUM?.selectedFeeItem?.fee
|
||||
|
||||
if (fee == null || amountUM == null) return TextReference.EMPTY
|
||||
|
||||
val fiatAmountValue = amountUM.amountTextField.fiatAmount.value
|
||||
val fiatFeeValue = feeSelectorUM.feeFiatRateUM?.rate?.let { fee.amount.value?.multiply(it) }
|
||||
|
||||
val fiatSendingValue = if (feeSelectorUM.feeFiatRateUM != null) {
|
||||
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
|
||||
} else {
|
||||
fiatAmountValue
|
||||
}
|
||||
|
||||
val fiatSending = fiatSendingValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
val fiatFee = formatFooterFiatFee(
|
||||
amount = fee.amount.copy(value = fiatFeeValue),
|
||||
isFeeConvertibleToFiat = feeSelectorUM.feeFiatRateUM != null,
|
||||
isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
|
||||
return if (fee is Fee.Tron) {
|
||||
getTronTokenFeeSendingText(
|
||||
fee = fee,
|
||||
fiatFee = fiatFee,
|
||||
fiatSending = stringReference(fiatSending),
|
||||
)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = if (feeSelectorUM.feeFiatRateUM != null) {
|
||||
R.string.send_summary_transaction_description
|
||||
} else {
|
||||
R.string.send_summary_transaction_description_no_fiat_fee
|
||||
},
|
||||
formatArgs = wrappedList(fiatSending, fiatFee),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.send.v2.send.confirm.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
|
|
@ -19,6 +21,7 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.common.ui.SendingText
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.common.ui.tapHelp
|
||||
|
|
@ -40,6 +43,7 @@ internal fun SendConfirmContent(
|
|||
destinationBlockComponent: DefaultSendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
notificationsComponent: DefaultSendNotificationsComponent,
|
||||
notificationsUM: ImmutableList<NotificationUM>,
|
||||
) {
|
||||
|
|
@ -54,6 +58,7 @@ internal fun SendConfirmContent(
|
|||
destinationBlockComponent = destinationBlockComponent,
|
||||
amountBlockComponent = amountBlockComponent,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
if (confirmUM != null) {
|
||||
tapHelp(isDisplay = confirmUM.showTapHelp)
|
||||
|
|
@ -79,18 +84,24 @@ private fun LazyListScope.blocks(
|
|||
destinationBlockComponent: DefaultSendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
item(key = BLOCKS_KEY) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (uiState.isRedesignEnabled) {
|
||||
amountBlockComponent.Content(modifier = Modifier)
|
||||
destinationBlockComponent.Content(modifier = Modifier)
|
||||
feeSelectorBlockComponent.Content(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
} else {
|
||||
TransactionDoneTitleAnimated(uiState)
|
||||
destinationBlockComponent.Content(modifier = Modifier)
|
||||
amountBlockComponent.Content(modifier = Modifier)
|
||||
feeBlockComponent.Content(modifier = Modifier)
|
||||
}
|
||||
feeBlockComponent.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import com.tangem.domain.wallets.models.requireColdWallet
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
|
|
@ -395,5 +396,6 @@ internal class SendModel @Inject constructor(
|
|||
navigationUM = NavigationUM.Empty,
|
||||
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
|
||||
confirmData = null,
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.v2.send.ui.state
|
|||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.confirm.model.ConfirmData
|
||||
|
|
@ -11,6 +12,7 @@ internal data class SendUM(
|
|||
val amountUM: AmountState,
|
||||
val destinationUM: DestinationUM,
|
||||
val feeUM: FeeUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
val isRedesignEnabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import com.tangem.domain.wallets.models.requireColdWallet
|
|||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
|
||||
|
|
@ -47,7 +49,6 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
|
|||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.utils.transformer.update
|
||||
|
|
@ -72,7 +73,8 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
|
||||
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
|
||||
private val notificationsUpdateListener: SendNotificationsUpdateListener,
|
||||
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
|
||||
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
|
||||
private val alertFactory: SendConfirmAlertFactory,
|
||||
|
|
@ -242,7 +244,7 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun subscribeOnNotificationsUpdateTrigger() {
|
||||
notificationsUpdateTrigger.hasErrorFlow
|
||||
notificationsUpdateListener.hasErrorFlow
|
||||
.onEach { hasError ->
|
||||
_uiState.update {
|
||||
val feeUM = it.feeUM as? FeeUM.Content
|
||||
|
|
@ -310,6 +312,7 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
CommonSendAnalyticEvents.TransactionError(
|
||||
categoryName = analyticsCategoryName,
|
||||
token = cryptoCurrency.symbol,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ internal class SendAmountModel @Inject constructor(
|
|||
},
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
|
|
@ -128,6 +127,8 @@ internal class SendAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
|
||||
if (uiState.value is AmountState.Data) {
|
||||
_uiState.update(
|
||||
AmountBoundaryUpdateTransformer(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor(
|
|||
}.launchIn(componentScope)
|
||||
}
|
||||
|
||||
fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
|
||||
override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,17 @@
|
|||
package com.tangem.features.send.v2.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface NotificationsUpdateTrigger {
|
||||
/** Flow triggers notifications update */
|
||||
val updateTriggerFlow: Flow<NotificationData>
|
||||
|
||||
/** Flow returns whether there is error notifications */
|
||||
val hasErrorFlow: Flow<Boolean>
|
||||
|
||||
/** Trigger return callback with check result */
|
||||
suspend fun callbackHasError(hasError: Boolean)
|
||||
|
||||
/** Trigger fee check reload */
|
||||
suspend fun triggerUpdate(data: NotificationData)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class DefaultNotificationsUpdateTrigger @Inject constructor() : NotificationsUpdateTrigger {
|
||||
internal class DefaultNotificationsUpdateTrigger @Inject constructor() :
|
||||
SendNotificationsUpdateListener,
|
||||
SendNotificationsUpdateTrigger {
|
||||
|
||||
private val _updateTriggerFlow = MutableSharedFlow<NotificationData>()
|
||||
override val updateTriggerFlow = _updateTriggerFlow.asSharedFlow()
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
package com.tangem.features.send.v2.subcomponents.notifications.di
|
||||
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.DefaultNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal object NotificationsModule {
|
||||
internal interface NotificationsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesNotificationsUpdateTrigger(): NotificationsUpdateTrigger {
|
||||
return DefaultNotificationsUpdateTrigger()
|
||||
}
|
||||
@Binds
|
||||
fun bindsNotificationsUpdateTrigger(impl: DefaultNotificationsUpdateTrigger): SendNotificationsUpdateTrigger
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun bindsNotificationsUpdateListener(impl: DefaultNotificationsUpdateTrigger): SendNotificationsUpdateListener
|
||||
}
|
||||
|
|
@ -37,12 +37,13 @@ import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
|||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.analytics.NotificationsAnalyticEvents
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
|
|
@ -74,7 +75,8 @@ internal class NotificationsModel @Inject constructor(
|
|||
private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase,
|
||||
private val sendFeeReloadTrigger: SendFeeReloadTrigger,
|
||||
private val sendAmountReduceTrigger: SendAmountReduceTrigger,
|
||||
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
|
||||
private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger,
|
||||
private val notificationsUpdateListener: SendNotificationsUpdateListener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -101,7 +103,7 @@ internal class NotificationsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun subscribeToNotificationUpdateTrigger() {
|
||||
notificationsUpdateTrigger.updateTriggerFlow
|
||||
notificationsUpdateListener.updateTriggerFlow
|
||||
.onEach { updateState(it) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,439 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
|
||||
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.Locale
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
|
||||
class SendConfirmationNotificationsTransformerTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
|
||||
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
|
||||
private val analyticsCategoryName = "test_category"
|
||||
|
||||
@Test
|
||||
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM: FeeUM = mockk(relaxed = true)
|
||||
val amountUM: AmountState = mockk(relaxed = true)
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState: ConfirmUM = ConfirmUM.Empty
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(initialState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee UM not content WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM: FeeUM = FeeUM.Empty()
|
||||
val amountUM: AmountState = mockk(relaxed = true)
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState: ConfirmUM.Content = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(initialState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM = createNormalFeeUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).isEmpty()
|
||||
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM = createFeeTooHighUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(1)
|
||||
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM = createFeeTooLowUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(1)
|
||||
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
|
||||
verify { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest {
|
||||
// GIVEN
|
||||
val feeUM = createFeeTooHighAndTooLowUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(2)
|
||||
assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
|
||||
assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
|
||||
}
|
||||
|
||||
private fun createTestConfirmUM(): ConfirmUM.Content {
|
||||
return ConfirmUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
walletName = mockk(relaxed = true),
|
||||
isSending = false,
|
||||
showTapHelp = false,
|
||||
sendingFooter = mockk(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestAmountUM(): AmountState.Data {
|
||||
val cryptoAmount = com.tangem.domain.tokens.model.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("1.5"),
|
||||
decimals = 8,
|
||||
)
|
||||
val fiatAmount = com.tangem.domain.tokens.model.Amount(
|
||||
currencySymbol = "USD",
|
||||
value = BigDecimal("50.00"),
|
||||
decimals = 2,
|
||||
)
|
||||
|
||||
return AmountState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
isRedesignEnabled = false,
|
||||
title = mockk(relaxed = true),
|
||||
availableBalance = mockk(relaxed = true),
|
||||
tokenName = mockk(relaxed = true),
|
||||
tokenIconState = mockk(relaxed = true),
|
||||
segmentedButtonConfig = persistentListOf(),
|
||||
selectedButton = 0,
|
||||
isSegmentedButtonsEnabled = false,
|
||||
amountTextField = AmountFieldModel(
|
||||
value = "1.5",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
cryptoAmount = cryptoAmount,
|
||||
fiatAmount = fiatAmount,
|
||||
isFiatValue = false,
|
||||
fiatValue = "50.00",
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = mockk(relaxed = true),
|
||||
),
|
||||
appCurrency = appCurrency,
|
||||
isEditingDisabled = false,
|
||||
reduceAmountBy = BigDecimal.ZERO,
|
||||
isIgnoreReduce = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNormalFeeUM(): FeeUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Market,
|
||||
selectedFee = fee,
|
||||
customValues = persistentListOf(),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighUM(): FeeUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooLowUM(): FeeUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.0001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = fee,
|
||||
priority = fee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighAndTooLowUM(): FeeUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = com.tangem.blockchain.common.Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.008"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setUpLocale() {
|
||||
Locale.setDefault(Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,505 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.Locale
|
||||
import com.tangem.domain.tokens.model.Amount as DomainAmount
|
||||
|
||||
class SendConfirmationNotificationsTransformerV2Test {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
|
||||
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
|
||||
private val analyticsCategoryName = "test_category"
|
||||
|
||||
@Test
|
||||
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM: FeeSelectorUM = mockk(relaxed = true)
|
||||
val amountUM: AmountState = mockk(relaxed = true)
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState: ConfirmUM = ConfirmUM.Empty
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(initialState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee selector not content WHEN transform THEN returns original state`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
val amountUM: AmountState = mockk(relaxed = true)
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(initialState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM = createNormalFeeSelectorUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).isEmpty()
|
||||
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM = createFeeTooHighUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(1)
|
||||
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM = createFeeTooLowUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(1)
|
||||
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
|
||||
verify { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest {
|
||||
// GIVEN
|
||||
val feeSelectorUM = createFeeTooHighAndTooLowUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
val transformer = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
val initialState = createTestConfirmUM()
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
val content = result as ConfirmUM.Content
|
||||
assertThat(content.notifications).hasSize(2)
|
||||
assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
|
||||
assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
|
||||
}
|
||||
|
||||
private fun createTestConfirmUM(): ConfirmUM.Content {
|
||||
return ConfirmUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
walletName = mockk(relaxed = true),
|
||||
isSending = false,
|
||||
showTapHelp = false,
|
||||
sendingFooter = mockk(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestAmountUM(): AmountState.Data {
|
||||
val cryptoAmount = DomainAmount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("1.5"),
|
||||
decimals = 8,
|
||||
)
|
||||
val fiatAmount = DomainAmount(
|
||||
currencySymbol = "USD",
|
||||
value = BigDecimal("50.00"),
|
||||
decimals = 2,
|
||||
)
|
||||
|
||||
return AmountState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
isRedesignEnabled = false,
|
||||
title = mockk(relaxed = true),
|
||||
availableBalance = mockk(relaxed = true),
|
||||
tokenName = mockk(relaxed = true),
|
||||
tokenIconState = mockk(relaxed = true),
|
||||
segmentedButtonConfig = persistentListOf(),
|
||||
selectedButton = 0,
|
||||
isSegmentedButtonsEnabled = false,
|
||||
amountTextField = AmountFieldModel(
|
||||
value = "1.5",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
cryptoAmount = cryptoAmount,
|
||||
fiatAmount = fiatAmount,
|
||||
isFiatValue = false,
|
||||
fiatValue = "50.00",
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = mockk(relaxed = true),
|
||||
),
|
||||
appCurrency = appCurrency,
|
||||
isEditingDisabled = false,
|
||||
reduceAmountBy = BigDecimal.ZERO,
|
||||
isIgnoreReduce = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(FeeItem.Market(fee)),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighUM(): FeeSelectorUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooLowUM(): FeeSelectorUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.0001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = fee,
|
||||
priority = fee,
|
||||
)
|
||||
return FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighAndTooLowUM(): FeeSelectorUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.008"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "SOL",
|
||||
value = BigDecimal("0.008"),
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "SOL",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setUpLocale() {
|
||||
Locale.setDefault(Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,870 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.v2.api.entity.*
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.Locale
|
||||
import com.tangem.domain.tokens.model.Amount as DomainAmount
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2
|
||||
|
||||
class TransformersComparisonTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
|
||||
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
|
||||
private val analyticsCategoryName = "test_category"
|
||||
|
||||
@Test
|
||||
fun `GIVEN equivalent input data WHEN both transformers transform THEN they produce equal ConfirmUM`() = runTest {
|
||||
// GIVEN
|
||||
val initialConfirmUM = createTestConfirmUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
|
||||
val feeUM = createTestFeeUM()
|
||||
val feeSelectorUMV2 = createTestFeeSelectorUMV2()
|
||||
|
||||
// WHEN
|
||||
val transformerV1 = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val transformerV2 = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUMV2,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val resultV1 = transformerV1.transform(initialConfirmUM)
|
||||
val resultV2 = transformerV2.transform(initialConfirmUM)
|
||||
|
||||
// THEN
|
||||
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
|
||||
val contentV1 = resultV1 as ConfirmUM.Content
|
||||
val contentV2 = resultV2 as ConfirmUM.Content
|
||||
|
||||
assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size)
|
||||
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
|
||||
assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled)
|
||||
assertThat(contentV1.isSending).isEqualTo(contentV2.isSending)
|
||||
assertThat(contentV1.showTapHelp).isEqualTo(contentV2.showTapHelp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too low WHEN both transformers transform THEN they produce equal notifications`() = runTest {
|
||||
// GIVEN
|
||||
val initialConfirmUM = createTestConfirmUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
|
||||
val feeUM = createFeeTooLowUM()
|
||||
val feeSelectorUMV2 = createFeeTooLowUMV2()
|
||||
|
||||
// WHEN
|
||||
val transformerV1 = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val transformerV2 = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUMV2,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val resultV1 = transformerV1.transform(initialConfirmUM)
|
||||
val resultV2 = transformerV2.transform(initialConfirmUM)
|
||||
|
||||
// THEN
|
||||
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
|
||||
val contentV1 = resultV1 as ConfirmUM.Content
|
||||
val contentV2 = resultV2 as ConfirmUM.Content
|
||||
|
||||
assertThat(contentV1.notifications).hasSize(1)
|
||||
assertThat(contentV2.notifications).hasSize(1)
|
||||
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
|
||||
assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
|
||||
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
|
||||
|
||||
// Verify analytics event was sent for both transformers
|
||||
verify(exactly = 2) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee too high WHEN both transformers transform THEN they produce equal notifications`() = runTest {
|
||||
// GIVEN
|
||||
val initialConfirmUM = createTestConfirmUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
|
||||
val feeUM = createFeeTooHighUM()
|
||||
val feeSelectorUMV2 = createFeeTooHighUMV2()
|
||||
|
||||
// WHEN
|
||||
val transformerV1 = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val transformerV2 = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUMV2,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val resultV1 = transformerV1.transform(initialConfirmUM)
|
||||
val resultV2 = transformerV2.transform(initialConfirmUM)
|
||||
|
||||
// THEN
|
||||
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
|
||||
val contentV1 = resultV1 as ConfirmUM.Content
|
||||
val contentV2 = resultV2 as ConfirmUM.Content
|
||||
|
||||
assertThat(contentV1.notifications).hasSize(1)
|
||||
assertThat(contentV2.notifications).hasSize(1)
|
||||
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
|
||||
assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
|
||||
|
||||
val tooHighV1 = contentV1.notifications.first() as NotificationUM.Warning.TooHigh
|
||||
val tooHighV2 = contentV2.notifications.first() as NotificationUM.Warning.TooHigh
|
||||
assertThat(tooHighV1.value).isEqualTo(tooHighV2.value)
|
||||
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee both too high and too low WHEN both transformers transform THEN they produce equal notifications`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val initialConfirmUM = createTestConfirmUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
|
||||
val feeUM = createFeeTooHighAndTooLowUM()
|
||||
val feeSelectorUMV2 = createFeeTooHighAndTooLowUMV2()
|
||||
|
||||
// WHEN
|
||||
val transformerV1 = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val transformerV2 = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUMV2,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val resultV1 = transformerV1.transform(initialConfirmUM)
|
||||
val resultV2 = transformerV2.transform(initialConfirmUM)
|
||||
|
||||
// THEN
|
||||
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
|
||||
val contentV1 = resultV1 as ConfirmUM.Content
|
||||
val contentV2 = resultV2 as ConfirmUM.Content
|
||||
|
||||
assertThat(contentV1.notifications).hasSize(2)
|
||||
assertThat(contentV2.notifications).hasSize(2)
|
||||
|
||||
assertThat(contentV1.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
|
||||
assertThat(contentV1.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
|
||||
assertThat(contentV2.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
|
||||
assertThat(contentV2.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
|
||||
|
||||
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
|
||||
|
||||
verify(exactly = 2) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN normal fee WHEN both transformers transform THEN they produce equal notifications`() = runTest {
|
||||
// GIVEN
|
||||
val initialConfirmUM = createTestConfirmUM()
|
||||
val amountUM = createTestAmountUM()
|
||||
|
||||
val feeUM = createNormalFeeUM()
|
||||
val feeSelectorUMV2 = createNormalFeeSelectorUMV2()
|
||||
|
||||
// WHEN
|
||||
val transformerV1 = SendConfirmationNotificationsTransformer(
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val transformerV2 = SendConfirmationNotificationsTransformerV2(
|
||||
feeSelectorUM = feeSelectorUMV2,
|
||||
amountUM = amountUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
)
|
||||
|
||||
val resultV1 = transformerV1.transform(initialConfirmUM)
|
||||
val resultV2 = transformerV2.transform(initialConfirmUM)
|
||||
|
||||
// THEN
|
||||
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
|
||||
|
||||
val contentV1 = resultV1 as ConfirmUM.Content
|
||||
val contentV2 = resultV2 as ConfirmUM.Content
|
||||
|
||||
assertThat(contentV1.notifications).isEmpty()
|
||||
assertThat(contentV2.notifications).isEmpty()
|
||||
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
|
||||
}
|
||||
|
||||
private fun createTestConfirmUM(): ConfirmUM.Content {
|
||||
return ConfirmUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
walletName = mockk(relaxed = true),
|
||||
isSending = false,
|
||||
showTapHelp = false,
|
||||
sendingFooter = mockk(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestAmountUM(): AmountState.Data {
|
||||
val cryptoAmount = DomainAmount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("1.5"),
|
||||
decimals = 8,
|
||||
)
|
||||
val fiatAmount = DomainAmount(
|
||||
currencySymbol = "USD",
|
||||
value = BigDecimal("50.00"),
|
||||
decimals = 2,
|
||||
)
|
||||
|
||||
return AmountState.Data(
|
||||
isPrimaryButtonEnabled = true,
|
||||
isRedesignEnabled = false,
|
||||
title = mockk(relaxed = true),
|
||||
availableBalance = mockk(relaxed = true),
|
||||
tokenName = mockk(relaxed = true),
|
||||
tokenIconState = mockk(relaxed = true),
|
||||
segmentedButtonConfig = persistentListOf(),
|
||||
selectedButton = 0,
|
||||
isSegmentedButtonsEnabled = false,
|
||||
amountTextField = AmountFieldModel(
|
||||
value = "1.5",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
cryptoAmount = cryptoAmount,
|
||||
fiatAmount = fiatAmount,
|
||||
isFiatValue = false,
|
||||
fiatValue = "50.00",
|
||||
isFiatUnavailable = false,
|
||||
isValuePasted = false,
|
||||
onValuePastedTriggerDismiss = {},
|
||||
isError = false,
|
||||
isWarning = false,
|
||||
error = mockk(relaxed = true),
|
||||
),
|
||||
appCurrency = appCurrency,
|
||||
isEditingDisabled = false,
|
||||
reduceAmountBy = BigDecimal.ZERO,
|
||||
isIgnoreReduce = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestFeeUM(): FeeUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Market,
|
||||
selectedFee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestFeeSelectorUMV2(): FeeSelectorUMV2.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeSelectorUMV2.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Market(fee),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooLowUM(): FeeUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.0001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = fee,
|
||||
priority = fee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = true,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooLowUMV2(): FeeSelectorUMV2.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.0001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = fee,
|
||||
priority = fee,
|
||||
)
|
||||
return FeeSelectorUMV2.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.0001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighUM(): FeeUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = true,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighUMV2(): FeeSelectorUMV2.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeSelectorUMV2.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.01",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighAndTooLowUM(): FeeUM.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.008"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Custom,
|
||||
selectedFee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = true,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFeeTooHighAndTooLowUMV2(): FeeSelectorUMV2.Content {
|
||||
val priorityFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val minimumFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.01"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val customFee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.008"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Choosable(
|
||||
minimum = minimumFee,
|
||||
normal = minimumFee,
|
||||
priority = priorityFee,
|
||||
)
|
||||
return FeeSelectorUMV2.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Custom(
|
||||
fee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Custom(
|
||||
fee = customFee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.008",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNormalFeeUM(): FeeUM.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeUM.Content(
|
||||
feeSelectorUM = FeeSelectorUM.Content(
|
||||
fees = transactionFee,
|
||||
selectedType = FeeType.Market,
|
||||
selectedFee = fee,
|
||||
customValues = persistentListOf(
|
||||
CustomFeeFieldUM(
|
||||
value = "0.001",
|
||||
onValueChange = {},
|
||||
keyboardOptions = mockk(relaxed = true),
|
||||
keyboardActions = mockk(relaxed = true),
|
||||
symbol = "TST",
|
||||
decimals = 8,
|
||||
title = mockk(relaxed = true),
|
||||
footer = mockk(relaxed = true),
|
||||
),
|
||||
),
|
||||
nonce = BigInteger.ZERO,
|
||||
),
|
||||
rate = BigDecimal("50000"),
|
||||
isFeeConvertibleToFiat = true,
|
||||
isFeeApproximate = false,
|
||||
isTronToken = false,
|
||||
isEditingDisabled = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isCustomSelected = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNormalFeeSelectorUMV2(): FeeSelectorUMV2.Content {
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "TST",
|
||||
value = BigDecimal("0.001"),
|
||||
decimals = 8,
|
||||
),
|
||||
)
|
||||
val transactionFee = TransactionFee.Single(fee)
|
||||
return FeeSelectorUMV2.Content(
|
||||
fees = transactionFee,
|
||||
feeItems = persistentListOf(
|
||||
FeeItem.Market(fee),
|
||||
),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
feeNonce = FeeNonce.Nonce(
|
||||
nonce = BigInteger.ZERO,
|
||||
onNonceChange = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@BeforeAll
|
||||
fun setUpLocale() {
|
||||
Locale.setDefault(Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,8 +47,6 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.tokens)
|
||||
|
|
@ -65,6 +63,7 @@ dependencies {
|
|||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.notifications.models)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.deeplink
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetYieldUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.staking.*
|
||||
import com.tangem.domain.staking.analytics.StakeScreenSource
|
||||
|
|
@ -37,6 +36,7 @@ import com.tangem.domain.staking.model.StakingApproval
|
|||
import com.tangem.domain.staking.model.stakekit.*
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.utils.getValidatorsCount
|
||||
import com.tangem.domain.tokens.*
|
||||
|
|
@ -120,6 +120,7 @@ internal class StakingModel @Inject constructor(
|
|||
private val getActionsUseCase: GetActionsUseCase,
|
||||
private val getYieldUseCase: GetYieldUseCase,
|
||||
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
|
||||
private val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase,
|
||||
private val paramsInterceptorHolder: ParamsInterceptorHolder,
|
||||
private val shareManager: ShareManager,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
|
|
@ -527,9 +528,20 @@ internal class StakingModel @Inject constructor(
|
|||
val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints
|
||||
|
||||
if (rewardBlockType == RewardBlockType.RewardsRequirementsError) {
|
||||
val minimumAmount = rewardPendingActionConstraints?.amountArg?.minimum
|
||||
// Temporary fix, until StakeKit adds minimum requirement amount to balance response
|
||||
val minimumAmountValue = if (minimumAmount == null && yieldBalance.integrationId != null) {
|
||||
getActionRequirementAmountUseCase.invoke(
|
||||
integrationId = yieldBalance.integrationId,
|
||||
actionType = StakingActionType.CLAIM_REWARDS,
|
||||
).getOrNull()
|
||||
} else {
|
||||
minimumAmount
|
||||
}
|
||||
|
||||
stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert(
|
||||
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
|
||||
cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format {
|
||||
cryptoAmountValue = minimumAmountValue?.format {
|
||||
crypto(cryptoCurrencyStatus.currency)
|
||||
}.orEmpty(),
|
||||
)
|
||||
|
|
@ -931,11 +943,6 @@ internal class StakingModel @Inject constructor(
|
|||
isSingleWalletWithTokens = false,
|
||||
)
|
||||
.conflate()
|
||||
.filter {
|
||||
val sources = it.getOrNull()?.value?.sources ?: return@filter true
|
||||
|
||||
sources.networkSource == StatusSource.ACTUAL && sources.yieldBalanceSource == StatusSource.ACTUAL
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.filter { value.currentStep == StakingStep.InitialInfo }
|
||||
.onEach { maybeStatus ->
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import java.math.BigDecimal
|
|||
@Immutable
|
||||
internal sealed class InnerYieldBalanceState {
|
||||
data class Data(
|
||||
val integrationId: String?,
|
||||
val reward: YieldReward,
|
||||
val isActionable: Boolean,
|
||||
val balances: ImmutableList<BalanceState>,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal class YieldBalancesConverter(
|
|||
?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS }
|
||||
|
||||
InnerYieldBalanceState.Data(
|
||||
integrationId = yieldBalance?.integrationId,
|
||||
reward = YieldReward(
|
||||
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
|
||||
rewardsFiat = fiatRewardsValue.format {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
fetchStakingYieldBalanceUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
isRefactoringEnabled = true,
|
||||
)
|
||||
},
|
||||
// we should update tx history and network for new balances
|
||||
|
|
@ -64,7 +63,11 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
coroutineScope {
|
||||
listOf(
|
||||
async {
|
||||
fetchCurrencyStatus()
|
||||
/*
|
||||
* It is important to use NonCancellable here to ensure the update is not interrupted midway.
|
||||
* For example, this can happen if the user enters and immediately leaves the screen.
|
||||
*/
|
||||
withContext(NonCancellable) { fetchCurrencyStatus() }
|
||||
},
|
||||
async {
|
||||
updateStakingActions()
|
||||
|
|
@ -78,7 +81,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
|
|||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
id = cryptoCurrencyStatus.currency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ internal object InitialStakingStatePreview {
|
|||
|
||||
val stateWithYield = defaultState.copy(
|
||||
yieldBalance = InnerYieldBalanceState.Data(
|
||||
integrationId = null,
|
||||
reward = YieldReward(
|
||||
rewardsFiat = "100 $",
|
||||
rewardsCrypto = "100 SOL",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.utils
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.staking.model.stakekit.BalanceType
|
||||
import com.tangem.domain.staking.model.stakekit.PendingAction
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.features.staking.impl.presentation.state.BalanceState
|
||||
|
|
@ -44,16 +45,18 @@ internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boole
|
|||
return isSingleAction && !isRestake || isCompositePendingActions
|
||||
}
|
||||
|
||||
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isStubUnstakeAction(networkId)) {
|
||||
activeStake.pendingActions.plus(
|
||||
PendingAction(
|
||||
type = StakingActionType.UNSTAKE,
|
||||
passthrough = "",
|
||||
args = null,
|
||||
),
|
||||
).toPersistentList()
|
||||
} else {
|
||||
activeStake.pendingActions
|
||||
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList<PendingAction> {
|
||||
return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) {
|
||||
activeStake.pendingActions.plus(
|
||||
PendingAction(
|
||||
type = StakingActionType.UNSTAKE,
|
||||
passthrough = "",
|
||||
args = null,
|
||||
),
|
||||
).toPersistentList()
|
||||
} else {
|
||||
activeStake.pendingActions
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
|
@ -54,6 +57,9 @@ dependencies {
|
|||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.settings)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ internal sealed class SwapAmountUM {
|
|||
abstract val primaryAmount: SwapAmountFieldUM
|
||||
abstract val secondaryAmount: SwapAmountFieldUM
|
||||
abstract val selectedAmountType: SwapAmountType
|
||||
abstract val swapDirection: SwapDirection
|
||||
|
||||
data object Empty : SwapAmountUM() {
|
||||
data class Empty(
|
||||
override val swapDirection: SwapDirection,
|
||||
) : SwapAmountUM() {
|
||||
override val isPrimaryButtonEnabled = false
|
||||
override val selectedAmountType = SwapAmountType.From
|
||||
override val primaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.From)
|
||||
|
|
@ -29,16 +32,16 @@ internal sealed class SwapAmountUM {
|
|||
|
||||
data class Content(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
override val swapDirection: SwapDirection,
|
||||
override val selectedAmountType: SwapAmountType,
|
||||
|
||||
// two amount fields
|
||||
override val primaryAmount: SwapAmountFieldUM,
|
||||
override val secondaryAmount: SwapAmountFieldUM,
|
||||
override val selectedAmountType: SwapAmountType,
|
||||
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
|
||||
// selected swap route
|
||||
val swapDirection: SwapDirection,
|
||||
val swapRateType: ExpressRateType,
|
||||
|
||||
// swap models
|
||||
|
|
|
|||
|
|
@ -13,13 +13,9 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.swap.models.SwapQuoteModel
|
||||
|
|
@ -38,17 +34,17 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener
|
|||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadyStateConverter
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapQuoteUMConverter
|
||||
import com.tangem.features.swap.v2.impl.amount.model.transformers.*
|
||||
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
import com.tangem.utils.transformer.update as transformerUpdate
|
||||
|
|
@ -77,18 +73,11 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private var userWallet = params.userWallet
|
||||
|
||||
private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency
|
||||
private var secondaryCryptoCurrency: CryptoCurrency? = params.secondaryCryptoCurrency
|
||||
|
||||
private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
|
||||
private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = primaryCryptoCurrencyStatus.currency,
|
||||
value = CryptoCurrencyStatus.Loading,
|
||||
)
|
||||
|
||||
private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null
|
||||
private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
|
||||
|
||||
|
|
@ -103,7 +92,6 @@ internal class SwapAmountModel @Inject constructor(
|
|||
subscribeOnCryptoCurrencyStatusFlow()
|
||||
subscribeOnAmountUpdateTriggerUpdates()
|
||||
observeChooseSelectToken()
|
||||
// todo observe balance hiding flow
|
||||
}
|
||||
|
||||
fun updateState(amountUM: SwapAmountUM) {
|
||||
|
|
@ -132,11 +120,10 @@ internal class SwapAmountModel @Inject constructor(
|
|||
override fun onInfoClick() {
|
||||
val amountUM = uiState.value as? SwapAmountUM.Content ?: return
|
||||
val selectedProvider = amountUM.selectedQuote.provider ?: return
|
||||
val cryptoCurrency = secondaryCryptoCurrency ?: return
|
||||
|
||||
swapAlertFactory.priceImpactAlert(
|
||||
hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null,
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
currencySymbol = amountUM.primaryCryptoCurrencyStatus.currency.symbol,
|
||||
provider = selectedProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -193,32 +180,58 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onSeparatorClick() {
|
||||
// todo send with swap make swap types
|
||||
// todo handle reverse pair click with swap redesign
|
||||
val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data
|
||||
val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return
|
||||
|
||||
val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
|
||||
if (primaryCryptoCurrencyStatus != null) {
|
||||
uiState.transformerUpdate(
|
||||
SwapAmountPrimaryReadyStateTransformer(
|
||||
userWallet = userWallet,
|
||||
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
swapDirection = swapDirection,
|
||||
clickIntents = this,
|
||||
isBalanceHidden = params.isBalanceHidingFlow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty())
|
||||
callback.onAmountResult(uiState.value)
|
||||
}
|
||||
|
||||
private fun subscribeOnCryptoCurrencyStatusFlow() {
|
||||
params.primaryCryptoCurrencyStatusFlow.onEach { primaryCurrencyStatus ->
|
||||
primaryCryptoCurrencyStatus = primaryCurrencyStatus
|
||||
|
||||
val state = uiState.value
|
||||
if (state is SwapAmountUM.Content) {
|
||||
secondaryCryptoCurrency = state.primaryCryptoCurrencyStatus.currency
|
||||
secondaryCryptoCurrencyStatus = state.primaryCryptoCurrencyStatus
|
||||
params.primaryCryptoCurrencyStatusFlow
|
||||
.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
|
||||
.onEach { primaryCurrencyStatus ->
|
||||
val secondaryStatus = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus
|
||||
initCurrencies(
|
||||
primaryStatus = state.primaryCryptoCurrencyStatus,
|
||||
secondaryStatus = state.secondaryCryptoCurrencyStatus,
|
||||
primaryStatus = primaryCurrencyStatus,
|
||||
secondaryStatus = secondaryStatus,
|
||||
)
|
||||
} else {
|
||||
initPairs(
|
||||
primaryCryptoCurrency = primaryCryptoCurrency,
|
||||
secondaryCryptoCurrency = secondaryCryptoCurrency,
|
||||
)
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
if (secondaryStatus != null) {
|
||||
uiState.transformerUpdate(
|
||||
SwapAmountUpdateBalanceTransformer(
|
||||
cryptoCurrencyStatus = primaryCurrencyStatus,
|
||||
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
|
||||
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
uiState.transformerUpdate(
|
||||
SwapAmountPrimaryReadyStateTransformer(
|
||||
userWallet = userWallet,
|
||||
primaryCryptoCurrencyStatus = primaryCurrencyStatus,
|
||||
appCurrency = appCurrency,
|
||||
swapDirection = swapDirection,
|
||||
clickIntents = this,
|
||||
isBalanceHidden = params.isBalanceHidingFlow.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAmountUpdateTriggerUpdates() {
|
||||
|
|
@ -226,6 +239,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
.onEach {
|
||||
if (uiState.value is SwapAmountUM.Content) {
|
||||
onAmountValueChange(it)
|
||||
saveResult()
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -246,12 +260,12 @@ internal class SwapAmountModel @Inject constructor(
|
|||
amountUM
|
||||
}
|
||||
}
|
||||
initPairs(primaryCryptoCurrency, currency)
|
||||
initPairs(currency)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) {
|
||||
private fun initPairs(secondaryCryptoCurrency: CryptoCurrency?) {
|
||||
modelScope.launch {
|
||||
val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase
|
||||
.invokeMultiWalletSync(userWallet.walletId)
|
||||
|
|
@ -277,22 +291,22 @@ internal class SwapAmountModel @Inject constructor(
|
|||
swapDirection = params.swapDirection,
|
||||
)
|
||||
|
||||
if (secondaryStatus != null) {
|
||||
initCurrencies(primaryCryptoCurrencyStatus, secondaryStatus)
|
||||
this@SwapAmountModel.secondaryCryptoCurrency = secondaryCryptoCurrency
|
||||
secondaryCryptoCurrencyStatus = secondaryStatus
|
||||
uiState.update {
|
||||
SwapAmountReadyStateConverter(
|
||||
swapCurrencies = swapCurrencies,
|
||||
val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
|
||||
if (secondaryStatus != null && primaryStatus != null) {
|
||||
initCurrencies(primaryStatus, secondaryStatus)
|
||||
uiState.transformerUpdate(
|
||||
SwapAmountSecondaryReadyStateTransformer(
|
||||
userWallet = userWallet,
|
||||
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
|
||||
swapCurrencies = swapCurrencies,
|
||||
primaryCryptoCurrencyStatus = primaryStatus,
|
||||
secondaryCryptoCurrencyStatus = secondaryStatus,
|
||||
appCurrency = appCurrency,
|
||||
swapDirection = swapDirection,
|
||||
clickIntents = this@SwapAmountModel,
|
||||
isBalanceHidden = params.isBalanceHidingFlow.value,
|
||||
).convert(Unit)
|
||||
}
|
||||
),
|
||||
)
|
||||
loadQuotes()
|
||||
} else {
|
||||
// todo not available currency to swap
|
||||
}
|
||||
|
|
@ -304,7 +318,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) {
|
||||
private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus?) {
|
||||
primaryMinimumAmountBoundary = EnterAmountBoundary(
|
||||
amount = getMinimumTransactionAmountSyncUseCase
|
||||
.invoke(
|
||||
|
|
@ -314,21 +328,25 @@ internal class SwapAmountModel @Inject constructor(
|
|||
fiatRate = primaryStatus.value.fiatRate,
|
||||
fiatAmount = primaryStatus.value.fiatAmount,
|
||||
)
|
||||
secondaryMinimumAmountBoundary = EnterAmountBoundary(
|
||||
amount = getMinimumTransactionAmountSyncUseCase
|
||||
.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrencyStatus = secondaryStatus,
|
||||
).getOrNull().orZero(),
|
||||
fiatRate = secondaryStatus.value.fiatRate,
|
||||
fiatAmount = secondaryStatus.value.fiatAmount,
|
||||
)
|
||||
primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus)
|
||||
secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus)
|
||||
|
||||
if (secondaryStatus != null) {
|
||||
secondaryMinimumAmountBoundary = EnterAmountBoundary(
|
||||
amount = getMinimumTransactionAmountSyncUseCase
|
||||
.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrencyStatus = secondaryStatus,
|
||||
).getOrNull().orZero(),
|
||||
fiatRate = secondaryStatus.value.fiatRate,
|
||||
fiatAmount = secondaryStatus.value.fiatAmount,
|
||||
)
|
||||
secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadQuotes() {
|
||||
val state = uiState.value as? SwapAmountUM.Content ?: return
|
||||
if (state.secondaryCryptoCurrencyStatus == null) return
|
||||
|
||||
val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) {
|
||||
SwapDirection.Direct -> {
|
||||
|
|
@ -376,11 +394,17 @@ internal class SwapAmountModel @Inject constructor(
|
|||
).takeIf { error is ExpressError.AmountError }
|
||||
},
|
||||
ifRight = { quote: SwapQuoteModel ->
|
||||
convertToSwapProviderUM(
|
||||
quote = quote,
|
||||
provider = provider,
|
||||
differencePercent = DifferencePercent.Empty,
|
||||
SwapQuoteUMConverter(
|
||||
primaryCurrency = fromCryptoCurrency,
|
||||
secondaryCurrency = toCryptoCurrency,
|
||||
swapDirection = swapDirection,
|
||||
allowanceContract = quote.allowanceContract,
|
||||
isApprovalNeeded = checkAllowance(state, quote),
|
||||
).convert(
|
||||
SwapQuoteUMConverter.Data(
|
||||
quote = quote,
|
||||
provider = provider,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -397,61 +421,19 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun convertToSwapProviderUM(
|
||||
quote: SwapQuoteModel,
|
||||
provider: ExpressProvider,
|
||||
differencePercent: DifferencePercent,
|
||||
swapDirection: SwapDirection,
|
||||
): SwapQuoteUM {
|
||||
// todo swap allowance
|
||||
private suspend fun checkAllowance(state: SwapAmountUM.Content, quote: SwapQuoteModel): Boolean {
|
||||
val allowanceContract = quote.allowanceContract
|
||||
return if (allowanceContract != null) {
|
||||
val allowance = getAllowanceUseCase(
|
||||
val allowance = if (allowanceContract != null) {
|
||||
getAllowanceUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = primaryCryptoCurrencyStatus.currency,
|
||||
cryptoCurrency = state.primaryCryptoCurrencyStatus.currency,
|
||||
spenderAddress = allowanceContract,
|
||||
).getOrNull().orZero()
|
||||
val isApprovalNeeded = allowance < primaryCryptoCurrencyStatus.value.amount.orZero()
|
||||
|
||||
if (isApprovalNeeded) {
|
||||
SwapQuoteUM.Allowance(
|
||||
provider = provider,
|
||||
allowanceContract = allowanceContract,
|
||||
)
|
||||
} else {
|
||||
SwapQuoteUM.Content(
|
||||
provider = provider,
|
||||
quoteAmount = quote.toTokenAmount,
|
||||
diffPercent = differencePercent,
|
||||
quoteAmountValue = stringReference(
|
||||
quote.toTokenAmount.format {
|
||||
crypto(
|
||||
when (swapDirection) {
|
||||
SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency
|
||||
SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
).getOrNull()
|
||||
} else {
|
||||
SwapQuoteUM.Content(
|
||||
provider = provider,
|
||||
quoteAmount = quote.toTokenAmount,
|
||||
diffPercent = differencePercent,
|
||||
quoteAmountValue = stringReference(
|
||||
quote.toTokenAmount.format {
|
||||
crypto(
|
||||
when (swapDirection) {
|
||||
SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency
|
||||
SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
|
||||
return allowance.orZero() < state.primaryCryptoCurrencyStatus.value.amount.orZero()
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
|
|
@ -475,20 +457,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = {
|
||||
// if (!route.isEditMode) {
|
||||
// todo analytics
|
||||
// analyticsEventHandler.send(
|
||||
// CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
// categoryName = params.analyticsCategoryName,
|
||||
// source = SendScreenSource.Address,
|
||||
// isFromSummary = false,
|
||||
// isValid = state.isPrimaryButtonEnabled,
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
params.callback.onBackClick()
|
||||
},
|
||||
backIconClick = params.callback::onBackClick,
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ internal object SwapAmountQuoteUtils {
|
|||
}
|
||||
|
||||
fun SwapAmountUM.updateAmount(
|
||||
onPrimaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM,
|
||||
onSecondaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM,
|
||||
onPrimaryAmount: SwapAmountFieldUM.Content.(CryptoCurrencyStatus) -> SwapAmountFieldUM,
|
||||
onSecondaryAmount: SwapAmountFieldUM.Content.(CryptoCurrencyStatus) -> SwapAmountFieldUM,
|
||||
): SwapAmountUM {
|
||||
if (this !is SwapAmountUM.Content) return this
|
||||
|
||||
|
|
@ -51,10 +51,11 @@ internal object SwapAmountQuoteUtils {
|
|||
selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct
|
||||
) {
|
||||
val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this
|
||||
copy(primaryAmount = amountFieldUM.onPrimaryAmount())
|
||||
copy(primaryAmount = amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus))
|
||||
} else {
|
||||
if (secondaryCryptoCurrencyStatus == null) return this
|
||||
val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this
|
||||
copy(secondaryAmount = amountFieldUM.onSecondaryAmount())
|
||||
copy(secondaryAmount = amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.model.converter
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.swap.models.SwapQuoteModel
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SwapQuoteUMConverter(
|
||||
private val swapDirection: SwapDirection,
|
||||
private val allowanceContract: String?,
|
||||
private val isApprovalNeeded: Boolean,
|
||||
private val primaryCurrency: CryptoCurrency,
|
||||
private val secondaryCurrency: CryptoCurrency,
|
||||
) : Converter<SwapQuoteUMConverter.Data, SwapQuoteUM> {
|
||||
override fun convert(value: Data): SwapQuoteUM {
|
||||
val (quote, provider) = value
|
||||
|
||||
return if (allowanceContract != null) {
|
||||
if (isApprovalNeeded) {
|
||||
SwapQuoteUM.Allowance(
|
||||
provider = provider,
|
||||
allowanceContract = allowanceContract,
|
||||
)
|
||||
} else {
|
||||
SwapQuoteUM.Content(
|
||||
provider = provider,
|
||||
quoteAmount = quote.toTokenAmount,
|
||||
diffPercent = DifferencePercent.Empty,
|
||||
quoteAmountValue = stringReference(
|
||||
quote.toTokenAmount.toQuoteValue(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SwapQuoteUM.Content(
|
||||
provider = provider,
|
||||
quoteAmount = quote.toTokenAmount,
|
||||
diffPercent = DifferencePercent.Empty,
|
||||
quoteAmountValue = stringReference(
|
||||
quote.toTokenAmount.toQuoteValue(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.toQuoteValue() = format {
|
||||
crypto(
|
||||
when (swapDirection) {
|
||||
SwapDirection.Direct -> secondaryCurrency
|
||||
SwapDirection.Reverse -> primaryCurrency
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val quote: SwapQuoteModel,
|
||||
val provider: ExpressProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,18 +11,18 @@ internal class SwapAmountChangeCurrencyTransformer(
|
|||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
if (prevState !is SwapAmountUM.Content) return prevState
|
||||
return prevState.updateAmount(
|
||||
onPrimaryAmount = {
|
||||
onPrimaryAmount = { primaryStatus ->
|
||||
copy(
|
||||
amountField = AmountCurrencyTransformer(
|
||||
cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = primaryStatus,
|
||||
value = isFiatSelected,
|
||||
).transform(prevState.primaryAmount.amountField),
|
||||
)
|
||||
},
|
||||
onSecondaryAmount = {
|
||||
onSecondaryAmount = { secondaryStatus ->
|
||||
copy(
|
||||
amountField = AmountCurrencyTransformer(
|
||||
cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = secondaryStatus,
|
||||
value = isFiatSelected,
|
||||
).transform(prevState.secondaryAmount.amountField),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.model.transformers
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.express.models.ExpressRateType
|
||||
import com.tangem.domain.swap.models.SwapCurrencies
|
||||
import com.tangem.domain.swap.models.SwapDirection
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapAmountPrimaryReadyStateTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: AmountScreenClickIntents,
|
||||
private val swapDirection: SwapDirection,
|
||||
private val isBalanceHidden: Boolean,
|
||||
) : Transformer<SwapAmountUM> {
|
||||
|
||||
private val amountFieldConverter = SwapAmountFieldConverter(
|
||||
swapDirection = swapDirection,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
userWallet = userWallet,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
return SwapAmountUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
primaryAmount = amountFieldConverter.convert(
|
||||
selectedType = SwapAmountType.From,
|
||||
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
),
|
||||
secondaryCryptoCurrencyStatus = null,
|
||||
secondaryAmount = SwapAmountFieldUM.Empty(
|
||||
SwapAmountType.To,
|
||||
),
|
||||
swapDirection = swapDirection,
|
||||
swapCurrencies = SwapCurrencies.EMPTY,
|
||||
selectedAmountType = prevState.selectedAmountType,
|
||||
swapRateType = ExpressRateType.Float,
|
||||
swapQuotes = persistentListOf(),
|
||||
selectedQuote = SwapQuoteUM.Empty,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.model.converter
|
||||
package com.tangem.features.swap.v2.impl.amount.model.transformers
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -9,21 +9,22 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapAmountReadyStateConverter(
|
||||
internal class SwapAmountSecondaryReadyStateTransformer(
|
||||
private val userWallet: UserWallet,
|
||||
private val swapCurrencies: SwapCurrencies,
|
||||
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val swapCurrencies: SwapCurrencies,
|
||||
private val clickIntents: AmountScreenClickIntents,
|
||||
private val swapDirection: SwapDirection,
|
||||
private val isBalanceHidden: Boolean,
|
||||
) : Converter<Unit, SwapAmountUM> {
|
||||
) : Transformer<SwapAmountUM> {
|
||||
|
||||
private val amountFieldConverter = SwapAmountFieldConverter(
|
||||
swapDirection = swapDirection,
|
||||
|
|
@ -33,26 +34,23 @@ internal class SwapAmountReadyStateConverter(
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
override fun convert(value: Unit): SwapAmountUM {
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
return SwapAmountUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
primaryAmount = prevState.primaryAmount,
|
||||
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
|
||||
primaryAmount = amountFieldConverter.convert(
|
||||
selectedType = SwapAmountType.From,
|
||||
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
|
||||
),
|
||||
secondaryAmount = amountFieldConverter.convert(
|
||||
selectedType = SwapAmountType.To,
|
||||
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
|
||||
),
|
||||
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
|
||||
swapCurrencies = swapCurrencies,
|
||||
selectedAmountType = prevState.selectedAmountType,
|
||||
swapDirection = swapDirection,
|
||||
swapRateType = ExpressRateType.Float,
|
||||
swapQuotes = persistentListOf(),
|
||||
selectedQuote = SwapQuoteUM.Empty,
|
||||
selectedAmountType = SwapAmountType.From,
|
||||
appCurrency = appCurrency,
|
||||
swapRateType = ExpressRateType.Float,
|
||||
isPrimaryButtonEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ import com.tangem.utils.transformer.Transformer
|
|||
|
||||
internal class SwapAmountSelectQuoteTransformer(
|
||||
private val quoteUM: SwapQuoteUM,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
|
||||
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
|
||||
private val secondaryMinimumAmountBoundary: EnterAmountBoundary?,
|
||||
) : Transformer<SwapAmountUM> {
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
if (prevState !is SwapAmountUM.Content) return prevState
|
||||
|
|
@ -46,7 +46,9 @@ internal class SwapAmountSelectQuoteTransformer(
|
|||
} else {
|
||||
prevState.primaryAmount
|
||||
},
|
||||
secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) {
|
||||
secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From &&
|
||||
prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null
|
||||
) {
|
||||
val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
|
||||
val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data)
|
||||
?.amountTextField?.cryptoAmount?.value.orZero()
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import java.math.BigDecimal
|
|||
|
||||
internal class SwapAmountSetQuotesTransformer(
|
||||
private val quotes: List<SwapQuoteUM>,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
|
||||
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
|
||||
private val secondaryMaximumAmountBoundary: EnterAmountBoundary?,
|
||||
private val secondaryMinimumAmountBoundary: EnterAmountBoundary?,
|
||||
) : Transformer<SwapAmountUM> {
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
if (prevState !is SwapAmountUM.Content) return prevState
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue