Updated on 2026-08-14
This commit is contained in:
commit
dbe235560b
428 changed files with 10165 additions and 3907 deletions
|
|
@ -25,7 +25,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags
|
||||
import com.tangem.core.ui.test.DetailsScreenTestTags
|
||||
import com.tangem.features.details.component.preview.PreviewDetailsComponent
|
||||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
|
|
@ -67,7 +67,7 @@ private fun Content(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.testTag(TestTags.DETAILS_SCREEN),
|
||||
modifier = modifier.testTag(DetailsScreenTestTags.SCREEN_CONTAINER),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
contentPadding = PaddingValues(
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -120,7 +120,7 @@ private fun Block(
|
|||
) {
|
||||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.DETAILS_SCREEN_ITEM)
|
||||
.testTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
|
||||
when (model) {
|
||||
is DetailsItemUM.Basic -> {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON
|
||||
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER
|
||||
import com.tangem.core.ui.test.DisclaimerScreenTestTags
|
||||
import com.tangem.core.ui.webview.applySafeSettings
|
||||
import com.tangem.features.disclaimer.impl.R
|
||||
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
|
||||
|
|
@ -58,7 +57,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
|
|||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.statusBarsPadding()
|
||||
.testTag(DISCLAIMER_SCREEN_CONTAINER),
|
||||
.testTag(DisclaimerScreenTestTags.SCREEN_CONTAINER),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -183,7 +182,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
|
|||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
),
|
||||
modifier = Modifier
|
||||
.testTag(DISCLAIMER_SCREEN_ACCEPT_BUTTON)
|
||||
.testTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,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 +40,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)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,29 @@
|
|||
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 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 +31,28 @@ 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 {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -7,24 +7,30 @@ import com.tangem.features.hotwallet.createmobilewallet.DefaultCreateMobileWalle
|
|||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
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 {
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal interface CreateMobileWalletModuleActivityBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
@ActivityScoped
|
||||
fun bindCreateMobileWalletComponentFactory(
|
||||
impl: DefaultCreateMobileWalletComponent.Factory,
|
||||
): CreateMobileWalletComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CreateMobileWalletModuleBinds {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet.entity
|
||||
|
||||
internal data class CreateMobileWalletUM(
|
||||
val createButtonLoading: Boolean,
|
||||
val onBackClick: () -> Unit,
|
||||
val onCreateClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -84,8 +84,8 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
text = "Create",
|
||||
showProgress = false,
|
||||
text = stringResourceSafe(R.string.common_create),
|
||||
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,39 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check
|
||||
|
||||
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.crypto.bip39.Mnemonic
|
||||
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
|
||||
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupCheckComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupCheckModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupCheckContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onCompleteClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val generatedWords: Mnemonic,
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.entity
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ManualBackupCheckUM(
|
||||
val onCompleteButtonClick: () -> Unit,
|
||||
val wordFields: ImmutableList<WordField>,
|
||||
val completeButtonEnabled: Boolean,
|
||||
val completeButtonProgress: Boolean,
|
||||
) {
|
||||
data class WordField(
|
||||
val index: Int,
|
||||
val word: TextFieldValue,
|
||||
val error: Boolean,
|
||||
val onChange: (TextFieldValue) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
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.manualbackup.check.ManualBackupCheckComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.String
|
||||
import kotlin.Suppress
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.map
|
||||
import kotlin.error
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class ManualBackupCheckModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ManualBackupCheckComponent.Params>()
|
||||
private val callbacks = params.callbacks
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupCheckUM>
|
||||
field = MutableStateFlow(getInitialUIState())
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getInitialUIState(): ManualBackupCheckUM {
|
||||
val wordFields = List(WORD_FIELD_INDICES.size) { index ->
|
||||
val shownIndex = WORD_FIELD_INDICES[index]
|
||||
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = shownIndex,
|
||||
word = TextFieldValue(""),
|
||||
error = false,
|
||||
onChange = { textFieldValue ->
|
||||
updateWordField(shownIndex, textFieldValue)
|
||||
},
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
return ManualBackupCheckUM(
|
||||
wordFields = wordFields,
|
||||
completeButtonEnabled = false,
|
||||
completeButtonProgress = false,
|
||||
onCompleteButtonClick = {
|
||||
val currentUIState = uiState.value
|
||||
if (currentUIState.completeButtonEnabled) {
|
||||
callbacks.onCompleteClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateWordField(shownIndex: Int, newText: TextFieldValue) {
|
||||
uiState.update { currentState ->
|
||||
val updatedFields = currentState.wordFields.map { wordField ->
|
||||
if (wordField.index == shownIndex) {
|
||||
val isCorrect = checkWordField(newText.text, shownIndex)
|
||||
wordField.copy(
|
||||
word = newText,
|
||||
error = !isCorrect,
|
||||
)
|
||||
} else {
|
||||
wordField
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
val allFieldsCorrect = updatedFields.all { field ->
|
||||
checkWordField(field.word.text, field.index)
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
wordFields = updatedFields,
|
||||
completeButtonEnabled = allFieldsCorrect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkWordField(word: String, shownIndex: Int): Boolean {
|
||||
val generatedWords = params.generatedWords
|
||||
val wordList = generatedWords.mnemonicComponents
|
||||
|
||||
return if (shownIndex <= wordList.size) {
|
||||
wordList[shownIndex - 1] == word
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val WORD_FIELD_INDICES = listOf(2, 7, 11)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.check.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.core.ui.components.OutlineTextField
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
|
||||
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.impl.R
|
||||
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun ManualBackupCheckContent(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_seed_user_validation_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_seed_user_validation_message),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 36.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Fields(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 30.dp, horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.imePadding()
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
enabled = state.completeButtonEnabled,
|
||||
showProgress = state.completeButtonProgress,
|
||||
onClick = state.onCompleteButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Fields(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
DisableContextMenu {
|
||||
state.wordFields.fastForEachIndexed { index, field ->
|
||||
OutlineTextField(
|
||||
value = field.word,
|
||||
onValueChange = field.onChange,
|
||||
label = field.index.toString(),
|
||||
isError = field.error,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { focusManager.clearFocus() },
|
||||
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCheckContent(
|
||||
state = ManualBackupCheckUM(
|
||||
onCompleteButtonClick = {},
|
||||
wordFields = persistentListOf(
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 2,
|
||||
word = TextFieldValue(text = "word"),
|
||||
onChange = {},
|
||||
error = false,
|
||||
),
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 7,
|
||||
word = TextFieldValue(text = "wor"),
|
||||
onChange = {},
|
||||
error = true,
|
||||
),
|
||||
ManualBackupCheckUM.WordField(
|
||||
index = 11,
|
||||
word = TextFieldValue(text = "word"),
|
||||
onChange = {},
|
||||
error = false,
|
||||
),
|
||||
),
|
||||
completeButtonEnabled = false,
|
||||
completeButtonProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed
|
||||
|
||||
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.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupCompletedComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupCompletedModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupCompletedContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed
|
||||
|
||||
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.manualbackup.completed.entity.ManualBackupCompletedUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ManualBackupCompletedModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupCompletedUM>
|
||||
field = MutableStateFlow(
|
||||
ManualBackupCompletedUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed.entity
|
||||
|
||||
internal data class ManualBackupCompletedUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.completed.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_success_blue_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 20.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 12.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupCompletedContent() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCompletedContent(
|
||||
state = ManualBackupCompletedUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase
|
||||
|
||||
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.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupPhraseComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupPhraseModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupPhraseContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.entity
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class ManualBackupPhraseUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
|
||||
) {
|
||||
data class MnemonicGridItem(
|
||||
val index: Int,
|
||||
val mnemonic: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.model
|
||||
|
||||
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.manualbackup.phrase.ManualBackupPhraseComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class ManualBackupPhraseModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ManualBackupPhraseComponent.Params>()
|
||||
private val callbacks = params.callbacks
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupPhraseUM>
|
||||
field = MutableStateFlow(getInitialUIState())
|
||||
|
||||
private fun getInitialUIState(): ManualBackupPhraseUM {
|
||||
return ManualBackupPhraseUM(
|
||||
onContinueClick = callbacks::onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.phrase.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
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.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding()
|
||||
.weight(1f),
|
||||
) {
|
||||
TitleBlock(
|
||||
state = state,
|
||||
modifier = Modifier.padding(top = 20.dp),
|
||||
)
|
||||
|
||||
SeedPhraseGridBlock(
|
||||
mnemonicGridItems = state.words,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, bottom = 32.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.backup_seed_responsibility),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
text = stringResourceSafe(id = R.string.common_continue),
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.size36)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.backup_seed_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_seed_description,
|
||||
state.words.size,
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeedPhraseGridBlock(
|
||||
mnemonicGridItems: ImmutableList<ManualBackupPhraseUM.MnemonicGridItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
VerticalGrid(
|
||||
modifier = modifier,
|
||||
items = mnemonicGridItems,
|
||||
) { item ->
|
||||
Row(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <T> VerticalGrid(
|
||||
items: ImmutableList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val columnLength = items.size / 2
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
repeat(2) { index ->
|
||||
Column {
|
||||
for (i in 0 until columnLength) {
|
||||
val item = items[index * columnLength + i]
|
||||
content(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
ManualBackupPhraseContent(
|
||||
state = ManualBackupPhraseUM(
|
||||
onContinueClick = {},
|
||||
words = List(12) {
|
||||
ManualBackupPhraseUM.MnemonicGridItem(
|
||||
index = it + 1,
|
||||
mnemonic = "word${it + 1}",
|
||||
)
|
||||
}.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start
|
||||
|
||||
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.features.hotwallet.manualbackup.start.ui.ManualBackupStartContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class ManualBackupStartComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: ManualBackupStartModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ManualBackupStartContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start
|
||||
|
||||
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.manualbackup.start.entity.ManualBackupStartUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ManualBackupStartModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<ManualBackupStartUM>
|
||||
field = MutableStateFlow(
|
||||
ManualBackupStartUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start.entity
|
||||
|
||||
internal data class ManualBackupStartUM(
|
||||
val seepPhraseLength: Int = 12,
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.features.hotwallet.manualbackup.start.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
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.manualbackup.start.entity.ManualBackupStartUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_info_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_info_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_save_title),
|
||||
description = stringResourceSafe(
|
||||
R.string.backup_info_save_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
iconRes = R.drawable.ic_lock_24,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_keep_title),
|
||||
description = stringResourceSafe(R.string.backup_info_keep_description),
|
||||
iconRes = R.drawable.ic_settings_24,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp),
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp),
|
||||
text = description,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupStartContent() {
|
||||
TangemThemePreview {
|
||||
ManualBackupStartContent(
|
||||
state = ManualBackupStartUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.hotwallet.setupfinished
|
||||
|
||||
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.features.hotwallet.setupfinished.ui.MobileWalletSetupFinishedContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class MobileWalletSetupFinishedComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
private val model: MobileWalletSetupFinishedModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
MobileWalletSetupFinishedContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.hotwallet.setupfinished
|
||||
|
||||
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.setupfinished.entity.MobileWalletSetupFinishedUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class MobileWalletSetupFinishedModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
|
||||
field = MutableStateFlow(
|
||||
MobileWalletSetupFinishedUM(
|
||||
onContinueClick = params.callbacks::onContinueClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.hotwallet.setupfinished.entity
|
||||
|
||||
internal data class MobileWalletSetupFinishedUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.features.hotwallet.setupfinished.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.FullScreen
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM, modifier: Modifier = Modifier) {
|
||||
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
|
||||
val progress by animateLottieCompositionAsState(composition)
|
||||
var showConfetti by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_success_blue_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 20.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.onboarding_done_header),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 48.dp,
|
||||
top = 12.dp,
|
||||
end = 48.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_complete_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfetti) {
|
||||
FullScreen(notTouchable = true) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
showConfetti = true
|
||||
}
|
||||
|
||||
LaunchedEffect(progress == 1f) {
|
||||
if (progress == 1f) {
|
||||
showConfetti = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewMobileWalletSetupFinishedContent() {
|
||||
TangemThemePreview {
|
||||
MobileWalletSetupFinishedContent(
|
||||
state = MobileWalletSetupFinishedUM(
|
||||
onContinueClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,6 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -41,14 +40,10 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val onrampFeatureToggles: OnrampFeatureToggles,
|
||||
private val shareManager: ShareManager,
|
||||
) {
|
||||
|
||||
private val disabledActionsInDemoMode = buildSet {
|
||||
if (!onrampFeatureToggles.isFeatureEnabled) {
|
||||
add(TokenActionsBSContentUM.Action.Buy)
|
||||
}
|
||||
add(TokenActionsBSContentUM.Action.Sell)
|
||||
}
|
||||
|
||||
|
|
@ -132,12 +127,11 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
|
||||
reduxStateHolder.dispatch(
|
||||
TradeCryptoAction.Buy(
|
||||
userWallet = cryptoCurrencyData.userWallet,
|
||||
router.push(
|
||||
AppRoute.Onramp(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
currency = cryptoCurrencyData.status.currency,
|
||||
source = OnrampSource.MARKETS,
|
||||
cryptoCurrencyStatus = cryptoCurrencyData.status,
|
||||
appCurrencyCode = currentAppCurrency().code,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
|
|
@ -32,6 +34,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.extensions.isPositive
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -49,6 +52,8 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
private val rampStateManager: RampStateManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteTopUpComponent.Params>()
|
||||
|
|
@ -83,10 +88,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
|
|||
showBalanceLoadingProgress(true)
|
||||
createUserWalletIfNull()
|
||||
val userWalletId = requireNotNull(userWallet?.walletId)
|
||||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWalletId,
|
||||
refresh = true,
|
||||
)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true)
|
||||
}
|
||||
showBalanceLoadingProgress(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
|
|||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
@ -85,6 +87,8 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
|
|
@ -329,13 +333,15 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
|
||||
cardRepository.finishCardActivation(params.scanResponse.card.cardId)
|
||||
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
refresh = true,
|
||||
).onLeft {
|
||||
Timber.e("Unable to fetch currency status: $it")
|
||||
setLoading(false)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e("Unable to fetch currency status: $it")
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
|
||||
.firstOrNull()?.getOrNull()
|
||||
|
|
@ -431,10 +437,12 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
it.copy(isLoading = true)
|
||||
}
|
||||
modelScope.launch {
|
||||
fetchCurrencyStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
refresh = true,
|
||||
)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
interface OnrampFeatureToggles {
|
||||
|
||||
val isFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface BuyRedirectDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): BuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultOnrampFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : OnrampFeatureToggles {
|
||||
|
||||
override val isFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_ENABLED")
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.onramp
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object OnrampFeatureModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles {
|
||||
return DefaultOnrampFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultBuyRedirectDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
onrampFeatureToggles: OnrampFeatureToggles,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : BuyRedirectDeepLinkHandler {
|
||||
|
||||
init {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) {
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse {
|
||||
Timber.e("Error on getting cryptoCurrency: $it")
|
||||
return@launch
|
||||
}
|
||||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : BuyRedirectDeepLinkHandler.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultBuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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,20 +11,10 @@ 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
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyRedirectDeepLinkHandler(
|
||||
impl: DefaultBuyRedirectDeepLinkHandler.Factory,
|
||||
): BuyRedirectDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyDeepLinkHandler(impl: DefaultBuyDeepLinkHandler.Factory): BuyDeepLinkHandler.Factory
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.selecttoken.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -23,7 +24,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.wallets.models.requireColdWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selecttoken.OnrampOperationComponent.Params
|
||||
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
|
||||
|
|
@ -46,7 +46,6 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val onrampFeatureToggles: OnrampFeatureToggles,
|
||||
private val rampStateManager: RampStateManager,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -100,7 +99,7 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun selectTokenIfDemoModeOff(status: CryptoCurrencyStatus) {
|
||||
if (params is Params.Sell || !onrampFeatureToggles.isFeatureEnabled) {
|
||||
if (params is Params.Sell) {
|
||||
showErrorIfDemoModeOrElse { selectToken(status) }
|
||||
} else {
|
||||
selectToken(status)
|
||||
|
|
@ -109,14 +108,25 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
|
||||
private fun selectToken(status: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code
|
||||
when (params) {
|
||||
is Params.Buy -> {
|
||||
router.push(
|
||||
AppRoute.Onramp(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
currency = status.currency,
|
||||
source = OnrampSource.ACTION_BUTTONS,
|
||||
),
|
||||
)
|
||||
}
|
||||
is Params.Sell -> {
|
||||
val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync()
|
||||
.getOrElse { AppCurrency.Default }.code
|
||||
|
||||
reduxStateHolder.dispatch(
|
||||
action = when (params) {
|
||||
is Params.Buy -> getBuyAction(status, appCurrencyCode)
|
||||
is Params.Sell -> TradeCryptoAction.Sell(status, appCurrencyCode)
|
||||
},
|
||||
)
|
||||
reduxStateHolder.dispatch(
|
||||
action = TradeCryptoAction.Sell(status, appCurrencyCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,15 +154,6 @@ internal class OnrampOperationModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
private fun getBuyAction(status: CryptoCurrencyStatus, appCurrencyCode: String): TradeCryptoAction {
|
||||
return TradeCryptoAction.Buy(
|
||||
userWallet = selectedUserWallet,
|
||||
cryptoCurrencyStatus = status,
|
||||
source = OnrampSource.ACTION_BUTTONS,
|
||||
appCurrencyCode = appCurrencyCode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
|
||||
if (isDemoCardUseCase(cardId = selectedUserWallet.cardId)) {
|
||||
val alertUM = AlertDemoModeUM(onConfirmClick = {})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -65,8 +65,12 @@ internal fun QrScanningContent(
|
|||
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = null,
|
||||
startButton = TopAppBarButtonUM.Back(uiState.onBackClick),
|
||||
title = uiState.topBarConfig.title?.resolveReference(),
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = uiState.topBarConfig.startIcon,
|
||||
onIconClicked = uiState.onBackClick,
|
||||
),
|
||||
textColor = TangemTheme.colors.text.constantWhite,
|
||||
iconTint = TangemColorPalette.White,
|
||||
containerColor = Color.Transparent,
|
||||
endContent = {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.feature.qrscanning.presentation
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class QrScanningState(
|
||||
val topBarConfig: TopBarConfig,
|
||||
val message: TextReference?,
|
||||
val onQrScanned: (String) -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
|
|
@ -14,6 +16,8 @@ internal data class QrScanningState(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
)
|
||||
|
||||
internal data class TopBarConfig(val title: TextReference?, @DrawableRes val startIcon: Int)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface PasteAction {
|
||||
data object None : PasteAction
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal class QrScanningStateController @Inject constructor() {
|
|||
|
||||
private fun getInitialState(): QrScanningState {
|
||||
return QrScanningState(
|
||||
topBarConfig = TopBarConfig(title = null, startIcon = 0),
|
||||
message = null,
|
||||
onBackClick = {},
|
||||
onQrScanned = {},
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ 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
|
||||
import com.tangem.feature.qrscanning.presentation.PasteAction
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
|
||||
private const val WC_SCHEME = "wc"
|
||||
import com.tangem.feature.qrscanning.presentation.TopBarConfig
|
||||
|
||||
internal class InitializeQrScanningStateTransformer(
|
||||
private val clickIntents: QrScanningClickIntents,
|
||||
|
|
@ -26,24 +26,34 @@ internal class InitializeQrScanningStateTransformer(
|
|||
}
|
||||
|
||||
return QrScanningState(
|
||||
topBarConfig = constructTopBarConfig(),
|
||||
message = message,
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
onQrScanned = clickIntents::onQrScanned,
|
||||
onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) },
|
||||
onGalleryClick = clickIntents::onGalleryClicked,
|
||||
pasteAction = constructPasteAction(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun constructTopBarConfig(): TopBarConfig {
|
||||
return when (source) {
|
||||
SourceType.SEND -> TopBarConfig(
|
||||
title = resourceReference(R.string.common_send),
|
||||
startIcon = R.drawable.ic_back_24,
|
||||
)
|
||||
SourceType.WALLET_CONNECT -> TopBarConfig(
|
||||
title = resourceReference(R.string.wc_new_connection),
|
||||
startIcon = R.drawable.ic_close_24,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructPasteAction(): PasteAction {
|
||||
val uri = clipboardManager.getText()
|
||||
return if (source == SourceType.WALLET_CONNECT && uri != null && isWalletConnectUri(uri)) {
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri) }
|
||||
return if (uri != null) {
|
||||
PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) }
|
||||
} else {
|
||||
PasteAction.None
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWalletConnectUri(uri: String): Boolean {
|
||||
return uri.lowercase().startsWith(WC_SCHEME)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
|
|
@ -9,5 +9,11 @@ interface FeeSelectorBlockComponent : ComposableContentComponent {
|
|||
|
||||
fun updateState(feeSelectorUM: FeeSelectorUM)
|
||||
|
||||
interface Factory : ComponentFactory<FeeSelectorParams.FeeSelectorBlockParams, FeeSelectorBlockComponent>
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
): FeeSelectorBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
|
||||
interface FeeSelectorComponent : ComposableBottomSheetComponent {
|
||||
interface Factory : ComponentFactory<FeeSelectorParams.FeeSelectorDetailsParams, FeeSelectorComponent>
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
onDismiss: () -> Unit,
|
||||
): FeeSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,12 @@ interface SendComponent : ComposableContentComponent {
|
|||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
val callback: ModelCallback? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendComponent>
|
||||
|
||||
interface ModelCallback {
|
||||
fun onConvertToAnotherToken(lastAmount: String)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,5 @@ package com.tangem.features.send.v2.api
|
|||
interface SendFeatureToggles {
|
||||
|
||||
val isSendRedesignEnabled: Boolean
|
||||
val isSendWithSwapEnabled: Boolean
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.v2.api.entity
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
|
|
@ -17,13 +18,12 @@ sealed class FeeSelectorUM {
|
|||
data class Error(val error: GetFeeError) : FeeSelectorUM()
|
||||
|
||||
data class Content(
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
@ -33,11 +33,26 @@ 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
|
||||
|
||||
fun isSame(other: FeeItem): Boolean {
|
||||
fun isSameClass(other: FeeItem): Boolean {
|
||||
return this::class == other::class
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,27 +13,36 @@ sealed class FeeSelectorParams {
|
|||
abstract val state: FeeSelectorUM
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val callback: FeeSelectorModelCallback
|
||||
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val suggestedFeeState: SuggestedFeeState
|
||||
abstract val feeDisplaySource: FeeDisplaySource
|
||||
|
||||
data class FeeSelectorBlockParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val callback: FeeSelectorModelCallback,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
data class FeeSelectorDetailsParams(
|
||||
override val state: FeeSelectorUM,
|
||||
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val callback: FeeSelectorModelCallback,
|
||||
override val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
override val suggestedFeeState: SuggestedFeeState,
|
||||
override val feeDisplaySource: FeeDisplaySource,
|
||||
val callback: FeeSelectorModelCallback,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
sealed class SuggestedFeeState {
|
||||
data object None : SuggestedFeeState()
|
||||
data class Suggestion(val title: TextReference, val fee: Fee) : SuggestedFeeState()
|
||||
}
|
||||
|
||||
enum class FeeDisplaySource {
|
||||
Screen,
|
||||
BottomSheet,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
package com.tangem.features.send.v2.api.subcomponents.destination
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
|
||||
interface SendDestinationBlockComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory :
|
||||
ComponentFactory<SendDestinationComponentParams.DestinationBlockParams, SendDestinationBlockComponent>
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendDestinationComponentParams.DestinationBlockParams,
|
||||
onClick: () -> Unit,
|
||||
onResult: (DestinationUM) -> Unit,
|
||||
): SendDestinationBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
|
|||
|
||||
interface SendDestinationComponent : ComposableContentComponent {
|
||||
|
||||
fun updateState(destinationUM: DestinationUM)
|
||||
|
||||
interface ModelCallback : NavigationModelCallback {
|
||||
fun onDestinationResult(destinationUM: DestinationUM)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -8,4 +8,6 @@ internal class DefaultSendFeatureToggles(
|
|||
) : SendFeatureToggles {
|
||||
override val isSendRedesignEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
|
||||
override val isSendWithSwapEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED")
|
||||
}
|
||||
|
|
@ -18,6 +18,11 @@ internal sealed class CommonSendRoute : Route {
|
|||
override val isEditMode: Boolean = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ConfirmSuccess : CommonSendRoute() {
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Destination(
|
||||
override val isEditMode: Boolean,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.*
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
|
|
@ -15,6 +14,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
|
||||
|
||||
@Composable
|
||||
internal fun SendContent(
|
||||
|
|
@ -32,12 +33,20 @@ internal fun SendContent(
|
|||
SendAppBar(navigationUM = navigationUM)
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(slide()),
|
||||
animation = stackAnimation { child ->
|
||||
when (child.instance) {
|
||||
is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f)
|
||||
is SendConfirmComponent -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
it.instance.Content(Modifier.weight(1f))
|
||||
}
|
||||
SendNavigationButtons(navigationUM = navigationUM)
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
SendNavigationButtons(navigationUM = navigationUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +1,66 @@
|
|||
package com.tangem.features.send.v2.feeselector
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
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
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
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.FeeSelectorComponent
|
||||
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.FeeSelectorModel
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
@Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
private val feeSelectorComponentFactory: FeeSelectorComponent.Factory,
|
||||
) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.feeSelectorBottomSheet,
|
||||
serializer = Unit.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = { _, componentContext ->
|
||||
feeSelectorComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = FeeSelectorParams.FeeSelectorDetailsParams(
|
||||
state = model.uiState.value,
|
||||
onLoadFee = params.onLoadFee,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
),
|
||||
onDismiss = {
|
||||
model.feeSelectorBottomSheet.dismiss()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
init {
|
||||
model.uiState
|
||||
.onEach(params.callback::onFeeResult)
|
||||
.onEach(onResult)
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +71,18 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
FeeSelectorBlockContent(modifier = modifier, state = state)
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
FeeSelectorBlockContent(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) {
|
||||
Modifier.clickable {
|
||||
model.feeSelectorBottomSheet.activate(Unit)
|
||||
}
|
||||
},
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -75,121 +90,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorBlockParams,
|
||||
onResult: (feeSelectorUM: FeeSelectorUM) -> Unit,
|
||||
): DefaultFeeSelectorBlockComponent
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_fee_new_24),
|
||||
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()
|
||||
when (state) {
|
||||
is FeeSelectorUM.Content -> FeeContent(state)
|
||||
is FeeSelectorUM.Loading -> FeeLoading()
|
||||
is FeeSelectorUM.Error -> FeeError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeError() {
|
||||
Text(
|
||||
text = EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeLoading() {
|
||||
TextShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size90),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.FeeContent(state: FeeSelectorUM.Content) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@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() {
|
||||
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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,18 +16,24 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
@Assisted private val onDismiss: () -> Unit,
|
||||
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: FeeSelectorModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun dismiss() {
|
||||
model.dismiss()
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state, feeSelectorIntents = model)
|
||||
FeeSelectorModalBottomSheet(
|
||||
onDismiss = ::dismiss,
|
||||
state = state,
|
||||
feeSelectorIntents = model,
|
||||
feeDisplaySource = params.feeDisplaySource,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
@ -35,6 +41,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
|
|||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: FeeSelectorParams.FeeSelectorDetailsParams,
|
||||
onDismiss: () -> Unit,
|
||||
): DefaultFeeSelectorComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +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 onCustomFeeNextClick()
|
||||
fun onNonceChange(value: String)
|
||||
fun onDoneClick()
|
||||
}
|
||||
|
||||
internal class StubFeeSelectorIntents : FeeSelectorIntents {
|
||||
override fun onFeeItemSelected(feeItem: FeeItem) {}
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
||||
override fun onCustomFeeNextClick() {}
|
||||
override fun onNonceChange(value: String) {}
|
||||
override fun onDoneClick() {}
|
||||
}
|
||||
|
|
@ -2,20 +2,20 @@ package com.tangem.features.send.v2.feeselector.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
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.decompose.navigation.Router
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
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.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
|
||||
|
|
@ -29,13 +29,14 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model(), FeeSelectorIntents {
|
||||
) : Model(), FeeSelectorIntents, FeeSelectorModelCallback {
|
||||
|
||||
private val params = paramsContainer.require<FeeSelectorParams>()
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
||||
val feeSelectorBottomSheet = SlotNavigation<Unit>()
|
||||
|
||||
val uiState: StateFlow<FeeSelectorUM>
|
||||
field = MutableStateFlow<FeeSelectorUM>(params.state)
|
||||
|
||||
|
|
@ -48,10 +49,6 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
uiState.value = feeSelectorUM
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -89,15 +87,27 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
uiState.update(
|
||||
FeeSelectorCustomValueChangedTransformer(
|
||||
index = index,
|
||||
value = value,
|
||||
intents = this,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCustomFeeNextClick() {
|
||||
// TODO: [REDACTED_JIRA]
|
||||
override fun onNonceChange(value: String) {
|
||||
uiState.update(FeeSelectorNonceChangeTransformer(value = value))
|
||||
}
|
||||
|
||||
override fun onDoneClick() {
|
||||
params.callback.onFeeResult(uiState.value)
|
||||
dismiss()
|
||||
(params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value)
|
||||
}
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
uiState.value = feeSelectorUM
|
||||
feeSelectorBottomSheet.dismiss()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ 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.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -17,7 +17,7 @@ internal class FeeItemConverter(
|
|||
private val feeSelectorIntents: FeeSelectorIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : Converter<TransactionFee, ImmutableList<FeeItem>> {
|
||||
) : Converter<FeeItemConverter.Input, ImmutableList<FeeItem>> {
|
||||
|
||||
private val customFeeFieldConverter = FeeSelectorCustomFieldConverter(
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
|
|
@ -26,7 +26,7 @@ internal class FeeItemConverter(
|
|||
normalFee = normalFee,
|
||||
)
|
||||
|
||||
override fun convert(value: TransactionFee): ImmutableList<FeeItem> {
|
||||
override fun convert(value: Input): ImmutableList<FeeItem> {
|
||||
val fees = mutableListOf<FeeItem>()
|
||||
|
||||
when (suggestedFeeState) {
|
||||
|
|
@ -38,26 +38,33 @@ internal class FeeItemConverter(
|
|||
),
|
||||
)
|
||||
}
|
||||
when (value) {
|
||||
when (value.transactionFee) {
|
||||
is TransactionFee.Choosable -> {
|
||||
fees.add(FeeItem.Slow(fee = value.minimum))
|
||||
fees.add(FeeItem.Market(fee = value.normal))
|
||||
fees.add(FeeItem.Fast(fee = value.priority))
|
||||
fees.add(FeeItem.Slow(fee = value.transactionFee.minimum))
|
||||
fees.add(FeeItem.Market(fee = value.transactionFee.normal))
|
||||
fees.add(FeeItem.Fast(fee = value.transactionFee.priority))
|
||||
}
|
||||
is TransactionFee.Single -> {
|
||||
fees.add(FeeItem.Market(fee = value.normal))
|
||||
fees.add(FeeItem.Market(fee = value.transactionFee.normal))
|
||||
}
|
||||
}
|
||||
val customFeeFields = customFeeFieldConverter.convert(normalFee)
|
||||
if (customFeeFields.isNotEmpty()) {
|
||||
fees.add(
|
||||
FeeItem.Custom(
|
||||
fee = customFeeFieldConverter.convertBack(customFeeFields),
|
||||
customValues = customFeeFields,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val customFee = value.customFee ?: constructCustomFee()
|
||||
customFee?.let(fees::add)
|
||||
|
||||
return fees.toImmutableList()
|
||||
}
|
||||
|
||||
private fun constructCustomFee(): FeeItem.Custom? {
|
||||
val customFeeFields = customFeeFieldConverter.convert(normalFee)
|
||||
|
||||
if (customFeeFields.isEmpty()) return null
|
||||
|
||||
return FeeItem.Custom(
|
||||
fee = customFeeFieldConverter.convertBack(customFeeFields),
|
||||
customValues = customFeeFields,
|
||||
)
|
||||
}
|
||||
|
||||
data class Input(val transactionFee: TransactionFee, val customFee: FeeItem.Custom?)
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
|
||||
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -25,7 +25,7 @@ internal class FeeSelectorCustomFieldConverter(
|
|||
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EthereumCustomFeeConverter(
|
||||
onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange,
|
||||
onNextClick = feeSelectorIntents::onCustomFeeNextClick,
|
||||
onNextClick = null,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -34,7 +34,7 @@ internal class FeeSelectorCustomFieldConverter(
|
|||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange,
|
||||
onNextClick = feeSelectorIntents::onCustomFeeNextClick,
|
||||
onNextClick = null,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -77,38 +77,43 @@ internal class FeeSelectorCustomFieldConverter(
|
|||
}
|
||||
}
|
||||
|
||||
fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) =
|
||||
when (val fee = feeSelectorState.fees.normal) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
|
||||
feeValue = fee,
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
txSize = fee.txSize,
|
||||
)
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
else -> feeSelectorState.customValues
|
||||
}
|
||||
|
||||
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) {
|
||||
is TransactionFee.Choosable -> feeSelectorState.fees.minimum
|
||||
is TransactionFee.Single -> feeSelectorState.fees.normal
|
||||
}.let {
|
||||
when (it) {
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
|
||||
minimumFee = it,
|
||||
customValues = feeSelectorState.customValues,
|
||||
)
|
||||
else -> feeSelectorState.customValues
|
||||
}
|
||||
fun onValueChange(
|
||||
feeSelectorState: FeeSelectorUM.Content,
|
||||
customValues: ImmutableList<CustomFeeFieldUM>,
|
||||
index: Int,
|
||||
value: String,
|
||||
) = when (val fee = feeSelectorState.fees.normal) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
|
||||
feeValue = fee,
|
||||
customValues = customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
|
||||
customValues = customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
txSize = fee.txSize,
|
||||
)
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
|
||||
customValues = customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
else -> customValues
|
||||
}
|
||||
|
||||
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content, customValues: ImmutableList<CustomFeeFieldUM>) =
|
||||
when (val fees = feeSelectorState.fees) {
|
||||
is TransactionFee.Choosable -> fees.minimum
|
||||
is TransactionFee.Single -> fees.normal
|
||||
}.let {
|
||||
when (it) {
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
|
||||
minimumFee = it,
|
||||
customValues = customValues,
|
||||
)
|
||||
else -> customValues
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.send.v2.feeselector.model.transformers
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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.features.send.v2.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class FeeSelectorCustomValueChangedTransformer(
|
||||
private val index: Int,
|
||||
private val value: String,
|
||||
private val intents: FeeSelectorIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
) : Transformer<FeeSelectorUM> {
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
val state = prevState as? FeeSelectorUM.Content ?: return prevState
|
||||
val customFee = state.feeItems.filterIsInstance<FeeItem.Custom>().firstOrNull() ?: return prevState
|
||||
val customFeeConverter = FeeSelectorCustomFieldConverter(
|
||||
feeSelectorIntents = intents,
|
||||
appCurrency = appCurrency,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
normalFee = state.selectedFeeItem.fee,
|
||||
)
|
||||
val updatedCustomValues = customFeeConverter.onValueChange(state, customFee.customValues, index, value)
|
||||
val newCustomFee = customFee.copy(
|
||||
fee = customFeeConverter.convertBack(updatedCustomValues),
|
||||
customValues = updatedCustomValues,
|
||||
)
|
||||
return state.copy(
|
||||
feeItems = state.feeItems.map { if (it is FeeItem.Custom) newCustomFee else it }.toImmutableList(),
|
||||
selectedFeeItem = newCustomFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,32 +28,50 @@ internal class FeeSelectorLoadedTransformer(
|
|||
normalFee = fees.normal,
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(fees)
|
||||
val prevCustomFee = if (prevState is FeeSelectorUM.Content) {
|
||||
prevState.feeItems.find { it is FeeItem.Custom } as? FeeItem.Custom
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(FeeItemConverter.Input(fees, prevCustomFee))
|
||||
|
||||
val selectedFee = when (prevState) {
|
||||
is FeeSelectorUM.Content -> feeItems.first { it.isSame(prevState.selectedFeeItem) }
|
||||
is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) }
|
||||
is FeeSelectorUM.Error,
|
||||
FeeSelectorUM.Loading,
|
||||
-> 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package com.tangem.features.send.v2.feeselector.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.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.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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
|
||||
|
||||
@Composable
|
||||
internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_fee_new_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
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()
|
||||
is FeeSelectorUM.Error -> FeeError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeLoading() {
|
||||
TextShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
style = TangemTheme.typography.body1,
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size90),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) {
|
||||
val fiatRate = state.feeFiatRateUM
|
||||
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(@PreviewParameter(FeeSelectorUMProvider::class) state: FeeSelectorUM) {
|
||||
TangemThemePreview {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -4,10 +4,8 @@ import android.content.res.Configuration
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
|
@ -17,7 +15,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
|
|
@ -31,6 +28,7 @@ import androidx.compose.ui.util.fastForEachIndexed
|
|||
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.getFiatReference
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
|
|
@ -39,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
|
||||
|
|
@ -47,10 +46,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.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
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
|
|
@ -58,12 +55,12 @@ 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(
|
||||
state: FeeSelectorUM,
|
||||
feeSelectorIntents: FeeSelectorIntents,
|
||||
feeDisplaySource: FeeSelectorParams.FeeDisplaySource,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
if (state !is FeeSelectorUM.Content) return
|
||||
|
|
@ -76,17 +73,13 @@ internal fun FeeSelectorModalBottomSheet(
|
|||
),
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onDismiss,
|
||||
)
|
||||
FeeTitle(feeDisplaySource = feeDisplaySource, onDismiss = onDismiss)
|
||||
},
|
||||
content = {
|
||||
FeeSelectorItems(
|
||||
state = state,
|
||||
feeSelectorIntents = feeSelectorIntents,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp),
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp),
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
|
|
@ -101,6 +94,26 @@ internal fun FeeSelectorModalBottomSheet(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDismiss: () -> Unit) {
|
||||
when (feeDisplaySource) {
|
||||
FeeSelectorParams.FeeDisplaySource.Screen -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onDismiss,
|
||||
)
|
||||
}
|
||||
FeeSelectorParams.FeeDisplaySource.BottomSheet -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.common_network_fee_title),
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
private fun FeeSelectorItems(
|
||||
|
|
@ -111,7 +124,7 @@ private fun FeeSelectorItems(
|
|||
Column(modifier = modifier) {
|
||||
val feeFiatRateUM = state.feeFiatRateUM
|
||||
state.feeItems.fastForEachIndexed { index, item ->
|
||||
val isSelected = item.isSame(state.selectedFeeItem)
|
||||
val isSelected = item.isSameClass(state.selectedFeeItem)
|
||||
val lastItem = index == state.feeItems.size - 1
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -128,21 +141,7 @@ private fun FeeSelectorItems(
|
|||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.then(
|
||||
if (isSelected) {
|
||||
Modifier
|
||||
.border(
|
||||
width = 2.5.dp,
|
||||
color = iconTint.copy(alpha = 0.2F),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
)
|
||||
.padding(2.5.dp)
|
||||
.border(width = 1.dp, color = iconTint, shape = RoundedCornerShape(14.dp))
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.selectedBorder(isSelected = isSelected)
|
||||
.clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) })
|
||||
when (item) {
|
||||
is FeeItem.Suggested -> RegularFeeItemContent(
|
||||
|
|
@ -156,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) {
|
||||
|
|
@ -182,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) {
|
||||
|
|
@ -208,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) {
|
||||
|
|
@ -234,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) {
|
||||
|
|
@ -255,9 +254,8 @@ private fun FeeSelectorItems(
|
|||
isSelected = isSelected,
|
||||
iconBackgroundColor = iconBackgroundColor,
|
||||
iconTint = iconTint,
|
||||
displayNonceInput = state.displayNonceInput,
|
||||
nonce = state.nonce,
|
||||
onNonceChange = state.onNonceChange,
|
||||
onValueChange = feeSelectorIntents::onCustomFeeValueChange,
|
||||
nonce = state.feeNonce,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -271,9 +269,8 @@ private fun CustomFeeBlock(
|
|||
isSelected: Boolean,
|
||||
iconBackgroundColor: Color,
|
||||
iconTint: Color,
|
||||
displayNonceInput: Boolean,
|
||||
nonce: BigInteger?,
|
||||
onNonceChange: (String) -> Unit,
|
||||
onValueChange: (Int, String) -> Unit,
|
||||
nonce: FeeNonce,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -305,10 +302,8 @@ private fun CustomFeeBlock(
|
|||
) {
|
||||
ExpandedCustomFeeItems(
|
||||
customFeeFields = customFee.customValues,
|
||||
onValueChange = { _, _ -> },
|
||||
displayNonceInput = displayNonceInput,
|
||||
onValueChange = onValueChange,
|
||||
nonce = nonce,
|
||||
onNonceChange = onNonceChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -318,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,
|
||||
|
|
@ -334,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) },
|
||||
|
|
@ -347,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,
|
||||
|
|
@ -355,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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -474,7 +472,12 @@ private fun FeeSelectorBS_Preview(
|
|||
state: FeeSelectorUM.Content,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
FeeSelectorModalBottomSheet(onDismiss = {}, state = state, feeSelectorIntents = StubFeeSelectorIntents())
|
||||
FeeSelectorModalBottomSheet(
|
||||
onDismiss = {},
|
||||
state = state,
|
||||
feeSelectorIntents = StubFeeSelectorIntents(),
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,14 +498,17 @@ 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),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ import com.arkivanov.decompose.value.subscribe
|
|||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
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
|
||||
|
|
@ -30,11 +32,14 @@ import com.tangem.features.send.v2.common.utils.safeNextClick
|
|||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -44,10 +49,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LargeClass")
|
||||
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>()
|
||||
|
|
@ -142,7 +149,8 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route)
|
||||
is CommonSendRoute.Amount -> getAmountComponent(factoryContext, route)
|
||||
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
|
||||
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
is CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
is CommonSendRoute.ConfirmSuccess -> getConfirmSuccessComponent(factoryContext)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(
|
||||
|
|
@ -288,7 +296,11 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
callback = model,
|
||||
predefinedValues = model.predefinedValues,
|
||||
onLoadFee = model::loadFee,
|
||||
onSendTransaction = {
|
||||
innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess)
|
||||
},
|
||||
),
|
||||
feeSelectorComponentFactory = feeSelectorComponentFactory,
|
||||
)
|
||||
} else {
|
||||
model.showAlertError()
|
||||
|
|
@ -296,6 +308,69 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
val state = model.uiState.value
|
||||
val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value
|
||||
val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl
|
||||
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
|
||||
val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value
|
||||
|
||||
if (sendAmount == null ||
|
||||
destinationAddress == null ||
|
||||
txUrl == null
|
||||
) {
|
||||
model.showAlertError()
|
||||
return getStubComponent()
|
||||
}
|
||||
|
||||
val destinationBlockComponent =
|
||||
DefaultSendDestinationBlockComponent(
|
||||
appComponentContext = child("sendConfirmDestinationBlock"),
|
||||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = analyticCategoryName,
|
||||
userWalletId = model.userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
predefinedValues = model.predefinedValues,
|
||||
),
|
||||
onResult = { },
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
val feeBlockComponent = SendFeeBlockComponent(
|
||||
appComponentContext = child("sendConfirmFeeBlock"),
|
||||
params = SendFeeComponentParams.FeeBlockParams(
|
||||
state = model.uiState.value.feeUM,
|
||||
analyticsCategoryName = analyticCategoryName,
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
appCurrency = model.appCurrency,
|
||||
sendAmount = sendAmount,
|
||||
destinationAddress = destinationAddress,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
onLoadFee = model::loadFee,
|
||||
),
|
||||
onResult = { },
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
return SendConfirmSuccessComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendConfirmSuccessComponent.Params(
|
||||
sendUMFlow = model.uiState,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
analyticsCategoryName = analyticCategoryName,
|
||||
currentRoute = currentRoute,
|
||||
txUrl = txUrl,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStubComponent() = StubComponent()
|
||||
|
||||
class StubComponent : ComposableContentComponent {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -155,6 +172,7 @@ internal class SendConfirmComponent(
|
|||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
val onSendTransaction: () -> Unit,
|
||||
)
|
||||
|
||||
interface ModelCallback {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -291,6 +301,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
isShowTapHelp = isShowTapHelp,
|
||||
walletName = stringReference(userWallet.name),
|
||||
).transform(uiState.value.confirmUM),
|
||||
confirmData = confirmData,
|
||||
)
|
||||
}
|
||||
updateConfirmNotifications()
|
||||
|
|
@ -299,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)
|
||||
|
|
@ -318,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,
|
||||
|
|
@ -385,6 +414,10 @@ internal class SendConfirmModel @Inject constructor(
|
|||
addTokenToWalletIfNeeded()
|
||||
sendBalanceUpdater.scheduleUpdates()
|
||||
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
|
||||
if (uiState.value.isRedesignEnabled) {
|
||||
params.callback.onResult(uiState.value)
|
||||
params.onSendTransaction()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -444,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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -463,23 +507,35 @@ internal class SendConfirmModel @Inject constructor(
|
|||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { it.second is CommonSendRoute.Confirm }.onEach { (state, _) ->
|
||||
).filter {
|
||||
it.second is CommonSendRoute.Confirm
|
||||
}.onEach { (state, _) ->
|
||||
val amountUM = state.amountUM as? AmountState.Data
|
||||
val confirmUM = state.confirmUM
|
||||
val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
title = resourceReference(
|
||||
id = R.string.send_summary_title,
|
||||
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
|
||||
),
|
||||
title = if (state.isRedesignEnabled) {
|
||||
stringReference("")
|
||||
} else {
|
||||
resourceReference(
|
||||
id = R.string.send_summary_title,
|
||||
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
|
||||
)
|
||||
},
|
||||
subtitle = if (uiState.value.isRedesignEnabled) {
|
||||
null
|
||||
} else {
|
||||
amountUM?.title
|
||||
},
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconRes = if (state.isRedesignEnabled) {
|
||||
when (confirmUM) {
|
||||
is ConfirmUM.Success -> R.drawable.ic_close_24
|
||||
else -> R.drawable.ic_back_24
|
||||
}
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
|
|
@ -491,31 +547,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
)
|
||||
appRouter.pop()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = when (confirmUM) {
|
||||
is ConfirmUM.Success -> resourceReference(R.string.common_close)
|
||||
is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
resourceReference(R.string.send_sending)
|
||||
} else {
|
||||
resourceReference(R.string.common_send)
|
||||
}
|
||||
else -> resourceReference(R.string.common_send)
|
||||
},
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend },
|
||||
isEnabled = confirmUM.isPrimaryButtonEnabled,
|
||||
isHapticClick = isReadyToSend,
|
||||
onClick = {
|
||||
when (confirmUM) {
|
||||
is ConfirmUM.Success -> appRouter.pop()
|
||||
is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
return@NavigationButton
|
||||
} else {
|
||||
onSendClick()
|
||||
}
|
||||
else -> return@NavigationButton
|
||||
}
|
||||
},
|
||||
),
|
||||
primaryButton = primaryButtonUM(),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (
|
||||
NavigationButton(
|
||||
|
|
@ -534,6 +566,42 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun primaryButtonUM(): NavigationButton {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending
|
||||
return NavigationButton(
|
||||
textReference = when (confirmUM) {
|
||||
is ConfirmUM.Success -> resourceReference(R.string.common_close)
|
||||
is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
resourceReference(R.string.send_sending)
|
||||
} else {
|
||||
resourceReference(R.string.common_send)
|
||||
}
|
||||
else -> resourceReference(R.string.common_send)
|
||||
},
|
||||
iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend },
|
||||
isEnabled = confirmUM.isPrimaryButtonEnabled,
|
||||
isHapticClick = isReadyToSend,
|
||||
onClick = {
|
||||
when (confirmUM) {
|
||||
is ConfirmUM.Success -> appRouter.pop()
|
||||
is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
return@NavigationButton
|
||||
} else {
|
||||
onSendClick()
|
||||
}
|
||||
else -> return@NavigationButton
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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,34 +84,45 @@ private fun LazyListScope.blocks(
|
|||
destinationBlockComponent: DefaultSendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
item(key = BLOCKS_KEY) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.confirmUM is ConfirmUM.Success,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success }
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.sent_transaction_sent_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_date_format,
|
||||
wrappedList(
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(),
|
||||
),
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TransactionDoneTitleAnimated(uiState: SendUM) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.confirmUM is ConfirmUM.Success,
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
) {
|
||||
val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success }
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.sent_transaction_sent_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_date_format,
|
||||
wrappedList(
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(),
|
||||
),
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -23,4 +24,9 @@ internal interface CommonSendModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(SendConfirmModel::class)
|
||||
fun provideSendConfirmModel(model: SendConfirmModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendConfirmSuccessModel::class)
|
||||
fun provideSendConfirmSuccessModel(model: SendConfirmSuccessModel): Model
|
||||
}
|
||||
|
|
@ -39,16 +39,19 @@ 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
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
|
|
@ -65,7 +68,8 @@ internal interface SendComponentCallback :
|
|||
SendAmountComponent.ModelCallback,
|
||||
SendFeeComponent.ModelCallback,
|
||||
SendDestinationComponent.ModelCallback,
|
||||
SendConfirmComponent.ModelCallback
|
||||
SendConfirmComponent.ModelCallback,
|
||||
SendConfirmSuccessComponent.ModelCallback
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
|
|
@ -87,7 +91,7 @@ internal class SendModel @Inject constructor(
|
|||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger,
|
||||
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
) : Model(), SendComponentCallback {
|
||||
|
||||
|
|
@ -151,6 +155,15 @@ internal class SendModel @Inject constructor(
|
|||
_uiState.update { sendUM }
|
||||
}
|
||||
|
||||
override fun onConvertToAnotherToken(lastAmount: String) {
|
||||
params.callback?.onConvertToAnotherToken(lastAmount = lastAmount)
|
||||
}
|
||||
|
||||
override fun onError(error: GetUserWalletError) {
|
||||
Timber.w(error.toString())
|
||||
showAlertError()
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val predefinedValues = predefinedValues
|
||||
val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) {
|
||||
|
|
@ -347,7 +360,7 @@ internal class SendModel @Inject constructor(
|
|||
)
|
||||
// If it is in active state use flow to update value in amount component
|
||||
modelScope.launch {
|
||||
amount?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) }
|
||||
amount?.let { sendAmountUpdateTrigger.triggerUpdateAmount(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -382,5 +395,7 @@ internal class SendModel @Inject constructor(
|
|||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
|
||||
confirmData = null,
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.send.v2.send.success
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel
|
||||
import com.tangem.features.send.v2.send.success.ui.SendConfirmSuccessContent
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class SendConfirmSuccessComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmSuccessModel = getOrCreateModel(params = params)
|
||||
private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent
|
||||
private val feeBlockComponent: SendFeeBlockComponent = params.feeBlockComponent
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsState()
|
||||
SendConfirmSuccessContent(
|
||||
sendUM = state,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sendUMFlow: StateFlow<SendUM>,
|
||||
val destinationBlockComponent: SendDestinationBlockComponent,
|
||||
val feeBlockComponent: SendFeeBlockComponent,
|
||||
val analyticsCategoryName: String,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val txUrl: String,
|
||||
val callback: ModelCallback,
|
||||
)
|
||||
|
||||
interface ModelCallback {
|
||||
fun onResult(sendUM: SendUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.features.send.v2.send.success.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SendConfirmSuccessModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
) : Model() {
|
||||
private val params: SendConfirmSuccessComponent.Params = paramsContainer.require()
|
||||
private val _uiState = params.sendUMFlow
|
||||
val uiState = _uiState
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
}
|
||||
|
||||
private fun configConfirmSuccessNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) ->
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
title = stringReference(""),
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
appRouter.pop()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
iconRes = null,
|
||||
isEnabled = true,
|
||||
isHapticClick = false,
|
||||
onClick = {
|
||||
appRouter.pop()
|
||||
},
|
||||
),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
|
||||
urlOpener.openUrl(params.txUrl)
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
|
||||
shareManager.shareText(params.txUrl)
|
||||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onResult(sendUM: SendUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.send.v2.send.success.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.toPx
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.v2.common.ui.SendNavigationButtons
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun SendConfirmSuccessContent(
|
||||
sendUM: SendUM,
|
||||
destinationBlockComponent: SendDestinationBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
) {
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(ANIMATION_DELAY)
|
||||
visible = true
|
||||
}
|
||||
|
||||
val height = ANIMATION_OFFSET.toPx().toInt()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { height },
|
||||
).plus(fadeIn()),
|
||||
exit = slideOutVertically().plus(fadeOut()),
|
||||
label = "Animate success content",
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.scrollable(
|
||||
state = rememberScrollState(),
|
||||
orientation = Orientation.Horizontal,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
if (sendUM.confirmUM is ConfirmUM.Success) {
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.sent_transaction_sent_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_date_format,
|
||||
wrappedList(
|
||||
sendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
sendUM.confirmUM.transactionDate.toTimeFormat(),
|
||||
),
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
amountState = sendUM.amountUM,
|
||||
isClickDisabled = true,
|
||||
isEditingDisabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
destinationBlockComponent.Content(modifier = Modifier)
|
||||
feeBlockComponent.Content(modifier = Modifier)
|
||||
}
|
||||
SpacerHMax()
|
||||
SendNavigationButtons(navigationUM = sendUM.navigationUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ANIMATION_DELAY = 600L
|
||||
private val ANIMATION_OFFSET = (-40).dp
|
||||
|
|
@ -2,15 +2,19 @@ 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
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
|
||||
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,
|
||||
val confirmData: ConfirmData?,
|
||||
)
|
||||
|
|
@ -2,8 +2,11 @@ package com.tangem.features.send.v2.sendnft.analytics
|
|||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.ui.extensions.capitalize
|
||||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
||||
|
||||
/**
|
||||
|
|
@ -18,11 +21,15 @@ internal sealed class NFTSendAnalyticEvents(
|
|||
data class TransactionScreenOpened(
|
||||
val token: String,
|
||||
val feeType: AnalyticsParam.FeeType,
|
||||
val blockchain: String,
|
||||
val nonceNotEmpty: Boolean,
|
||||
) : NFTSendAnalyticEvents(
|
||||
event = "NFT Sent Screen Opened",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
FEE_TYPE to feeType.value,
|
||||
BLOCKCHAIN to blockchain,
|
||||
NONCE to nonceNotEmpty.toString().capitalize(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ internal class NFTSendAnalyticHelper @Inject constructor(
|
|||
NFTSendAnalyticEvents.TransactionScreenOpened(
|
||||
token = cryptoCurrency.symbol,
|
||||
feeType = feeType,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
nonceNotEmpty = feeSelectorUM.nonce != null,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ 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.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
|
||||
|
|
@ -61,6 +59,8 @@ internal class NFTSendModel @Inject constructor(
|
|||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase,
|
||||
|
|
@ -146,9 +146,16 @@ internal class NFTSendModel @Inject constructor(
|
|||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
|
||||
cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull()
|
||||
?.filterIsInstance<CryptoCurrency.Coin>()
|
||||
?.firstOrNull { it.network == nftAsset.network }
|
||||
cryptoCurrency = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
|
||||
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId),
|
||||
)
|
||||
?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network }
|
||||
} else {
|
||||
getCryptoCurrenciesUseCase(userWalletId).getOrNull()
|
||||
?.filterIsInstance<CryptoCurrency.Coin>()
|
||||
?.firstOrNull { it.network == nftAsset.network }
|
||||
}
|
||||
?: return@launch
|
||||
|
||||
getCurrenciesStatusUpdates(
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenContent
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
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.res.TangemTheme
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.v2.subcomponents.amount.ui.SendAmountContent
|
||||
|
||||
internal class SendAmountComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
|
|
@ -29,15 +28,18 @@ internal class SendAmountComponent(
|
|||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
|
||||
|
||||
AmountScreenContent(
|
||||
SendAmountContent(
|
||||
amountState = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
clickIntents = model,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
isSendWithSwapEnabled = model.isSendWithSwapEnabled,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallback : NavigationModelCallback {
|
||||
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
|
||||
fun onConvertToAnotherToken(lastAmount: String)
|
||||
fun onError(error: GetUserWalletError)
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ interface SendAmountReduceListener {
|
|||
* Trigger amount change from another component.
|
||||
* Different from another triggers because it takes raw string instead of BigDecimal
|
||||
*/
|
||||
interface SendAmountUpdateQRTrigger {
|
||||
interface SendAmountUpdateTrigger {
|
||||
suspend fun triggerUpdateAmount(amountValue: String)
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ interface SendAmountUpdateQRTrigger {
|
|||
* Trigger amount change from another component.
|
||||
* Different from another triggers because it takes raw string instead of BigDecimal
|
||||
*/
|
||||
interface SendAmountUpdateQRListener {
|
||||
interface SendAmountUpdateListener {
|
||||
val updateAmountTriggerFlow: Flow<String>
|
||||
}
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ interface SendAmountUpdateQRListener {
|
|||
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
|
||||
SendAmountReduceTrigger,
|
||||
SendAmountReduceListener,
|
||||
SendAmountUpdateQRTrigger,
|
||||
SendAmountUpdateQRListener {
|
||||
SendAmountUpdateTrigger,
|
||||
SendAmountUpdateListener {
|
||||
|
||||
override val reduceToTriggerFlow = MutableSharedFlow<BigDecimal>()
|
||||
override val reduceByTriggerFlow = MutableSharedFlow<ReduceByData>()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.di
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.amount.*
|
||||
import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -22,9 +21,9 @@ internal interface SendAmountModule {
|
|||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideSendAmountUpdateQRListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRListener
|
||||
fun provideSendAmountUpdateListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateListener
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideSendAmountUpdateQRTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRTrigger
|
||||
fun provideSendAmountUpdateTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateTrigger
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.model
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
|
||||
interface SendAmountClickIntents : AmountScreenClickIntents {
|
||||
|
||||
fun onConvertToAnotherToken()
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.send.v2.subcomponents.amount.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.amountScreen.converters.*
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountBoundaryUpdateTransformer
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
|
||||
|
|
@ -31,7 +30,7 @@ import com.tangem.features.send.v2.api.entity.PredefinedValues
|
|||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRListener
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents
|
||||
import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeData
|
||||
|
|
@ -54,12 +53,12 @@ internal class SendAmountModel @Inject constructor(
|
|||
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
|
||||
private val sendAmountReduceListener: SendAmountReduceListener,
|
||||
private val feeReloadTrigger: SendFeeReloadTrigger,
|
||||
private val sendAmountUpdateQRListener: SendAmountUpdateQRListener,
|
||||
private val sendAmountUpdateListener: SendAmountUpdateListener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
) : Model(), AmountScreenClickIntents {
|
||||
) : Model(), SendAmountClickIntents {
|
||||
|
||||
private val params: SendAmountComponentParams = paramsContainer.require()
|
||||
private var appCurrency: AppCurrency = AppCurrency.Default
|
||||
|
|
@ -68,6 +67,8 @@ internal class SendAmountModel @Inject constructor(
|
|||
private val _uiState = MutableStateFlow(params.state)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
val isSendWithSwapEnabled = sendFeatureToggles.isSendWithSwapEnabled
|
||||
|
||||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = params.cryptoCurrency,
|
||||
|
|
@ -84,10 +85,18 @@ internal class SendAmountModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initAppCurrency() {
|
||||
modelScope.launch {
|
||||
userWallet = getUserWalletUseCase(params.userWalletId).getOrNull()
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
}
|
||||
getUserWalletUseCase.invokeFlow(params.userWalletId)
|
||||
.onEach { either ->
|
||||
either.fold(
|
||||
ifLeft = { error ->
|
||||
val amountParams = params as? SendAmountComponentParams.AmountParams
|
||||
amountParams?.callback?.onError(error)
|
||||
},
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnCryptoCurrencyStatusFlow() {
|
||||
|
|
@ -99,7 +108,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
subscribeOnAmountReduceByTriggerUpdates()
|
||||
subscribeOnAmountReduceToTriggerUpdates()
|
||||
subscribeOnAmountIgnoreReduceTriggerUpdates()
|
||||
subscribeOnAmountUpdateQRTriggerUpdates()
|
||||
subscribeOnAmountUpdateTriggerUpdates()
|
||||
}
|
||||
initMinBoundary()
|
||||
}
|
||||
|
|
@ -118,6 +127,8 @@ internal class SendAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
|
||||
|
||||
if (uiState.value is AmountState.Data) {
|
||||
_uiState.update(
|
||||
AmountBoundaryUpdateTransformer(
|
||||
|
|
@ -217,6 +228,12 @@ internal class SendAmountModel @Inject constructor(
|
|||
saveResult()
|
||||
}
|
||||
|
||||
override fun onConvertToAnotherToken() {
|
||||
val amountFieldData = uiState.value as? AmountState.Data
|
||||
val amountParams = params as? SendAmountComponentParams.AmountParams
|
||||
amountParams?.callback?.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
|
||||
}
|
||||
|
||||
private fun subscribeOnAmountReduceToTriggerUpdates() {
|
||||
sendAmountReduceListener.reduceToTriggerFlow
|
||||
.onEach { reduceTo ->
|
||||
|
|
@ -261,8 +278,8 @@ internal class SendAmountModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAmountUpdateQRTriggerUpdates() {
|
||||
sendAmountUpdateQRListener.updateAmountTriggerFlow
|
||||
private fun subscribeOnAmountUpdateTriggerUpdates() {
|
||||
sendAmountUpdateListener.updateAmountTriggerFlow
|
||||
.onEach { amount ->
|
||||
onAmountValueChange(amount)
|
||||
saveResult()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.features.send.v2.subcomponents.amount.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenContent
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
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.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountClickIntentsStub
|
||||
|
||||
@Composable
|
||||
fun SendAmountContent(
|
||||
amountState: AmountState,
|
||||
isBalanceHidden: Boolean,
|
||||
clickIntents: SendAmountClickIntents,
|
||||
isSendWithSwapEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
AmountScreenContent(
|
||||
amountState = amountState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
if (isSendWithSwapEnabled) {
|
||||
SendConvertTokenButton(
|
||||
onConvertToAnother = clickIntents::onConvertToAnotherToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = null,
|
||||
onClick = onConvertToAnother,
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.align(Alignment.Center),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_convert_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.background(TangemTheme.colors.control.unchecked, CircleShape)
|
||||
.padding(2.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(com.tangem.common.ui.R.string.send_amount_convert_to_another_token),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreviewProvider::class) params: AmountState) {
|
||||
TangemThemePreview {
|
||||
SendAmountContent(
|
||||
amountState = params,
|
||||
isBalanceHidden = true,
|
||||
clickIntents = SendAmountClickIntentsStub,
|
||||
isSendWithSwapEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendAmountContentPreviewProvider : PreviewParameterProvider<AmountState> {
|
||||
override val values: Sequence<AmountState>
|
||||
get() = sequenceOf(
|
||||
AmountStatePreviewData.amountStateV2,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
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