From 8a948333fefc268e3aac95fd64166ca40b134099 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Oct 2023 15:29:33 +0400 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../managetokens/state/SearchBarState.kt | 11 ++ .../managetokens/ui/components/SearchBar.kt | 143 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt new file mode 100644 index 0000000000..d64808239e --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state + +/** + * SearchBar state. + */ +internal data class SearchBarState( + val query: String, + val onQueryChange: (String) -> Unit, + val active: Boolean, + val onActiveChange: (Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt new file mode 100644 index 0000000000..6040c96260 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt @@ -0,0 +1,143 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.SearchBarState + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun TokensSearchBar(state: SearchBarState, modifier: Modifier = Modifier) { + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + TextField( + value = state.query, + onValueChange = state.onQueryChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + keyboardController?.hide() + focusManager.clearFocus() + }, + ), + singleLine = true, + maxLines = 1, + textStyle = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.primary1, + ), + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.ic_search_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier.clickable { state.onActiveChange(true) }, + ) + }, + trailingIcon = { + if (state.query.isNotEmpty() || state.active) { + IconButton( + onClick = { + if (state.query.isNotEmpty()) { + state.onQueryChange("") + } + focusManager.clearFocus() + keyboardController?.hide() + state.onActiveChange(false) + }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + }, + placeholder = { + Text( + text = stringResource(R.string.manage_tokens_search_placeholder), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + }, + shape = RoundedCornerShape(TangemTheme.dimens.radius36), + colors = searchbarTextFieldColors(), + modifier = modifier + .fillMaxWidth() + .onFocusChanged { + if (it.isFocused) { + state.onActiveChange(true) + } else { + state.onActiveChange(false) + } + }, + ) +} + +@Composable +private fun searchbarTextFieldColors(): TextFieldColors { + return TextFieldDefaults.textFieldColors( + backgroundColor = TangemTheme.colors.field.primary, + textColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) +} + +@Preview +@Composable +private fun Preview_TokensSearchBar_Light( + @PreviewParameter(SearchBarkConfigProvider::class) + state: SearchBarState, +) { + TangemTheme(isDark = false) { + TokensSearchBar(state) + } +} + +@Preview +@Composable +private fun Preview_TokensSearchBar_Dark(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState) { + TangemTheme(isDark = true) { + TokensSearchBar(state) + } +} + +private class SearchBarkConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + SearchBarState( + query = "BTC", + onQueryChange = {}, + active = true, + onActiveChange = {}, + ), + SearchBarState( + query = "", + onQueryChange = {}, + active = false, + onActiveChange = {}, + ), + ), +) \ No newline at end of file From 96f5aa8238a5e57af1100a4d4913014affdbb8a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Nov 2023 21:52:01 +0300 Subject: [PATCH 2/4] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 2 +- core/ui/build.gradle.kts | 1 + .../appbar/AppBarWithBackButtonAndIcon.kt | 103 +++++++++ .../fields/AmountVisualTransformation.kt | 32 +++ .../core/ui/utils/DateTimeFormatters.kt | 13 +- .../com/tangem/core/ui/utils/DateUtils.kt | 33 +++ .../main/res/drawable/ic_qrcode_scan_24.xml | 13 ++ .../DefaultWalletManagersFacade.kt | 5 +- .../walletmanager/WalletManagersFacade.kt | 7 + features/send/impl/build.gradle.kts | 13 +- .../send/impl/presentation/SendFragment.kt | 44 ++-- .../presentation/domain/AvailableWallet.kt | 15 ++ .../domain/SendRecipientListContent.kt | 21 ++ .../presentation/state/SendStateFactory.kt | 133 ++++++++++-- .../impl/presentation/state/SendUiState.kt | 102 ++++----- .../impl/presentation/state/StateRouter.kt | 39 ++++ .../state/amount/SendAmountStateConverter.kt | 73 +++---- .../fields/SendAmountFieldChangeConverter.kt | 60 +++--- .../state/fields/SendAmountFieldConverter.kt | 1 - .../state/fields/SendTextField.kt | 37 +++- .../SendRecipientAddressFieldConverter.kt | 31 +++ .../recipient/SendRecipientListConverter.kt | 113 ++++++++++ .../SendRecipientMemoFieldConverter.kt | 51 +++++ .../recipient/SendRecipientStateConverter.kt | 30 +++ .../presentation/ui/SendNavigationButtons.kt | 48 +++-- .../send/impl/presentation/ui/SendScreen.kt | 96 ++++++--- .../presentation/ui/amount/AmountField.kt | 31 +-- .../ui/amount/AmountFieldContainer.kt | 10 +- .../ui/{ => amount}/SendAmountContent.kt | 29 +-- .../presentation/ui/common/FooterContainer.kt | 38 ++++ .../ui/recipient/ListItemWithIcon.kt | 135 +++++++----- .../ui/recipient/SendRecipientContent.kt | 199 ++++++++++++++++++ .../presentation/ui/recipient/TextFields.kt | 50 +++-- .../viewmodel/AddressVerification.kt | 18 ++ .../viewmodel/MemoVerification.kt | 46 ++++ .../viewmodel/SendClickIntents.kt | 12 ++ .../presentation/viewmodel/SendViewModel.kt | 167 +++++++++++++-- .../TokenDetailsTxHistoryItemFlowConverter.kt | 32 +-- gradle/dependencies.toml | 4 +- 40 files changed, 1503 insertions(+), 388 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt create mode 100644 core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt rename features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/{ => amount}/SendAmountContent.kt (75%) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2af2ae2c68..52aa5dfca4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -395,7 +395,7 @@ Сумма к получению %1$s Адрес Код назначения - %1$s в %2$s + %1$s в %2$s Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. @@ -427,7 +427,7 @@ Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению. Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. Сумма резерва не может быть менее %1$s. - Пожалуйста, пополните свой баланс, чтобы продолжить. + Пожалуйста, пополните свой баланс, чтобы продолжить. Увеличение комиссии Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Комиссия превышает баланс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 020371fd62..09f3ec83d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -395,7 +395,7 @@ The recipient will receive %1$s Address Destination Tag - %1$s at %2$s + %1$s at %2$s Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5868a39017..ab9b42cd1f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { /** Project - Core */ implementation(projects.core.res) + implementation(projects.core.utils) /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt new file mode 100644 index 0000000000..73509da21f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -0,0 +1,103 @@ +package com.tangem.core.ui.components.appbar + +import androidx.annotation.DrawableRes +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun AppBarWithBackButtonAndIcon( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + text: String? = null, + @DrawableRes backIconRes: Int? = null, + @DrawableRes iconRes: Int? = null, + onIconClick: (() -> Unit)? = null, + backgroundColor: Color = TangemTheme.colors.background.secondary, +) { + Row( + modifier = modifier + .background(color = backgroundColor) + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(backIconRes ?: R.drawable.ic_back_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable { onBackClick() }, + tint = TangemTheme.colors.icon.primary1, + ) + AnimatedContent( + targetState = text, + modifier = Modifier.weight(1f), + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Toolbar title change", + ) { + if (!it.isNullOrBlank()) { + Text( + text = it, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + ) + } + } + AnimatedContent( + targetState = iconRes, + transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, + label = "Toolbar icon change", + ) { + if (onIconClick != null && it != null) { + Icon( + painter = painterResource(it), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size24) + .clickable { onIconClick() }, + tint = TangemTheme.colors.icon.primary1, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { + TangemTheme(isDark = false) { + AppBarWithBackButtonAndIcon( + text = "Title", + iconRes = R.drawable.ic_qrcode_scan_24, + onBackClick = {}, + onIconClick = {}, + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { + TangemTheme(isDark = true) { + AppBarWithBackButtonAndIcon( + text = "Title", + iconRes = R.drawable.ic_qrcode_scan_24, + onBackClick = {}, + onIconClick = {}, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt new file mode 100644 index 0000000000..89e4fa09ce --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +class AmountVisualTransformation( + private val symbol: String, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return TransformedText( + buildAnnotatedString { + append(text) + if (text.isNotBlank()) { + append(" ") + append(symbol) + } + }, + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return text.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 80e9488f0a..dc27c1641a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -9,12 +9,14 @@ import java.util.Locale @Suppress("MagicNumber") object DateTimeFormatters { + private const val DDMMYYYY = "dd.MM.yyyy" + /** * Two SS means, SHORT style for date and time. * If pattern contains "a", it means time is in 12 hour format. * [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) */ - val timeFormatter by lazy { + val timeFormatter: DateTimeFormatter by lazy { val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a") if (is12HourFormat) { DateTimeFormatterBuilder() @@ -35,7 +37,7 @@ object DateTimeFormatters { } } - val dateFormatter by lazy { + val dateFormatter: DateTimeFormatter by lazy { DateTimeFormatterBuilder() .appendDayOfMonth(1) .appendLiteral(' ') @@ -46,6 +48,13 @@ object DateTimeFormatters { .withLocale(Locale.getDefault()) } + val dateDDMMYYYY: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendPattern(DDMMYYYY) + .toFormatter() + .withLocale(Locale.getDefault()) + } + fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String { return formatter.print(time) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt new file mode 100644 index 0000000000..b0237e897a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.utils + +import android.text.format.DateUtils +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatter + +/** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date. + */ +fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + DateTimeFormatters.formatDate(formatter = formatter, date = localDate) + } +} + +/** + * Returns formatted time according to [formatter]. + */ +fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String { + return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault())) +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml new file mode 100644 index 0000000000..7cde4123e0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index f9bc744566..bc1bf00b14 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -323,6 +323,10 @@ class DefaultWalletManagersFacade( } override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ + return getAddresses(userWalletId, network).sortedBy { it.type } + } + + override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
{ val blockchain = Blockchain.fromId(network.id.value) return getOrCreateWalletManager( @@ -332,7 +336,6 @@ class DefaultWalletManagersFacade( ) ?.wallet ?.addresses - ?.sortedBy { it.type } .orEmpty() } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index e4817280a7..31345d6f99 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -114,6 +114,13 @@ interface WalletManagersFacade { */ suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ /** Returns list of all addresses for all currencies in selected wallet + * + * @param userWalletId selected wallet id + * @param network required to create wallet manager + */ + suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
+ /** * Returns info about rent if wallet manager implemented [RentProvider], otherwise null * diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 9c3a10e456..65f362206d 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -14,12 +14,14 @@ dependencies { /** AndroidX */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.appCompat) + implementation(deps.androidx.paging.runtime) /** Other dependencies */ implementation(deps.kotlin.immutable.collections) implementation(deps.material) implementation(deps.arrow.core) - implementation(deps.tangem.card.core) + implementation(deps.lifecycle.compose) + implementation(deps.jodatime) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -29,6 +31,12 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) + implementation(deps.compose.constraintLayout) + + /** Tangem SDKs */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) /** Common */ implementation(projects.common) @@ -40,12 +48,15 @@ dependencies { /** Domain modules */ implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 61136054f5..2790caab93 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -1,43 +1,53 @@ package com.tangem.features.send.impl.presentation -import androidx.activity.compose.BackHandler +import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.fragment.app.viewModels import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.screen.ComposeBottomSheetFragment +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint +import java.lang.ref.WeakReference import javax.inject.Inject /** * Send fragment */ @AndroidEntryPoint -internal class SendFragment : ComposeBottomSheetFragment() { +internal class SendFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder - override val expandedHeightFraction: Float = 1f + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + viewModel.setRouter( + StateRouter( + fragmentManager = WeakReference(parentFragmentManager), + ), + ) + } @Composable override fun ScreenContent(modifier: Modifier) { - val viewModel = hiltViewModel() - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - - SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) } - BackHandler { dismiss() } - - when (val state = viewModel.uiState) { - is SendUiState.Content -> SendScreen(state) - SendUiState.Dismiss -> dismiss() + val systemBarsColor = TangemTheme.colors.background.tertiary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) } + SendScreen(viewModel.uiState) + } + + override fun onDestroy() { + lifecycle.removeObserver(viewModel) + super.onDestroy() } companion object { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt new file mode 100644 index 0000000000..69e56bfc62 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt @@ -0,0 +1,15 @@ +package com.tangem.features.send.impl.presentation.domain + +import androidx.compose.runtime.Immutable + +/** + * Available wallet to send + * + * @property name wallet name + * @property address blockchain address + */ +@Immutable +data class AvailableWallet( + val name: String, + val address: String, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt new file mode 100644 index 0000000000..6cb3516497 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.impl.presentation.domain + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed class SendRecipientListContent { + data class Item( + val id: String, + val title: TextReference, + val subtitle: TextReference, + val info: TextReference? = null, + @DrawableRes val subtitleIconRes: Int? = null, + ) : SendRecipientListContent() + + data class Wallets( + val list: PersistentList, + ) : SendRecipientListContent() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index d60ad8e28b..87ac690720 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,22 +1,35 @@ package com.tangem.features.send.impl.presentation.state -import arrow.core.Either +import androidx.paging.PagingData +import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter +import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet +import com.tangem.features.send.impl.presentation.viewmodel.validateMemo +import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update internal class SendStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, + private val userWalletProvider: Provider, + private val walletAddressesProvider: Provider>, private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -27,33 +40,129 @@ internal class SendStateFactory( private val amountStateConverter by lazy { SendAmountStateConverter( - currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - clickIntents = clickIntents, iconStateConverter = iconStateConverter, userWalletProvider = userWalletProvider, sendAmountFieldConverter = amountFieldConverter, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) - - fun getAmountState(cryptoCurrencyStatus: Either): SendUiState { - return amountStateConverter.convert(cryptoCurrencyStatus) + private val recipientStateConverter by lazy { + SendRecipientStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) } - fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + private val recipientListStateConverter by lazy { + SendRecipientListConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + // region UI states + fun getInitialState(): SendUiState = SendUiState( + clickIntents = clickIntents, + currentState = MutableStateFlow(SendUiStateType.Amount), + ) + + fun getReadyState(): SendUiState = currentStateProvider().copy( + amountState = amountStateConverter.convert(Unit), + recipientState = recipientStateConverter.convert(Unit), + feeState = SendStates.FeeState(), + ) + //endregion + + //region amount state clicks fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { val state = currentStateProvider() - val amountState = state as? SendUiState.Content.AmountState ?: return state + val amountState = state.amountState ?: return state return if (amountState.isFiatValue == isFiat) { state } else { - return state.copy(isFiatValue = isFiat) + return state.copy(amountState = amountState.copy(isFiatValue = isFiat)) } } + //endregion + + //region recipient + fun onLoadedRecipientList(wallets: List, txHistory: PagingData) { + recipientListStateConverter.convert( + wallets = wallets, + txHistory = txHistory, + ) + } + + fun getOnRecipientAddressValueChangeState(value: String): SendUiState { + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state + + val isValidMemo = validateMemo( + memo = value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + val isAddressInWallet = isNotAddressInWallet( + walletAddresses = walletAddressesProvider(), + address = recipientState.addressTextField.value.value, + ) + val isValidAddress = verifyAddress( + address = recipientState.addressTextField.value.value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + + recipientState.addressTextField.update { + it.copy( + value = value, + error = when { + !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error) + !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) + else -> null + }, + isError = !isValidAddress || !isAddressInWallet, + ) + } + return state.copy( + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + ), + ) + } + + fun getOnRecipientMemoValueChangeState(value: String): SendUiState { + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state + + val isValidMemo = validateMemo( + memo = value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + val isAddressInWallet = isNotAddressInWallet( + walletAddresses = walletAddressesProvider(), + address = recipientState.addressTextField.value.value, + ) + val isValidAddress = verifyAddress( + address = recipientState.addressTextField.value.value, + cryptoCurrency = cryptoCurrencyStatusProvider().currency, + ) + + // todo add memo validation error text + recipientState.memoTextField?.update { + it.copy( + value = value, + error = TextReference.Res(R.string.send_memo_destination_tag_error), + isError = !isValidMemo, + ) + } + return state.copy( + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet, + ), + ) + } + //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index f4c9014b85..6baeb27cfb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -1,71 +1,77 @@ package com.tangem.features.send.impl.presentation.state import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.paging.PagingData import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.flow.MutableStateFlow /** * Ui states of the send screen */ @Immutable -internal sealed class SendUiState { +internal data class SendUiState( + val clickIntents: SendClickIntents, + val amountState: SendStates.AmountState? = null, + val recipientState: SendStates.RecipientState? = null, + val feeState: SendStates.FeeState? = null, + val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), + val currentState: MutableStateFlow, +) - /** States with content */ - sealed class Content : SendUiState() { +@Stable +internal sealed class SendStates { - /** Is primary button enabled */ - abstract val isPrimaryButtonEnabled: Boolean + abstract val type: SendUiStateType - /** Click intents */ - abstract val clickIntents: SendClickIntents + /** Amount state */ + data class AmountState( + override val type: SendUiStateType = SendUiStateType.Amount, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val walletName: String, + val walletBalance: String, + val tokenIconState: TokenIconState, + val isFiatValue: Boolean, + val segmentedButtonConfig: PersistentList, + val amountTextField: MutableStateFlow, + val isPrimaryButtonEnabled: Boolean, + ) : SendStates() - /** Initial state */ - data class Initial( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() + /** Recipient state */ + data class RecipientState( + override val type: SendUiStateType = SendUiStateType.Recipient, + val addressTextField: MutableStateFlow, + val memoTextField: MutableStateFlow?, + val recipients: MutableStateFlow> = MutableStateFlow(PagingData.empty()), + val network: String, + val isPrimaryButtonEnabled: Boolean, + ) : SendStates() - /** Amount state */ - data class AmountState( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - val walletName: String, - val walletBalance: String, - val tokenIconState: TokenIconState, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrency: AppCurrency, - val isFiatValue: Boolean, - val segmentedButtonConfig: PersistentList, - val amountTextField: SendTextField.Amount, - ) : Content() + // todo [REDACTED_JIRA] + /** Fee and speed state */ + data class FeeState( + override val type: SendUiStateType = SendUiStateType.Fee, + ) : SendStates() - // todo [REDACTED_JIRA] - /** Recipient state */ - data class RecipientState( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() + // todo [REDACTED_JIRA] + /** Send state */ + data class SendState( + val isSuccess: Boolean, + ) +} - // todo [REDACTED_JIRA] - /** Fee and speed state */ - data class FeeState( - override val isPrimaryButtonEnabled: Boolean = false, - override val clickIntents: SendClickIntents, - ) : Content() - - // todo [REDACTED_JIRA] - /** Send state */ - data class SendState( - override val isPrimaryButtonEnabled: Boolean = true, - override val clickIntents: SendClickIntents, - ) : Content() - } - - /** Dismiss screen */ - object Dismiss : SendUiState() +enum class SendUiStateType { + Amount, + Recipient, + Fee, + Send, + Done, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt new file mode 100644 index 0000000000..e7526fdf04 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import java.lang.ref.WeakReference + +internal class StateRouter( + private val fragmentManager: WeakReference, +) { + var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount) + + fun onBackClick() { + fragmentManager.get()?.popBackStack() + } + + fun onNextClick() { + when (currentState.value) { + SendUiStateType.Amount -> { + currentState.update { SendUiStateType.Recipient } + } + SendUiStateType.Recipient -> { + currentState.update { SendUiStateType.Fee } + } + else -> { + // todo implement + } + } + } + + fun onPrevClick() { + when (currentState.value) { + SendUiStateType.Amount -> onBackClick() + SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } + SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } + else -> onBackClick() + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index 3545aeccc6..346cd114aa 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -1,64 +1,55 @@ package com.tangem.features.send.impl.presentation.state.amount -import arrow.core.Either import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow internal class SendAmountStateConverter( - private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, - private val clickIntents: SendClickIntents, + private val userWalletProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, private val iconStateConverter: CryptoCurrencyToIconStateConverter, private val sendAmountFieldConverter: SendAmountFieldConverter, -) : Converter, SendUiState> { +) : Converter { - override fun convert(value: Either): SendUiState { - val userWallet = userWalletProvider() ?: return currentStateProvider() + override fun convert(value: Unit): SendStates.AmountState { + val userWallet = userWalletProvider() val appCurrency = appCurrencyProvider() - return value.fold( - ifLeft = { - // TODO add error handling - currentStateProvider() - }, - ifRight = { - val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol) - val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals) - SendUiState.Content.AmountState( - cryptoCurrencyStatus = it, - walletName = userWallet.name, - walletBalance = "$crypto ($fiat)", - tokenIconState = iconStateConverter.convert(it), - appCurrency = appCurrency, - amountTextField = sendAmountFieldConverter.convert(Unit), - isFiatValue = false, - clickIntents = clickIntents, - segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( - title = stringReference(it.currency.symbol), - iconState = iconStateConverter.convert(it), - isFiat = false, - ), - SendAmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconState = iconStateConverter.convert(it), - isFiat = true, - ), - ), - ) - }, + val status = cryptoCurrencyStatusProvider() + val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) + + return SendStates.AmountState( + appCurrency = appCurrency, + cryptoCurrencyStatus = status, + walletName = userWallet.name, + walletBalance = "$crypto ($fiat)", + tokenIconState = iconStateConverter.convert(status), + amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)), + isFiatValue = false, + isPrimaryButtonEnabled = false, + segmentedButtonConfig = persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(status.currency.symbol), + iconState = iconStateConverter.convert(status), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconState = iconStateConverter.convert(status), + isFiat = true, + ), + ), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 5c91a7489e..cacb180097 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,8 +1,11 @@ package com.tangem.features.send.impl.presentation.state.fields import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update import java.text.DecimalFormatSymbols import java.text.NumberFormat @@ -11,21 +14,14 @@ internal class SendAmountFieldChangeConverter( ) : Converter { override fun convert(value: String): SendUiState { val state = currentStateProvider() + val amountState = state.amountState ?: return state - if ( - state !is SendUiState.Content.AmountState || - value.checkDecimalSeparatorDuplicate() - ) { - return state - } - + if (value.checkDecimalSeparatorDuplicate()) return state if (value.isEmpty()) return state.emptyState() - val fiatRate = state.cryptoCurrencyStatus.value.fiatRate - + val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate val trimmedValue = value.trim() - - val cryptoValue = if (state.isFiatValue) { + val cryptoValue = if (amountState.isFiatValue) { if (value.isNotBlank()) { trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() } else { @@ -35,7 +31,7 @@ internal class SendAmountFieldChangeConverter( trimmedValue } - val fiatValue = if (!state.isFiatValue) { + val fiatValue = if (!amountState.isFiatValue) { if (value.isNotBlank()) { trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() } else { @@ -45,37 +41,48 @@ internal class SendAmountFieldChangeConverter( trimmedValue } - val isExceedBalance = value.checkExceedBalance(state) - return state.copy( - amountTextField = state.amountTextField.copy( + val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState) + amountState.amountTextField.update { + it.copy( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + ) + } + return state.copy( + amountState = amountState.copy( + isPrimaryButtonEnabled = !isExceedBalance, ), - isPrimaryButtonEnabled = !isExceedBalance, ) } - private fun SendUiState.Content.AmountState.emptyState(): SendUiState { - return copy( - amountTextField = amountTextField.copy( - value = if (!isFiatValue) "" else DEFAULT_VALUE, - fiatValue = if (isFiatValue) "" else DEFAULT_VALUE, + private fun SendUiState.emptyState(): SendUiState { + amountState?.amountTextField?.update { + it.copy( + value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE, + fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE, isError = false, + ) + } + return copy( + amountState = amountState?.copy( + isPrimaryButtonEnabled = false, ), - isPrimaryButtonEnabled = false, ) } private fun String.checkDecimalSeparatorDuplicate(): Boolean { - val regex = "[\\.\\,]".toRegex() + val regex = TRIM_REGEX.toRegex() val decimalSeparatorCount = regex.findAll(this).count() return decimalSeparatorCount > 1 } - private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean { - val currencyStatus = state.cryptoCurrencyStatus.value + private fun String.checkExceedBalance( + cryptoCurrencyStatus: CryptoCurrencyStatus, + state: SendStates.AmountState, + ): Boolean { + val currencyStatus = cryptoCurrencyStatus.value return if (state.isFiatValue) { toBigDecimal() > currencyStatus.fiatAmount } else { @@ -88,10 +95,11 @@ internal class SendAmountFieldChangeConverter( if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString() - return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar) + return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar) } companion object { private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + private const val TRIM_REGEX = "[.,]" } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index d6577f14c2..4b027a624d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -22,7 +22,6 @@ internal class SendAmountFieldConverter( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, ), - label = TextReference.Str(""), placeholder = TextReference.Str(DEFAULT_VALUE), isError = false, error = TextReference.Res(R.string.send_insufficient_funds), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index f62f981b67..456359a16c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -16,20 +16,43 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - /** Label */ - abstract val label: TextReference - - /** Placeholder (hint) */ - abstract val placeholder: TextReference + // /** Placeholder (hint) */ + // abstract val placeholder: TextReference data class Amount( override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, - override val label: TextReference, - override val placeholder: TextReference, + val placeholder: TextReference, val fiatValue: String, val isError: Boolean, val error: TextReference, ) : SendTextField() + + data class RecipientAddress( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + ) : SendTextField() + + data class RecipientMemo( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + ) : SendTextField() + + data class CustomFee( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val label: TextReference? = null, + ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt new file mode 100644 index 0000000000..75cd69aa6a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendRecipientAddressFieldConverter( + private val clickIntents: SendClickIntents, +) : Converter> { + + override fun convert(value: Unit): MutableStateFlow { + return MutableStateFlow( + SendTextField.RecipientAddress( + value = "", + onValueChange = clickIntents::onRecipientAddressValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + placeholder = TextReference.Res(R.string.send_enter_address_field), + label = TextReference.Res(R.string.send_recipient), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt new file mode 100644 index 0000000000..da8a97622d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt @@ -0,0 +1,113 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import androidx.paging.* +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.update + +internal class SendRecipientListConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) { + + fun convert(wallets: List, txHistory: PagingData) { + val filteredWallets = wallets.filterNotNull() + .groupBy { item -> item.name } + .values.flatten() + .mapIndexed { index, item -> + item.copy( + name = "${item.name} ${index.inc()}", + ) + } + + currentStateProvider().recipientList.update { + txHistory.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + isTransfer && isSingleAddress && isNotContract + }.map { tx -> + SendRecipientListContent.Item( + id = tx.txHash, + title = tx.extractAddress(), + subtitle = TextReference.Str(tx.getAmount()), + info = tx.extractTimestamp(), + subtitleIconRes = tx.extractIconRes(), + ) + }.insertWallets(filteredWallets) + } + } + + private fun PagingData.insertWallets( + wallets: List, + ): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + return@insertSeparators when { + before == null && after is SendRecipientListContent.Item -> { + SendRecipientListContent.Wallets( + wallets.map { + SendRecipientListContent.Item( + id = it.address, + title = TextReference.Str(it.address), + subtitle = TextReference.Str(it.name), + ) + }.toPersistentList(), + ) + } + else -> null + } + } + } + + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { + when (val destination = destinationType) { + is TxHistoryItem.DestinationType.Multiple -> TextReference.Res( + R.string.transaction_history_multiple_addresses, + ) + is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address) + } + } else { + when (val source = sourceType) { + is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.SourceType.Single -> TextReference.Str(source.address) + } + } + + private fun TxHistoryItem.extractIconRes() = if (isOutgoing) { + R.drawable.ic_arrow_up_24 + } else { + R.drawable.ic_arrow_down_24 + } + + private fun TxHistoryItem.getAmount(): String { + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + return amount.toFormattedCurrencyString( + currency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } + + private fun TxHistoryItem.extractTimestamp(): TextReference { + val date = timestampInMillis.toDateFormat( + formatter = DateTimeFormatters.dateDDMMYYYY, + ) + val time = timestampInMillis.toTimeFormat() + return TextReference.Res(R.string.send_date_format, wrappedList(date, time)) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt new file mode 100644 index 0000000000..d1890038cb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -0,0 +1,51 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendRecipientMemoFieldConverter( + private val clickIntents: SendClickIntents, + private val cryptoCurrencyStatus: Provider, +) : Converter> { + + fun convertOrNull(): MutableStateFlow? { + val cryptoCurrency = cryptoCurrencyStatus().currency + + return when (cryptoCurrency.network.id.value) { + Blockchain.XRP.id -> convert(R.string.send_destination_tag_field) + Blockchain.Binance.id, + Blockchain.TON.id, + Blockchain.Cosmos.id, + Blockchain.TerraV1.id, + Blockchain.TerraV2.id, + Blockchain.Stellar.id, + -> convert(R.string.send_extras_hint_memo) + else -> null + } + } + + override fun convert(value: Int): MutableStateFlow { + return MutableStateFlow( + SendTextField.RecipientMemo( + value = "", + onValueChange = clickIntents::onRecipientMemoValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Text, + ), + placeholder = TextReference.Res(R.string.send_optional_field), + label = TextReference.Res(value), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt new file mode 100644 index 0000000000..ebdafea549 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter + +internal class SendRecipientStateConverter( + private val clickIntents: SendClickIntents, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) } + private val memoFieldConverter by lazy { + SendRecipientMemoFieldConverter( + clickIntents, + cryptoCurrencyStatusProvider, + ) + } + + override fun convert(value: Unit): SendStates.RecipientState { + return SendStates.RecipientState( + addressTextField = addressFieldConverter.convert(Unit), + memoTextField = memoFieldConverter.convertOrNull(), + network = cryptoCurrencyStatusProvider().currency.network.name, + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 599b46babc..30ca727485 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource @@ -19,9 +20,10 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable -internal fun SendNavigationButtons(uiState: SendUiState.Content) { +internal fun SendNavigationButtons(uiState: SendUiState) { Row( modifier = Modifier .fillMaxWidth() @@ -38,9 +40,11 @@ internal fun SendNavigationButtons(uiState: SendUiState.Content) { } @Composable -private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { +private fun SendSecondaryNavigationButton(uiState: SendUiState) { + val currentState = uiState.currentState.collectAsState() AnimatedVisibility( - visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState, + visible = currentState.value == SendUiStateType.Recipient || + currentState.value == SendUiStateType.Fee, ) { Icon( modifier = Modifier @@ -48,46 +52,52 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) .clickable { - // todo add prev click + uiState.clickIntents.onPrevClick() } .padding(TangemTheme.dimens.spacing12), painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) } } @Composable -private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) { - val buttonTextId = when (uiState) { - is SendUiState.Content.AmountState, - is SendUiState.Content.RecipientState, - is SendUiState.Content.FeeState, +private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { + val currentState = uiState.currentState.collectAsState() + + val buttonTextId = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + SendUiStateType.Fee, -> R.string.common_next - is SendUiState.Content.SendState -> R.string.common_send + SendUiStateType.Send -> R.string.common_send else -> R.string.common_close } + + val isButtonEnabled = when (currentState.value) { + SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false + SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false + else -> true + } + AnimatedContent( targetState = buttonTextId, label = "Update send screen state", modifier = modifier, ) { textId -> - if (uiState is SendUiState.Content.SendState) { + if (currentState.value == SendUiStateType.Send) { PrimaryButtonIconEnd( text = stringResource(textId), iconResId = R.drawable.ic_tangem_24, - enabled = uiState.isPrimaryButtonEnabled, - onClick = { - // todo add next click - }, + enabled = isButtonEnabled, + onClick = uiState.clickIntents::onNextClick, ) } else { PrimaryButton( text = stringResource(textId), - enabled = uiState.isPrimaryButtonEnabled, - onClick = { - // todo add next click - }, + enabled = isButtonEnabled, + onClick = uiState.clickIntents::onNextClick, ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 34df63ae5a..93a0f62d57 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -1,53 +1,95 @@ package com.tangem.features.send.impl.presentation.ui +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent +import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @Composable -internal fun SendScreen(uiState: SendUiState.Content) { +internal fun SendScreen(uiState: SendUiState) { + val currentState = uiState.currentState.collectAsStateWithLifecycle() + BackHandler { uiState.clickIntents.onPrevClick() } Column( modifier = Modifier + .fillMaxSize() + .systemBarsPadding() .imePadding() - .background( - color = TangemTheme.colors.background.tertiary, - shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius24, - topEnd = TangemTheme.dimens.radius24, - ), - ), + .background(color = TangemTheme.colors.background.tertiary), horizontalAlignment = Alignment.CenterHorizontally, ) { - TangemBottomSheetDraggableHeader( - color = TangemTheme.colors.background.tertiary, - ) - Box( - modifier = Modifier - .weight(1f) - .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical), - ) { - SendScreenContent(uiState) + val titleRes = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Send, + -> R.string.common_send + SendUiStateType.Recipient -> R.string.send_recipient + SendUiStateType.Fee -> R.string.common_fee_selector_title + SendUiStateType.Done -> null } + val iconRes = when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + -> R.drawable.ic_qrcode_scan_24 + else -> null + } + + AppBarWithBackButtonAndIcon( + text = titleRes?.let { stringResource(it) }, + onBackClick = uiState.clickIntents::onBackClick, + onIconClick = uiState.clickIntents::onQrCodeScanClick, + backIconRes = R.drawable.ic_close_24, + iconRes = iconRes, + backgroundColor = TangemTheme.colors.background.tertiary, + ) + SendScreenContent( + uiState = uiState, + currentState = currentState, + modifier = Modifier + .weight(1f), + ) SendNavigationButtons(uiState) } } @Composable -private fun SendScreenContent(uiState: SendUiState.Content) { - when (uiState) { - is SendUiState.Content.AmountState -> SendAmountContent(uiState) - else -> { /* [REDACTED_TODO_COMMENT]*/ +private fun SendScreenContent( + uiState: SendUiState, + currentState: State, + modifier: Modifier = Modifier, +) { + val recipientList = uiState.recipientList.collectAsLazyPagingItems() + AnimatedContent( + targetState = currentState.value, + label = "Send Scree Navigation", + modifier = modifier, + ) { state -> + when (state) { + SendUiStateType.Amount -> SendAmountContent( + uiState.amountState, + uiState.clickIntents, + ) + SendUiStateType.Recipient -> SendRecipientContent( + uiState.recipientState, + uiState.clickIntents, + recipientList, + ) + else -> { /* [REDACTED_TODO_COMMENT]*/ } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 5cd1c54f34..2fedacc5c1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -18,12 +18,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.OffsetMapping -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.fields.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -140,29 +136,4 @@ private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: M textAlign = TextAlign.Center, ) } -} - -private class AmountVisualTransformation( - private val symbol: String, -) : VisualTransformation { - override fun filter(text: AnnotatedString): TransformedText { - return TransformedText( - buildAnnotatedString { - append(text) - if (text.isNotBlank()) { - append(" ") - append(symbol) - } - }, - object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return text.length - } - - override fun transformedToOriginal(offset: Int): Int { - return text.length - } - }, - ) - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 0bb6d13de0..363392865b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -11,12 +11,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendStates @Composable -internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) { +internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) { + val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle() Column( modifier = modifier .fillMaxWidth() @@ -52,10 +54,10 @@ internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, .align(Alignment.CenterHorizontally), ) AmountField( - sendField = amountState.amountTextField, + sendField = amountTextField.value, + isFiat = amountState.isFiatValue, cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, fiatSymbol = amountState.appCurrency.symbol, - isFiat = amountState.isFiatValue, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt similarity index 75% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 0cc832db0c..d400239936 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui +package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -6,10 +6,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons @@ -18,29 +16,20 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @Composable -internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { +internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) { + if (amountState == null) return Column( modifier = Modifier .background(TangemTheme.colors.background.tertiary), ) { - Text( - text = stringResource(R.string.common_send), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing16) - .align(CenterHorizontally), - ) AmountFieldContainer(amountState = amountState) Row( modifier = Modifier - .fillMaxWidth() .padding( top = TangemTheme.dimens.spacing12, start = TangemTheme.dimens.spacing16, @@ -49,16 +38,16 @@ internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { ) { SegmentedButtons( modifier = Modifier - .height(TangemTheme.dimens.size40) - .weight(1f), + .weight(1f) + .height(TangemTheme.dimens.size40), config = amountState.segmentedButtonConfig, - onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) }, + onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) }, ) { SendAmountCurrencyButton(it) } SecondaryButton( text = stringResource(R.string.send_max_amount), - onClick = amountState.clickIntents::onMaxValueClick, + onClick = clickIntents::onMaxValueClick, size = TangemButtonSize.Text, shape = RoundedCornerShape(TangemTheme.dimens.radius26), modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt new file mode 100644 index 0000000000..ce0fbcee3b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.send.impl.presentation.ui.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.TangemTheme + +/** + * Container for footer info below the text field + * + * @param modifier of component + * @param footer text + * @param footerTopPadding padding between footer and field + * @param content field content + */ +@Composable +internal fun FooterContainer( + modifier: Modifier = Modifier, + footer: String? = null, + footerTopPadding: Dp = TangemTheme.dimens.spacing8, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + content() + footer?.let { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = footerTopPadding), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index 2c3bfff4b4..f7c2cd613f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.components.MiddleEllipsisText import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.res.TangemTheme @@ -28,75 +32,98 @@ import com.tangem.features.send.impl.R * @param subtitle subtitle * @param onClick click listener * @param modifier modifier + * @param info info * @param subtitleIconRes icon */ +@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod") @Composable fun ListItemWithIcon( title: String, subtitle: String, onClick: () -> Unit, modifier: Modifier = Modifier, + info: String? = null, @DrawableRes subtitleIconRes: Int? = null, ) { - Row( + ConstraintLayout( modifier = modifier .fillMaxWidth() - .background(TangemTheme.colors.background.action) .clickable { onClick() } - .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing12, - ), + .padding(horizontal = TangemTheme.dimens.spacing12), ) { + val (iconRef, titleRef, subtitleRef, subtitleIconRef, infoRef) = createRefs() + + val spacing2 = TangemTheme.dimens.spacing2 + val spacing8 = TangemTheme.dimens.spacing8 + val spacing10 = TangemTheme.dimens.spacing10 + val spacing12 = TangemTheme.dimens.spacing12 IdentIcon( address = title, modifier = Modifier .size(TangemTheme.dimens.size40) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), + .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) + .constrainAs(iconRef) { + start.linkTo(parent.start) + top.linkTo(parent.top, margin = spacing8) + bottom.linkTo(parent.bottom, margin = spacing8) + }, ) - Column( + MiddleEllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, modifier = Modifier - .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing2, - bottom = TangemTheme.dimens.spacing2, - ), - ) { - MiddleEllipsisText( - text = title, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Justify, - modifier = Modifier.fillMaxSize(), + .constrainAs(titleRef) { + start.linkTo(iconRef.end, margin = spacing12) + end.linkTo(parent.end) + top.linkTo(parent.top, margin = spacing10) + width = Dimension.fillToConstraints + }, + ) + Icon( + painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .background(TangemTheme.colors.background.tertiary, CircleShape) + .constrainAs(subtitleIconRef) { + start.linkTo(iconRef.end, margin = spacing12) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible + }, + ) + Text( + text = subtitle, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .constrainAs(subtitleRef) { + start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12) + end.linkTo(infoRef.start) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + width = Dimension.fillToConstraints + }, + ) + info?.let { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + modifier = Modifier + .constrainAs(infoRef) { + start.linkTo(subtitleRef.end, goneMargin = spacing12) + end.linkTo(parent.end) + top.linkTo(titleRef.bottom) + bottom.linkTo(parent.bottom, margin = spacing10) + }, ) - Row { - subtitleIconRes?.let { iconRes -> - Icon( - painter = painterResource(id = iconRes), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .background(TangemTheme.colors.background.tertiary, CircleShape) - .padding(TangemTheme.dimens.spacing3), - ) - } - Text( - text = subtitle, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - modifier = Modifier - .then( - if (subtitleIconRes != null) { - Modifier.padding(start = TangemTheme.dimens.spacing4) - } else { - Modifier - }, - ), - ) - } } } } @@ -111,6 +138,7 @@ private fun ListItemWithIconPreview_Light( ListItemWithIcon( title = config.title, subtitle = config.subtitle, + info = config.info, subtitleIconRes = config.iconRes, onClick = {}, ) @@ -126,6 +154,7 @@ private fun ListItemWithIconPreview_Dark( ListItemWithIcon( title = config.title, subtitle = config.subtitle, + info = config.info, subtitleIconRes = config.iconRes, onClick = {}, ) @@ -135,6 +164,7 @@ private fun ListItemWithIconPreview_Dark( private data class ListItemWithIconPreviewConfig( val title: String, val subtitle: String, + val info: String? = null, val iconRes: Int? = null, ) @@ -142,7 +172,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid collection = listOf( ListItemWithIconPreviewConfig( title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - subtitle = "Wallet", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "1 BTC", + info = "0.0.0000 at 00:00", iconRes = R.drawable.ic_arrow_down_24, ), ListItemWithIconPreviewConfig( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt new file mode 100644 index 0000000000..ada2667527 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -0,0 +1,199 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" +private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" +private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY" + +@Composable +internal fun SendRecipientContent( + uiState: SendStates.RecipientState?, + clickIntents: SendClickIntents, + recipientList: LazyPagingItems, +) { + if (uiState == null) return + val address = uiState.addressTextField.collectAsState().value + val memo = uiState.memoTextField?.collectAsState()?.value + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = ADDRESS_FIELD_KEY) { + TextFieldWithPasteAndIcon( + value = address.value, + label = address.label, + placeholder = address.placeholder, + footer = stringResource(R.string.send_recipient_address_footer, uiState.network), + onValueChange = address.onValueChange, + onPasteClick = clickIntents::onRecipientAddressValueChange, + singleLine = true, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), + isError = address.isError, + error = address.error, + ) + } + memo?.let { memoField -> + item(key = MEMO_FIELD_KEY) { + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = memoField.placeholder, + footer = stringResource(R.string.send_recipient_memo_footer), + onValueChange = memoField.onValueChange, + onPasteClick = clickIntents::onRecipientMemoValueChange, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), + isError = memoField.isError, + error = memoField.error, + ) + } + } + recipientListItem( + recipientList = recipientList, + clickIntents = clickIntents, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.recipientListItem( + recipientList: LazyPagingItems, + clickIntents: SendClickIntents, +) { + items( + count = recipientList.itemCount, + key = recipientList.itemKey { + when (it) { + is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY + is SendRecipientListContent.Item -> it.id + } + }, + contentType = recipientList.itemContentType { it::class.java }, + ) { index -> + recipientList[index]?.let { item -> + when (item) { + is SendRecipientListContent.Wallets -> { + RecipientWalletListItem( + item = item, + clickIntents = clickIntents, + modifier = Modifier + .animateItemPlacement() + .padding(top = TangemTheme.dimens.spacing20) + .then( + if (index == 0) { + Modifier.clip( + RoundedCornerShape( + topEnd = TangemTheme.dimens.radius12, + topStart = TangemTheme.dimens.radius12, + ), + ) + } else { + Modifier + }, + ), + ) + } + is SendRecipientListContent.Item -> { + val title = item.title.resolveReference() + ListItemWithIcon( + title = item.title.resolveReference(), + subtitle = item.subtitle.resolveReference(), + info = item.info?.let { ", ${it.resolveReference()}" }, + subtitleIconRes = item.subtitleIconRes, + modifier = Modifier + .then( + if (index == recipientList.itemCount - 1) { + Modifier + .padding(bottom = TangemTheme.dimens.spacing20) + .clip( + RoundedCornerShape( + bottomEnd = TangemTheme.dimens.radius12, + bottomStart = TangemTheme.dimens.radius12, + ), + ) + } else { + Modifier + }, + ) + .background(TangemTheme.colors.background.action), + onClick = { clickIntents.onRecipientAddressValueChange(title) }, + ) + } + } + } + } +} + +@Composable +private fun RecipientWalletListItem( + item: SendRecipientListContent.Wallets, + clickIntents: SendClickIntents, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(top = TangemTheme.dimens.spacing12), + ) { + if (item.list.isNotEmpty()) { + Text( + text = stringResource(R.string.send_recipient_wallets_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing8, + ), + ) + } + item.list.forEachIndexed { _, wallet -> + val title = wallet.title.resolveReference() + ListItemWithIcon( + title = wallet.title.resolveReference(), + subtitle = wallet.subtitle.resolveReference(), + onClick = { clickIntents.onRecipientAddressValueChange(title) }, + ) + } + Text( + text = stringResource(R.string.send_recent_transactions), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt index 1b9118851a..3a2ed57e1a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -23,14 +23,15 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer @Composable internal fun TextFieldWithPasteAndIcon( @@ -42,7 +43,14 @@ internal fun TextFieldWithPasteAndIcon( modifier: Modifier = Modifier, footer: String? = null, singleLine: Boolean = false, + error: TextReference? = null, + isError: Boolean = false, ) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } FooterContainer(modifier, footer) { Column( modifier = Modifier @@ -53,9 +61,9 @@ internal fun TextFieldWithPasteAndIcon( ), ) { Text( - text = label.resolveReference(), + text = title.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, + color = color, modifier = Modifier .padding( start = TangemTheme.dimens.spacing12, @@ -114,7 +122,14 @@ internal fun TextFieldWithPaste( onPasteClick: (String) -> Unit, modifier: Modifier = Modifier, footer: String? = null, + error: TextReference? = null, + isError: Boolean = false, ) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } FooterContainer(modifier, footer) { Row( modifier = Modifier @@ -129,9 +144,9 @@ internal fun TextFieldWithPaste( .padding(TangemTheme.dimens.spacing12), ) { Text( - text = label.resolveReference(), + text = title.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, + color = color, ) SimpleTextField( value = value, @@ -160,6 +175,7 @@ internal fun TextFieldWithInfo( modifier: Modifier = Modifier, info: TextReference? = null, footer: String? = null, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { FooterContainer( footer = footer, @@ -189,6 +205,7 @@ internal fun TextFieldWithInfo( SimpleTextField( value = value, onValueChange = onValueChange, + visualTransformation = visualTransformation, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6) .weight(1f), @@ -208,27 +225,6 @@ internal fun TextFieldWithInfo( } } -@Composable -private fun FooterContainer( - modifier: Modifier = Modifier, - footer: String? = null, - footerTopPadding: Dp = TangemTheme.dimens.spacing8, - content: @Composable () -> Unit, -) { - Column(modifier = modifier) { - content() - footer?.let { - Text( - text = it, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = footerTopPadding), - ) - } - } -} - @Composable private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { val clipboardManager = LocalClipboardManager.current @@ -287,6 +283,7 @@ private fun SimpleTextField( modifier: Modifier = Modifier, placeholder: TextReference? = null, singleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, ) { val focusRequester = remember { FocusRequester() } BasicTextField( @@ -295,6 +292,7 @@ private fun SimpleTextField( textStyle = TangemTheme.typography.body2, cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, + visualTransformation = visualTransformation, decorationBox = { textValue -> Box { if (value.isBlank() && placeholder != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt new file mode 100644 index 0000000000..3dd89596a1 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.impl.presentation.viewmodel + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.Address +import com.tangem.domain.tokens.model.CryptoCurrency + +internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean { + if (address.isEmpty()) return true + val blockchain = cryptoCurrency?.let { + Blockchain.fromId(cryptoCurrency.id.rawNetworkId) + } ?: return false + + return blockchain.validateAddress(address) +} + +internal fun isNotAddressInWallet(walletAddresses: Set
, address: String): Boolean { + return walletAddresses.all { it.value != address } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt new file mode 100644 index 0000000000..a27ce13f23 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt @@ -0,0 +1,46 @@ +package com.tangem.features.send.impl.presentation.viewmodel + +import androidx.core.text.isDigitsOnly +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigInteger + +internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { + if (cryptoCurrency == null) return false + return when (cryptoCurrency.network.id.value) { + Blockchain.XRP.id -> { + val tag = memo.toLongOrNull() + tag != null && tag <= XRP_TAG_MAX_NUMBER + } + Blockchain.Stellar.id -> { + isAssignableValue(memo) + } + else -> true + } +} + +private fun isAssignableValue(value: String): Boolean { + val memoType = when { + value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID + else -> XlmMemoType.TEXT + } + return when (memoType) { + XlmMemoType.TEXT -> { + // from org.stellar.sdk.MemoText + value.toByteArray().size <= XLM_MEMO_MAX_LENGTH + } + XlmMemoType.ID -> { + try { + // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo + value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() + } catch (ex: NumberFormatException) { + false + } + } + } +} + +private enum class XlmMemoType { TEXT, ID } + +private const val XRP_TAG_MAX_NUMBER = 4294967295 +private const val XLM_MEMO_MAX_LENGTH = 28 \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 118d166c69..03740cb51e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -2,13 +2,25 @@ package com.tangem.features.send.impl.presentation.viewmodel interface SendClickIntents { + fun onBackClick() + fun onNextClick() fun onPrevClick() + fun onQrCodeScanClick() + + // region Amount fun onAmountValueChange(value: String) fun onCurrencyChangeClick(isFiat: Boolean) fun onMaxValueClick() + // endregion + + // region Recipient + fun onRecipientAddressValueChange(value: String) + + fun onRecipientMemoValueChange(value: String) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 274feef98e..2bc9bb638a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -4,32 +4,53 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import androidx.paging.PagingData import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.xrp.XrpAddressService +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates +@Suppress("LongParameterList") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -42,26 +63,41 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var innerRouter: StateRouter by Delegates.notNull() + private val stateFactory = SendStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, + walletAddressesProvider = Provider { walletAddresses }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, ) var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set - private var userWallet: UserWallet? = null + private var userWallet: UserWallet by Delegates.notNull() + private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var walletAddresses = emptySet
() private var balanceJobHolder = JobHolder() + private var recipientsJobHolder = JobHolder() + private var walletAddressesJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { + getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) + getWalletsAndRecent() + } + + fun setRouter(router: StateRouter) { + innerRouter = router + uiState = uiState.copy(currentState = router.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet @@ -86,11 +122,12 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { either -> - uiState = stateFactory.getAmountState( - cryptoCurrencyStatus = either, - ) + either.onRight { + cryptoCurrencyStatus = it + uiState = stateFactory.getReadyState() + } } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(balanceJobHolder) } @@ -107,27 +144,81 @@ internal class SendViewModel @Inject constructor( ) } - // region screen state navigation - override fun onNextClick() { - when (uiState) { - is SendUiState.Content.AmountState -> onRecipientStateClick() - is SendUiState.Content.RecipientState -> onFeeStateClick() - else -> { - // todo implement + private fun getWalletsAndRecent() { + combine( + flow = getUserWallets().conflate(), + flow2 = getTxHistory().conflate(), + ) { wallets, txHistory -> + stateFactory.onLoadedRecipientList( + wallets = wallets, + txHistory = txHistory, + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(recipientsJobHolder) + } + + private fun getUserWallets(): Flow> { + return getWalletsUseCase() + .distinctUntilChanged() + .map { userWallets -> + coroutineScope { + userWallets + .filterNot { it.walletId == userWalletId || it.isLocked } + .map { wallet -> + async(dispatchers.io) { + getCryptoCurrenciesUseCase(wallet.walletId) + .fold( + ifRight = { currencyItem -> + val walletCurrency = currencyItem.firstOrNull { + it.network.id == cryptoCurrency.network.id + } ?: return@fold null + val addresses = walletManagersFacade.getAddress( + userWalletId = wallet.walletId, + network = walletCurrency.network, + ) + return@fold AvailableWallet( + name = wallet.name, + address = addresses.first().value, + ) + }, + ifLeft = { null }, + ) + } + } + }.awaitAll() } + } + + private fun getTxHistory(): Flow> { + return flow { + txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifRight = { emitAll(it.distinctUntilChanged()) }, + ifLeft = {}, + ) } } - override fun onPrevClick() { - // todo implement + private fun getWalletAddresses() { + viewModelScope.launch(dispatchers.io) { + walletAddresses = walletManagersFacade.getAddresses( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + }.saveIn(walletAddressesJobHolder) } - private fun onRecipientStateClick() { - stateFactory.getOnReceiveState() - } + // region screen state navigation + override fun onBackClick() = innerRouter.onBackClick() + override fun onNextClick() = innerRouter.onNextClick() + override fun onPrevClick() = innerRouter.onPrevClick() - private fun onFeeStateClick() { - // todo implement + override fun onQrCodeScanClick() { + // TODO Add QR code scanning } // endregion @@ -141,14 +232,44 @@ internal class SendViewModel @Inject constructor( } override fun onMaxValueClick() { - val amountState = uiState as? SendUiState.Content.AmountState ?: return - + val amountState = uiState.amountState ?: return val amount = if (amountState.isFiatValue) { amountState.cryptoCurrencyStatus.value.fiatAmount } else { amountState.cryptoCurrencyStatus.value.amount } - onAmountValueChange(amount?.toPlainString() ?: "0.00") + onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE) } // endregion + + // region recipient state clicks + override fun onRecipientAddressValueChange(value: String) { + if (!checkIfXrpAddressValue(value)) { + uiState = stateFactory.getOnRecipientAddressValueChangeState(value) + } + } + + override fun onRecipientMemoValueChange(value: String) { + if (!checkIfXrpAddressValue(value)) { + uiState = stateFactory.getOnRecipientMemoValueChangeState(value) + } + } + + private fun checkIfXrpAddressValue(value: String): Boolean { + if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) { + viewModelScope.launch(dispatchers.io) { + val result = XrpAddressService.decodeXAddress(value) + onRecipientAddressValueChange(result?.address.orEmpty()) + onRecipientMemoValueChange(result?.destinationTag.toString()) + } + return true + } + return false + } + // endregion + + companion object { + private const val XRP_X_ADDRESS = 'X' + private const val DEFAULT_VALUE = "0.00" + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index 40261aefd2..819d32e1de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -1,23 +1,19 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory -import android.text.format.DateUtils import androidx.paging.* import com.tangem.common.Provider import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState -import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isToday -import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import org.joda.time.DateTime -import org.joda.time.DateTimeZone internal class TokenDetailsTxHistoryItemFlowConverter( private val currentStateProvider: Provider, @@ -101,7 +97,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter( ) { val txContent = txHistoryItemState.state as TransactionState.Content txHistoryItemState.copy( - state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()), ) } else { txHistoryItemState @@ -117,26 +113,4 @@ internal class TokenDetailsTxHistoryItemFlowConverter( null } } - - /** - * If [this] timestamp is today or yesterday, returns relative date, - * otherwise returns formatting date. - */ - private fun Long.toDateFormat(): String { - val localDate = DateTime(this, DateTimeZone.getDefault()) - return if (localDate.isToday() || localDate.isYesterday()) { - DateUtils.getRelativeTimeSpanString( - this, - DateTime.now().millis, - DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() - } else { - DateTimeFormatters.formatDate(date = localDate) - } - } - - private fun String.toTimeFormat(): String { - return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) - } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6de5a3f3ab..86e6d89da1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -34,7 +34,7 @@ compose-navigation = "2.7.4" compose-accompanist = "0.30.1" compose-paging = "3.2.1" compose-reorderable = "0.9.6" -compoese-lifecycle-runtime = "2.6.2" +compose-lifecycle-runtime = "2.6.2" # endregion Compose # region Other libraries @@ -142,7 +142,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } -lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" } +lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } # region AndroidX From 4dc4b544d8646c5f4288caff34917ff930511c69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 14:42:48 +0400 Subject: [PATCH 3/4] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 70 +++++++++------- core/res/src/main/res/values/strings.xml | 80 +++++++++++-------- .../components/buttons/common/TangemButton.kt | 3 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../presentation/common/state/AlertState.kt | 42 ++++++++++ .../presentation/common/state/Event.kt | 8 ++ .../presentation/common/ui/EventEffect.kt | 18 +++++ .../common/ui/components/Alert.kt | 50 ++++++++++++ 8 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 52aa5dfca4..857c2f4ff1 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -11,8 +11,9 @@ Не удалось отправить письмо Причина: %s Не могу отправить транзакцию + Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. - Токены в сети Solana не поддерживаются этой картой из-за ограничений прошивки. + Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem @@ -57,10 +58,7 @@ Настройки карты Tangem Bot Чат - Оценить агента Отправить логи - Пожалуйста, выберите действие - Пожалуйста, оцените работу агента Принять Доступ запрещен Добавить @@ -89,12 +87,12 @@ Обменять Посмотреть историю транзакций Обозреватель - Скорость и комиссия Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции. - Медленно - По рынку - Быстро Свое + Быстро + По рынку + Медленно + Скорость и комиссия Сгенерировать адреса Импортировать Нравится @@ -144,7 +142,10 @@ Обязательное поле Количество знаков после запятой должно быть корректным числом не больше %d Своя деривация + Например m/00\'/0000\'/0\'/0/0 + Введите свою деривацию Знаков после запятой + Путь деривации По умолчанию Деривация по BIP44 Введенный путь деривации некорректен @@ -152,11 +153,15 @@ Название токена Не выбрано Сеть + Сеть + Вы можете добавить токен в ручную, если он не поддерживается Tangem Например, USDC Символ Символ токена Этот токен/сеть уже находится в вашем списке Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Остерегайтесь мошеннических токенов, они могут ничего не стоить + Токены могут быть созданы кем угодно Чат Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа. @@ -182,6 +187,13 @@ Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. Вы получаете Вы отправляете + Мои токены + У вас нет добавленных токенов. Добавьте токены для обмена + Недоступен для обмена с %s + Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами + Выберите провайдера + Лучший курс + Требуется разрешение Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта? @@ -238,7 +250,9 @@ Рыночная капитализация Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать + Кошелек не поддерживает выбранную монету Выберите кошелек + Кошелёк не поддерживает более одной сети Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -392,50 +406,50 @@ Приготовьте свою карту Сумма Вычесть из суммы отправки - Сумма к получению %1$s + Сумма к получению %s + %1$s в %2$s Адрес Код назначения - %1$s в %2$s Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. Tag Memo - Последние - Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов. - Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Включая комиссию Комиссия Низкая Нормальная Приоритетная + Лимит газа Цена газа Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. - Лимит газа - Максимальная сумма Всё + Максимальная сумма + Комиссия не превысит Максимальная cумма комиссии - Комиссия не превысит Сетевая комиссия - Покрытие сетевой комиссии Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции + Покрытие сетевой комиссии + Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса. - Недопустимая сумма - Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению. - Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. - Сумма резерва не может быть менее %1$s. - Пожалуйста, пополните свой баланс, чтобы продолжить. - Увеличение комиссии - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. - Комиссия превышает баланс Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. + Комиссия превышает баланс + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. + Увеличение комиссии + Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению + Недопустимая сумма + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. + Пожалуйста, пополните свой баланс, чтобы продолжить + Сумма резерва не может быть менее %1$s + Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Необязательное QR код содержит информацию о сумме отправки равной %s + Последние Получатель + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки Отправка %s Всего @@ -650,7 +664,7 @@ Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. Некоторые сети недоступны - Это Testnet карта. Она не может обрабатывать транзакции и используется только в целях тестирования и разработки. + Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 09f3ec83d1..b26afb1dea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -11,8 +11,9 @@ Failed to send the email Reason: %s Can\'t send a transaction + The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. - Tokens in Solana network are not supported by this card due to firmware limitation. + Tokens in %1$s network are not supported by this card due to firmware limitation. Are you having difficulty scanning your card? This card is not designed to work with this app Go to settings to enable biometric authentication in the Tangem App @@ -55,10 +56,7 @@ Card Settings Tangem Bot Support - Rate agent Send logs - Please select an action - Please, rate the work of the agent Accept Access denied Add @@ -85,15 +83,15 @@ Enabled Error Exchange - Explore transaction history Explore + Explore transaction history Explorer - Speed and fee - Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority - Slow - Market - Fast + Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority. Custom + Fast + Market + Slow + Speed and fee Generate addresses Import Learn & Earn @@ -162,6 +160,8 @@ Token symbol This token/network has already been added to your list Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + Be aware of adding scam tokens, they can cost nothing + Note that tokens can be created by anyone Chat Access code You will have to submit the correct access code before scanning the card @@ -187,6 +187,13 @@ You have used a card from another wallet. Tap the card associated with this wallet You Receive You Send + My tokens + You don\'t have any added tokens yet. Add tokens via Market to swap + Unavailable for swap from %s + Providers facilitate transactions, ensuring smooth and efficient token exchanges + Choose Provider + Best Rate + Permission Needed The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have @@ -224,6 +231,8 @@ Tokens Add Edit + Couldn’t find this token, you can add it manually + Coin market cap Blockchain the cryptocurrency was initially created Native network Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. @@ -242,7 +251,9 @@ Coin market cap The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote + Wallet Incompatible with selected Coin Choose wallet + The wallet doesn\'t support more than one network You have to set up a single access code to protect all your wallets Protect You can set up an individual access code on each card later @@ -392,10 +403,10 @@ Get your card ready! Amount Subtract from send amount - The recipient will receive %1$s + The recipient will receive %s + %1$s at %2$s Address Destination Tag - %1$s at %2$s Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -408,47 +419,47 @@ Low Normal Priority - Network fee info unreachable Check your network connection + Network fee info unreachable + From **%s** Gas limit Gas Limit is auto-calculated; raise it during network congestion Gas price - Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed. - Maximum amount + Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed Max - Maximum fee amount + Maximum amount Max fee + Maximum fee amount Numbers only for Destination Tag Network fee - Network fee coverage Sending amount will be reduced to cover the selected fee level - Kindly be aware that your transaction may experience delays under specific fee settings + Network fee coverage + Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance. - Invalid amount - The included commission exceeds the transfer amount, leading to a negative value. - The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. - The balance amount must be at least %1$s. - Please top up your balance to continue. - Fee is increased - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. - Fee exceeds balance The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. + Fee exceeds balance + The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. + Fee is increased + The included commission exceeds the transfer amount, leading to a negative value + Invalid amount + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. + Please top up your balance to continue + The balance amount must be at least %1$s + Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible Optional - Please align your QR code with the square to scan it. Ensure you scan %s network address. - Recipient’s address scanned - Sending amount was changed - QR code contains information about the sending amount equal to %s - Change the entered amount? Change Decline + Recipient’s address scanned + Change the entered amount? + QR code contains information about the sending amount equal to %s + Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent Recipient Not a valid address + Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds My wallets - Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens. - A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds. Sending %s Total %1$s and %2$s will be sent @@ -456,6 +467,7 @@ %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address + Transaction sent Buy now I have a promo code… Tangem Wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 8ce113a33f..ae762b2803 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.graphics.Shape @@ -108,7 +109,7 @@ private inline fun RowScope.ButtonContentContainer( if (showProgress) { progressIndicator() } else { - Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Row(horizontalArrangement = Arrangement.Center) { if (buttonIcon is TangemButtonIconPosition.Start) { icon(buttonIcon.iconResId) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index cb2f374b0c..2fc319885d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -55,6 +55,7 @@ data class TangemDimens internal constructor( val size20: Dp = 20.dp, val size24: Dp = 24.dp, val size28: Dp = 28.dp, + val size30: Dp = 30.dp, val size32: Dp = 32.dp, val size34: Dp = 34.dp, val size36: Dp = 36.dp, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt new file mode 100644 index 0000000000..767427f16d --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt @@ -0,0 +1,42 @@ +package com.tangem.managetokens.presentation.common.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.managetokens.impl.R + +internal sealed class AlertState { + + abstract val message: TextReference + + class DefaultAlert( + override val message: TextReference, + ) : AlertState() + + class TokenUnavailable( + val onUpvoteClick: () -> Unit, + ) : AlertState() { + override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description) + val confirmButtonText: TextReference = resourceReference(R.string.common_close) + val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote) + } + + object NonNative : AlertState() { + override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info) + } + + object TokensUnsupported : AlertState() { + override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_message) + } + + object TokensUnsupportedCurve : AlertState() { + override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message) + } + + class TokensUnsupportedBlockchainByCard(val token: String) : AlertState() { + override val message: TextReference = resourceReference( + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, + formatArgs = wrappedList(token), + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt new file mode 100644 index 0000000000..4024d7a4d8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt @@ -0,0 +1,8 @@ +package com.tangem.managetokens.presentation.common.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface Event { + data class ShowAlert(val state: AlertState) : Event +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt new file mode 100644 index 0000000000..0bfc404f35 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt @@ -0,0 +1,18 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.event.StateEvent +import com.tangem.managetokens.presentation.common.state.AlertState +import com.tangem.managetokens.presentation.common.state.Event + +@Composable +internal fun EventEffect(event: StateEvent, onAlertStateSet: (AlertState) -> Unit) { + com.tangem.core.ui.event.EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is Event.ShowAlert -> onAlertStateSet(value.state) + } + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt new file mode 100644 index 0000000000..27b83aca6b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt @@ -0,0 +1,50 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.AlertState + +@Composable +internal fun Alert(state: AlertState, onDismiss: () -> Unit) { + when (state) { + is AlertState.DefaultAlert, + is AlertState.NonNative, + AlertState.TokensUnsupportedCurve, + AlertState.TokensUnsupported, + is AlertState.TokensUnsupportedBlockchainByCard, + -> DefaultAlert(state, onDismiss) + is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) + } +} + +@Composable +private fun DefaultAlert(state: AlertState, onDismiss: () -> Unit) { + BasicDialog( + message = state.message.resolveReference(), + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + ) +} + +@Composable +private fun TokenUnavailableAlert(state: AlertState.TokenUnavailable, onDismiss: () -> Unit) { + BasicDialog( + message = state.message.resolveReference(), + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ), + dismissButton = DialogButton( + title = state.dismissButtonText.resolveReference(), + onClick = { state.onUpvoteClick() }, + ), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file From d214ec6ec64619836104dc7e88569a49e37b08d0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Nov 2023 18:35:58 +0300 Subject: [PATCH 4/4] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/di/domain/TransactionDomainModule.kt | 24 ++ .../components/SettingsSwitchItem.kt | 2 +- core/res/src/main/res/values/strings.xml | 2 +- .../core/ui/components}/TangemSwitch.kt | 2 +- core/ui/src/main/res/drawable/ic_bird_24.xml | 9 + core/ui/src/main/res/drawable/ic_edit_24.xml | 7 + core/ui/src/main/res/drawable/ic_hare_24.xml | 9 + .../src/main/res/drawable/ic_tortoise_24.xml | 9 + .../DefaultWalletManagersFacade.kt | 19 ++ .../walletmanager/WalletManagersFacade.kt | 18 ++ domain/transaction/.gitignore | 1 + domain/transaction/build.gradle.kts | 23 ++ .../domain/transaction/error/GetFeeError.kt | 5 + .../transaction/usecase/GetFeeUseCase.kt | 65 +++++ features/send/impl/build.gradle.kts | 1 + .../presentation/state/SendStateFactory.kt | 43 ++- .../impl/presentation/state/SendUiState.kt | 6 +- .../impl/presentation/state/StateRouter.kt | 17 +- .../state/fee/FeeSelectorState.kt | 27 ++ .../state/fee/SendFeeCustomFieldConverter.kt | 61 ++++ .../state/fee/SendFeeStateConverter.kt | 17 ++ .../send/impl/presentation/ui/SendScreen.kt | 5 + .../presentation/ui/common/FooterContainer.kt | 5 +- .../ui/fee/SendCustomFeeEthereum.kt | 63 ++++ .../ui/fee/SendSpeedAndFeeContent.kt | 86 ++++++ .../presentation/ui/fee/SendSpeedSelector.kt | 271 ++++++++++++++++++ .../presentation/ui/fee/SendSpeedSubtract.kt | 66 +++++ .../presentation/ui/recipient/TextFields.kt | 9 +- .../viewmodel/SendClickIntents.kt | 10 + .../presentation/viewmodel/SendViewModel.kt | 98 +++++++ gradle/dependencies.toml | 2 +- settings.gradle.kts | 1 + 33 files changed, 963 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt rename {app/src/main/java/com/tangem/tap/features/details/ui/common => core/ui/src/main/java/com/tangem/core/ui/components}/TangemSwitch.kt (98%) create mode 100644 core/ui/src/main/res/drawable/ic_bird_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_edit_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_hare_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_tortoise_24.xml create mode 100644 domain/transaction/.gitignore create mode 100644 domain/transaction/build.gradle.kts create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a66725eb7..e3f7a4fcde 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.domain.appTheme.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) implementation(projects.common) implementation(projects.core.analytics) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt new file mode 100644 index 0000000000..5b68512d77 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TransactionDomainModule { + + @Provides + @ViewModelScoped + fun provideGetUseCase( + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): GetFeeUseCase { + return GetFeeUseCase(walletManagersFacade, dispatchers) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 78084743b7..74815a675f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.tap.features.details.ui.common.TangemSwitch +import com.tangem.core.ui.components.TangemSwitch @Composable internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b26afb1dea..6bd6a2445a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -421,7 +421,7 @@ Priority Check your network connection Network fee info unreachable - From **%s** + From Gas limit Gas Limit is auto-calculated; raise it during network congestion Gas price diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 9839280049..c4d41ef91b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.details.ui.common +package com.tangem.core.ui.components import androidx.compose.animation.animateColor import androidx.compose.animation.core.* diff --git a/core/ui/src/main/res/drawable/ic_bird_24.xml b/core/ui/src/main/res/drawable/ic_bird_24.xml new file mode 100644 index 0000000000..234ea4f693 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bird_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_edit_24.xml b/core/ui/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000000..cc2504eb91 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_hare_24.xml b/core/ui/src/main/res/drawable/ic_hare_24.xml new file mode 100644 index 0000000000..e2714dc9d0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hare_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tortoise_24.xml b/core/ui/src/main/res/drawable/ic_tortoise_24.xml new file mode 100644 index 0000000000..8dee215fd1 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tortoise_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index bc1bf00b14..a581d0d2fa 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult @@ -411,6 +412,24 @@ class DefaultWalletManagersFacade( } } + override suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as? TransactionSender)?.getFee( + amount = amount, + destination = destination, + ) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 31345d6f99..8e830e0bdd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,10 +2,13 @@ package com.tangem.domain.walletmanager import arrow.core.Either import com.tangem.blockchain.blockchains.solana.RentProvider +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -143,4 +146,19 @@ interface WalletManagersFacade { network: Network, signedHashes: Int, ): Either + + /** + * Returns fee for transaction + * + * @param amount of transaction + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun getFee( + amount: Amount, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): Result? } \ No newline at end of file diff --git a/domain/transaction/.gitignore b/domain/transaction/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/transaction/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts new file mode 100644 index 0000000000..8da795757a --- /dev/null +++ b/domain/transaction/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.transaction" +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + implementation(projects.core.utils) + + implementation(deps.tangem.blockchain) + + implementation(projects.domain.legacy) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt new file mode 100644 index 0000000000..f08011b2d4 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.transaction.error + +sealed class GetFeeError { + object DataError : GetFeeError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt new file mode 100644 index 0000000000..dee4c3cdce --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -0,0 +1,65 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.math.BigDecimal + +/** + * Use case to get transaction fee + */ +class GetFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val dispatcher: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + amount: BigDecimal, + destination: String, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Flow> { + return flow { + val result = walletManagersFacade.getFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + destination = destination, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + + val maybeFee = when (result) { + is Result.Success -> result.data.right() + else -> GetFeeError.DataError.left() + } + emit(maybeFee) + }.flowOn(dispatcher.io) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 65f362206d..bb59a8e295 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 87ac690720..3bb47678a9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference @@ -12,6 +13,10 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fee.SendFeeCustomFieldConverter +import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter @@ -35,8 +40,13 @@ internal class SendStateFactory( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } - private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + private val customFeeFieldConverter by lazy { + SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + ) + } private val amountStateConverter by lazy { SendAmountStateConverter( @@ -47,13 +57,17 @@ internal class SendStateFactory( cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } - private val recipientStateConverter by lazy { SendRecipientStateConverter( clickIntents = clickIntents, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } + private val feeStateConverter by lazy { + SendFeeStateConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val recipientListStateConverter by lazy { SendRecipientListConverter( @@ -71,7 +85,7 @@ internal class SendStateFactory( fun getReadyState(): SendUiState = currentStateProvider().copy( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), - feeState = SendStates.FeeState(), + feeState = feeStateConverter.convert(Unit), ) //endregion @@ -164,5 +178,28 @@ internal class SendStateFactory( ), ) } + + fun onFeeOnLoadingState() { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Loading + } + } + + fun onFeeOnLoadedState(fees: TransactionFee) { + currentStateProvider().feeState?.feeSelectorState?.update { + FeeSelectorState.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + } + } + //endregion + + //region fee + fun onFeeSelectedState(feeType: FeeType) { + currentStateProvider().feeState?.feeSelectorState?.update { + (it as? FeeSelectorState.Content)?.copy(selectedFee = feeType) ?: it + } + } //endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 6baeb27cfb..207e63064d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -8,6 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList @@ -55,10 +56,13 @@ internal sealed class SendStates { val isPrimaryButtonEnabled: Boolean, ) : SendStates() - // todo [REDACTED_JIRA] /** Fee and speed state */ data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeSelectorState: MutableStateFlow = MutableStateFlow(FeeSelectorState.Empty), + val isSubtract: MutableStateFlow = MutableStateFlow(false), + val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() // todo [REDACTED_JIRA] diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e7526fdf04..e50ae05ae8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -16,15 +16,11 @@ internal class StateRouter( fun onNextClick() { when (currentState.value) { - SendUiStateType.Amount -> { - currentState.update { SendUiStateType.Recipient } - } - SendUiStateType.Recipient -> { - currentState.update { SendUiStateType.Fee } - } - else -> { - // todo implement - } + SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient } + SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Fee -> currentState.update { SendUiStateType.Send } + SendUiStateType.Send -> currentState.update { SendUiStateType.Done } + SendUiStateType.Done -> onBackClick() } } @@ -33,7 +29,8 @@ internal class StateRouter( SendUiStateType.Amount -> onBackClick() SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - else -> onBackClick() + SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } + SendUiStateType.Done -> onBackClick() } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt new file mode 100644 index 0000000000..f920348ac8 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +internal sealed class FeeSelectorState { + + object Loading : FeeSelectorState() + + object Empty : FeeSelectorState() + + data class Content( + val fees: TransactionFee, + val selectedFee: FeeType = FeeType.MARKET, + val customValues: MutableStateFlow> = MutableStateFlow(emptyList()), + ) : FeeSelectorState() +} + +enum class FeeType { + SLOW, + MARKET, + FAST, + CUSTOM, +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt new file mode 100644 index 0000000000..39851a417a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow + +internal class SendFeeCustomFieldConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, +) : Converter>> { + + override fun convert(value: Fee): MutableStateFlow> { + val ethereumFee = value as? Fee.Ethereum ?: return MutableStateFlow(emptyList()) + val appCurrency = appCurrencyProvider() + + val maxFeeFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = ethereumFee.amount.value, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + + return MutableStateFlow( + listOf( + SendTextField.CustomFee( + value = ethereumFee.amount.value.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(0, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(maxFeeFiat), + ), + SendTextField.CustomFee( + value = ethereumFee.gasPrice.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(1, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + ), + SendTextField.CustomFee( + value = ethereumFee.gasLimit.toString(), + onValueChange = { clickIntents.onCustomFeeValueChange(2, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt new file mode 100644 index 0000000000..13a2ebe5c2 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.converter.Converter + +internal class SendFeeStateConverter( + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + override fun convert(value: Unit): SendStates.FeeState { + return SendStates.FeeState( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 93a0f62d57..c8f289df54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -20,6 +20,7 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent +import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @Composable @@ -89,6 +90,10 @@ private fun SendScreenContent( uiState.clickIntents, recipientList, ) + SendUiStateType.Fee -> SendSpeedAndFeeContent( + uiState.feeState, + uiState.clickIntents, + ) else -> { /* [REDACTED_TODO_COMMENT]*/ } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt index ce0fbcee3b..26b442e62a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.common +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -25,9 +26,9 @@ internal fun FooterContainer( ) { Column(modifier = modifier) { content() - footer?.let { + AnimatedVisibility(visible = footer != null) { Text( - text = it, + text = footer.orEmpty(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt new file mode 100644 index 0000000000..a96d8c5f1b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo + +private const val ETHEREUM_UNIT = "GWEI" + +@Composable +internal fun SendCustomFeeEthereum( + customValues: State>, + selectedFee: FeeType, + symbol: String, + modifier: Modifier = Modifier, +) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + + if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = modifier, + ) { + TextFieldWithInfo( + value = fee.value, + label = stringResource(R.string.send_max_fee), + footer = stringResource(R.string.send_max_fee_footer), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasPrice.value, + label = stringResource(R.string.send_gas_price), + footer = stringResource(R.string.send_gas_price_footer), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + TextFieldWithInfo( + value = gasLimit.value, + label = stringResource(R.string.send_gas_limit), + footer = stringResource(R.string.send_gas_limit_footer), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt new file mode 100644 index 0000000000..595d04491a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" +private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { + if (state == null) return + val feeSendState = state.feeSelectorState.collectAsStateWithLifecycle() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding( + horizontal = TangemTheme.dimens.spacing16, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + item( + key = FEE_SELECTOR_KEY, + ) { + SendSpeedSelector( + state = feeSendState, + clickIntents = clickIntents, + ) + } + if (feeSendState.value is FeeSelectorState.Content) { + item( + key = FEE_CUSTOM_KEY, + ) { + AnimatedVisibility( + visible = feeSendState.value is FeeSelectorState.Content, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary), + ) { + val fee = feeSendState.value as FeeSelectorState.Content + val customValues = fee.customValues.collectAsStateWithLifecycle() + SendCustomFeeEthereum( + customValues = customValues, + selectedFee = fee.selectedFee, + symbol = state.cryptoCurrencyStatus.currency.symbol, + modifier = Modifier + .animateItemPlacement(), + ) + } + } + } + item { + val topPadding = (feeSendState.value as? FeeSelectorState.Content)?.let { state -> + if (state.selectedFee != FeeType.CUSTOM) { + TangemTheme.dimens.spacing8 + } else { + TangemTheme.dimens.spacing0 + } + } ?: TangemTheme.dimens.spacing0 + + SendSpeedSubtract( + receivingAmount = state.receivedAmount, + isSubtract = state.isSubtract, + onSelectClick = clickIntents::onSubtractSelect, + modifier = Modifier + .animateItemPlacement() + .padding( + top = topPadding, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt new file mode 100644 index 0000000000..de69e011d6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -0,0 +1,271 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents + +@Suppress("LongMethod") +@Composable +internal fun SendSpeedSelector( + state: State, + clickIntents: SendClickIntents, + modifier: Modifier = Modifier, +) { + FooterContainer( + footer = stringResource(R.string.common_fee_selector_footer), + modifier = modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + when (val selector = state.value) { + FeeSelectorState.Loading -> { + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + SendSpeedSelectorItemLoading() + } + is FeeSelectorState.Content -> { + when (selector.fees) { + is TransactionFee.Choosable -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + amount = TextReference.Str(selector.fees.minimum.amount.value.toString()), + symbol = TextReference.Str(selector.fees.minimum.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.SLOW, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.MARKET, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + amount = TextReference.Str(selector.fees.priority.amount.value.toString()), + symbol = TextReference.Str(selector.fees.priority.amount.currencySymbol), + isSelected = selector.selectedFee == FeeType.FAST, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, + showDivider = selector.fees.normal is Fee.Ethereum, + ) + if (selector.fees.normal is Fee.Ethereum) { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_custom, + iconRes = R.drawable.ic_edit_24, + isSelected = selector.selectedFee == FeeType.CUSTOM, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) }, + showDivider = selector.fees.normal !is Fee.Ethereum, + ) + } + } + is TransactionFee.Single -> { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + isSelected = true, + amount = TextReference.Str(selector.fees.normal.amount.value.toString()), + symbol = TextReference.Str(selector.fees.normal.amount.currencySymbol), + onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, + showDivider = false, + ) + } + } + } + FeeSelectorState.Empty -> Unit + } + } + } +} + +@Composable +private fun SendSpeedSelectorItemLoading() { + Row(modifier = Modifier.fillMaxWidth()) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + start = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size50, + height = TangemTheme.dimens.size12, + ), + ) + SpacerWMax() + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + end = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size90, + height = TangemTheme.dimens.size12, + ), + ) + } +} + +@Composable +private fun SendSpeedSelectorItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + amount: TextReference? = null, + symbol: TextReference? = null, + isSelected: Boolean = false, + showDivider: Boolean = true, +) { + val iconTint by animateColorAsState( + targetValue = if (isSelected) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.informative + }, + label = "Selector icon tint change", + ) + + val textStyle = if (isSelected) { + TangemTheme.typography.subtitle2 + } else { + TangemTheme.typography.body2 + } + + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + if (amount != null && symbol != null) { + SelectorValueContent( + amount = amount, + symbol = symbol, + textStyle = textStyle, + ) + } + } + if (showDivider) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size1) + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary) + .align(Alignment.BottomCenter), + ) + } + } +} + +@Composable +private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { + Text( + text = amount.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier + .weight(1f) + .padding( + start = TangemTheme.dimens.spacing4, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + Text( + text = symbol.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing1, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + +//region preview +@Preview +@Composable +private fun FeeSelectorPreview_Light() { + TangemTheme { + SendSpeedSelectorItemLoading() + } +} + +@Preview +@Composable +private fun FeeSelectorPreview_Dark() { + TangemTheme(isDark = true) { + SendSpeedSelectorItemLoading() + } +} +//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt new file mode 100644 index 0000000000..fac746da5b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt @@ -0,0 +1,66 @@ +package com.tangem.features.send.impl.presentation.ui.fee + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import kotlinx.coroutines.flow.StateFlow + +@Composable +internal fun SendSpeedSubtract( + receivingAmount: StateFlow, + isSubtract: StateFlow, + onSelectClick: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val isSelected = isSubtract.collectAsStateWithLifecycle() + val footer = receivingAmount.collectAsStateWithLifecycle() + + val footerText = if (isSelected.value) { + stringResource(R.string.send_amount_substract_footer, footer.value) + } else { + null + } + + FooterContainer( + footer = footerText, + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + ) { + Text( + text = stringResource(R.string.send_amount_substract), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing12), + ) + TangemSwitch( + checked = isSelected.value, + onCheckedChange = onSelectClick, + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt index 3a2ed57e1a..f2a792f03e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -175,7 +176,9 @@ internal fun TextFieldWithInfo( modifier: Modifier = Modifier, info: TextReference? = null, footer: String? = null, + isSingleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { FooterContainer( footer = footer, @@ -206,6 +209,8 @@ internal fun TextFieldWithInfo( value = value, onValueChange = onValueChange, visualTransformation = visualTransformation, + singleLine = isSingleLine, + keyboardOptions = keyboardOptions, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6) .weight(1f), @@ -284,15 +289,17 @@ private fun SimpleTextField( placeholder: TextReference? = null, singleLine: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { val focusRequester = remember { FocusRequester() } BasicTextField( value = value, onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2, + textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1), cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, decorationBox = { textValue -> Box { if (value.isBlank() && placeholder != null) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 03740cb51e..35df0a6614 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.viewmodel +import com.tangem.features.send.impl.presentation.state.fee.FeeType + interface SendClickIntents { fun onBackClick() @@ -23,4 +25,12 @@ interface SendClickIntents { fun onRecipientMemoValueChange(value: String) // endregion + + // region Fee + fun onFeeSelectorClick(feeType: FeeType) + + fun onCustomFeeValueChange(index: Int, value: String) + + fun onSubtractSelect(value: Boolean) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 2bc9bb638a..3e8bc7477f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -9,13 +9,16 @@ import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -27,7 +30,10 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -37,6 +43,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -50,6 +57,7 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getFeeUseCase: GetFeeUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -84,11 +92,13 @@ internal class SendViewModel @Inject constructor( private var balanceJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() private var walletAddressesJobHolder = JobHolder() + private var feeJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { getWalletAddresses() subscribeOnCurrencyStatusUpdates(owner) getWalletsAndRecent() + getFee() } fun setRouter(router: StateRouter) { @@ -203,6 +213,38 @@ internal class SendViewModel @Inject constructor( } } + private fun getFee() { + viewModelScope.launch(dispatchers.main) { + uiState.currentState + .filter { it == SendUiStateType.Fee } + .onEach { + val amountState = uiState.amountState ?: return@onEach + val recipientState = uiState.recipientState ?: return@onEach + + stateFactory.onFeeOnLoadingState() + getFeeUseCase.invoke( + amount = amountState.amountTextField.value.value.toBigDecimal(), + destination = recipientState.addressTextField.value.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) + .conflate() + .distinctUntilChanged() + .onEach { maybeFee -> + maybeFee.fold( + ifRight = { + stateFactory.onFeeOnLoadedState(it) + }, + ifLeft = { + // TODO add error handling + }, + ) + } + .launchIn(viewModelScope) + }.launchIn(viewModelScope) + }.saveIn(feeJobHolder) + } + private fun getWalletAddresses() { viewModelScope.launch(dispatchers.io) { walletAddresses = walletManagersFacade.getAddresses( @@ -268,6 +310,62 @@ internal class SendViewModel @Inject constructor( } // endregion + //region fee + override fun onFeeSelectorClick(feeType: FeeType) { + stateFactory.onFeeSelectedState(feeType) + updateReceiveAmount() + } + + override fun onCustomFeeValueChange(index: Int, value: String) { + uiState.feeState?.apply { + (feeSelectorState.value as? FeeSelectorState.Content)?.let { feeSelector -> + feeSelector.customValues.update { + it.toMutableList().apply { + set(index, it[index].copy(value = value)) + } + } + updateReceiveAmount() + } + } + } + + override fun onSubtractSelect(value: Boolean) { + uiState.feeState?.isSubtract?.update { value } + if (value) { + updateReceiveAmount() + } + } + + private fun updateReceiveAmount() { + uiState.feeState?.receivedAmount?.update { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = calculateReceiveAmount(), + cryptoCurrency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } + } + + private fun calculateReceiveAmount(): BigDecimal { + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return BigDecimal.ZERO + val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO + + val fee = when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> feeState.customValues.value.firstOrNull()?.value?.let { BigDecimal(it) } + } + } + is TransactionFee.Single -> selectedFee.normal.amount.value + } ?: BigDecimal.ZERO + + return BigDecimal(amount.value).minus(fee) + } + //endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 86e6d89da1..eed86ea134 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-384" +tangemBlockchainSdk = "develop-387" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-310" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds diff --git a/settings.gradle.kts b/settings.gradle.kts index 2bda17e30f..59ac1ba906 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":domain:app-theme") include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") +include(":domain:transaction") // endregion Domain modules