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 index 5b68512d77..8dfb19bd16 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -1,6 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -15,10 +18,25 @@ internal object TransactionDomainModule { @Provides @ViewModelScoped - fun provideGetUseCase( + fun provideGetFeeUseCase( walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): GetFeeUseCase { return GetFeeUseCase(walletManagersFacade, dispatchers) } + + @Provides + @ViewModelScoped + fun provideSendTransactionUseCase( + isDemoCardUseCase: IsDemoCardUseCase, + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + ): SendTransactionUseCase { + return SendTransactionUseCase( + isDemoCardUseCase = isDemoCardUseCase, + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + + ) + } } \ 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 74815a675f..b584136754 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 @@ -63,6 +63,7 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier checked = item.isChecked, enabled = item.isEnabled, onCheckedChange = item.onCheckedChange, + checkedColor = TangemTheme.colors.icon.accent, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 847c27307f..91dd9a2506 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -13,6 +13,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.* 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 androidx.compose.ui.unit.Dp @@ -162,11 +163,12 @@ fun CardWithIcon( * >Figma component */ @Composable -fun IconWithTitleAndDescription( +internal fun IconWithTitleAndDescription( title: String, - description: String, + description: String?, icon: @Composable () -> Unit, additionalContent: @Composable () -> Unit = {}, + iconBackground: Color = TangemTheme.colors.background.secondary, ) { Row( modifier = Modifier @@ -182,7 +184,7 @@ fun IconWithTitleAndDescription( Box( modifier = Modifier .background( - color = TangemTheme.colors.background.secondary, + color = iconBackground, shape = CircleShape, ) .height(TangemTheme.dimens.size40) @@ -205,12 +207,14 @@ fun IconWithTitleAndDescription( color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, ) - SpacerH4() - Text( - text = description, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) + if (description != null) { + SpacerH4() + Text( + text = description, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } } additionalContent() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index c4d41ef91b..8e0081a633 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = TangemTheme.colors.icon.accent, + checkedColor: Color = TangemTheme.colors.control.checked, uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index b6b858d2cd..d0d3315f20 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -9,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.runtime.Composable 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 @@ -36,6 +37,28 @@ fun WarningCard(title: String, description: String, icon: @Composable (() -> Uni ) } +/** + * A card with a warning icon to the left and title without description shown to the right of it. + * + * @param title title of the warning in bold + * + * @see Figma component + */ +@Composable +fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) { + WarningCardMaterial3Style( + content = { + WarningBody( + title = title, + description = null, + icon = icon, + iconBackground = TangemTheme.colors.button.disabled, + ) + }, + ) +} + /** * [WarningCard], but clickable (with an 'greater then' icon to the left) * @@ -105,7 +128,8 @@ fun RefreshableWaringCard( @Composable private fun WarningBody( title: String, - description: String, + description: String?, + iconBackground: Color = TangemTheme.colors.background.secondary, icon: @Composable (() -> Unit)? = null, additionalContent: @Composable () -> Unit = {}, ) { @@ -119,6 +143,7 @@ private fun WarningBody( contentDescription = null, ) }, + iconBackground = iconBackground, ) } @@ -136,6 +161,20 @@ private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composab } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { + Card( + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + backgroundColor = TangemTheme.colors.button.disabled, + elevation = TangemTheme.dimens.elevation0, + onClick = onClick ?: {}, + enabled = onClick != null, + ) { + content() + } +} + // endregion elements // region Preview 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 index 73509da21f..6151499106 100644 --- 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 @@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon( contentDescription = null, modifier = Modifier .size(size = TangemTheme.dimens.size24) - .clickable { onBackClick() }, + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onBackClick() }, tint = TangemTheme.colors.icon.primary1, ) AnimatedContent( 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/visualtransformations/AmountVisualTransformation.kt similarity index 93% rename from core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 89e4fa09ce..b3f27e218e 100644 --- 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/visualtransformations/AmountVisualTransformation.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.fields +package com.tangem.core.ui.components.fields.visualtransformations import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt new file mode 100644 index 0000000000..d576eafe66 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -0,0 +1,87 @@ +package com.tangem.core.ui.components.transactions + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat + +/** + * Common transaction done screen title + * + * @param titleRes title resource + * @param date transaction timestamp in millis + */ +@Composable +fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_empty_in_process_64), + contentDescription = null, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size64), + ) + Text( + text = stringResource(id = titleRes), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32), + ) + Text( + text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + ) + } +} + +// region Previews +@Preview +@Composable +private fun TransactionDoneTitlePreview_Light() { + TangemTheme { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} + +@Preview +@Composable +private fun TransactionDoneTitlePreview_Dark() { + TangemTheme(isDark = true) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 362f4d95e9..4a014cd824 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -7,18 +7,34 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import com.tangem.core.ui.res.TangemTheme -fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { - val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) +fun Modifier.roundedShapeItemDecoration( + currentIndex: Int, + lastIndex: Int, + addDefaultPadding: Boolean = true, +): Modifier = composed { + val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this val isSingleItem = currentIndex == 0 && lastIndex == 0 when { isSingleItem -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium) } currentIndex == 0 -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip( shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, @@ -27,7 +43,7 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ) } currentIndex == lastIndex -> { - modifierWithHorizontalPadding + modifier .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, @@ -35,6 +51,6 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ), ) } - else -> modifierWithHorizontalPadding + else -> modifier } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index d8e209f0ad..c64c860e28 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.utils +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal import java.math.RoundingMode import java.text.NumberFormat @@ -24,6 +25,10 @@ object BigDecimalFormatter { return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" } + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { + return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals) + } + fun formatFiatAmount( fiatAmount: BigDecimal?, fiatCurrencyCode: String, diff --git a/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml new file mode 100644 index 0000000000..e75b15979e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_web_24.xml b/core/ui/src/main/res/drawable/ic_web_24.xml new file mode 100644 index 0000000000..69e2fc27d9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_web_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 6015c1ed3a..872f5ce751 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -13,6 +13,10 @@ android { dependencies { implementation(deps.androidx.datastore) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index a093b24ba0..97c5e7dc35 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.card import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy @@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository( } override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes + + override fun getCommonSigner(cardId: String?) = CommonSigner( + tangemSdk = sdk, + cardId = cardId, + initialMessage = null, + ) + + override fun isLinkedTerminal() = sdk.config.linkedTerminal + + override fun setLinkedTerminal(isLinked: Boolean?) { + sdk.config.linkedTerminal = isLinked + } } \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 71512156cd..342a410f5a 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -19,4 +19,8 @@ dependencies { implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index 1190a2952d..47d13a5a8c 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card.repository import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.domain.models.scan.ProductType /** @@ -28,4 +29,13 @@ interface CardSdkConfigRepository { /** Check if access code saving is enabled */ fun isAccessCodeSavingEnabled(): Boolean + + /** Get common signer by [cardId] */ + fun getCommonSigner(cardId: String?): CommonSigner + + /** Check if linked terminal is enabled */ + fun isLinkedTerminal(): Boolean? + + /** Set linked terminal by [isLinked] */ + fun setLinkedTerminal(isLinked: Boolean?) } \ No newline at end of file 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 15010e8b52..df99d53034 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.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result @@ -35,6 +36,7 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.math.BigDecimal +import java.util.EnumSet @Suppress("LargeClass") // FIXME: Move to its own module and make internal @@ -431,6 +433,58 @@ class DefaultWalletManagersFacade( ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return walletManager?.validateTransaction(amount, fee) + } + + override suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val txData = walletManager?.createTransaction(amount, fee, destination)?.copy( + extras = null, // todo add memo [[REDACTED_JIRA]] + ) + + return txData + } + + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as TransactionSender).send(txData, signer) + } + 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 8e830e0bdd..f2b89b51a6 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,13 +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.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -19,6 +19,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import java.math.BigDecimal +import java.util.EnumSet // TODO: Move to its own module /** @@ -161,4 +162,54 @@ interface WalletManagersFacade { userWalletId: UserWalletId, network: Network, ): Result? + + /** + * Validates transaction + * + * @param amount of transaction + * @param fee of transaction + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? + + /** + * Creates transaction [TransactionData] + * + * @param amount of transaction + * @param fee of transaction + * @param memo of transaction optional + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + @Suppress("LongParameterList") + suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? + + /** + * Sends transaction + * + * @param txData transaction data + * @param signer card signer + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index e9075cefa0..a354fa4700 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -12,4 +12,7 @@ android { dependencies { implementation(projects.domain.txhistory.models) implementation(projects.core.analytics.models) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt new file mode 100644 index 0000000000..fe93d1d8ec --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ +fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = this, + 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/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 8da795757a..571b6c0f50 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -14,10 +14,15 @@ dependencies { implementation(projects.core.utils) + /** Tangem SDKs */ + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) + implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.demo) + implementation(projects.domain.card) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt new file mode 100644 index 0000000000..26fdceff5a --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +sealed class SendTransactionError { + + object DemoCardError : SendTransactionError() + + data class DataError(val message: String?) : SendTransactionError() + + data class NetworkError(val message: String?) : SendTransactionError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt new file mode 100644 index 0000000000..0a289bfcdc --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet + +class SendTransactionUseCase( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + txData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val signer = cardSdkConfigRepository.getCommonSigner( + userWallet.cardId, + ) + + val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() + if (userWallet.scanResponse.card.isStart2Coin) { + cardSdkConfigRepository.setLinkedTerminal(false) + } + val sendResult = try { + if (isDemoCardUseCase(cardId = userWallet.cardId)) { + SendTransactionError.DemoCardError.left() + } else { + walletManagersFacade.sendTransaction( + txData = txData, + signer = signer, + userWalletId = userWallet.walletId, + network = network, + ).right() + } + } catch (ex: Exception) { + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + SendTransactionError.DataError(ex.message).left() + } + + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult.fold( + ifRight = { result -> + when (result) { + is SimpleResult.Success -> true.right() + is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left() + } + }, + ifLeft = { it.left() }, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt new file mode 100644 index 0000000000..44a0529872 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt @@ -0,0 +1,31 @@ +package com.tangem.managetokens.presentation.common.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal sealed class ChooseWalletState { + data class Choose( + val wallets: ImmutableList, + val selectedWallet: WalletState?, + val onChooseWalletClick: () -> Unit, + val onCloseChoosingWalletClick: () -> Unit, + ) : ChooseWalletState() + + object NoSelection : ChooseWalletState() + + class Warning(val type: ChooseWalletWarning) : ChooseWalletState() { + val message: TextReference + get() = when (type) { + ChooseWalletWarning.SINGLE_CURRENCY -> + TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title) + ChooseWalletWarning.WALLET_INCOMPATIBLE -> + TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain) + } + } +} + +enum class ChooseWalletWarning { + SINGLE_CURRENCY, + WALLET_INCOMPATIBLE, +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt new file mode 100644 index 0000000000..a759836afc --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt @@ -0,0 +1,90 @@ +package com.tangem.managetokens.presentation.common.state + +import androidx.compose.runtime.MutableState +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +/** + * Network item state + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconRes network icon id from resources + */ +internal sealed interface NetworkItemState { + + val name: String + val protocolName: String + val id: String + val blockchain: Blockchain + + val iconRes: Int + get() = when (this) { + is Selectable -> this.iconResId + is Toggleable -> this.iconResId.value + } + + /** + * Network item state that can be added and deleted + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconResId network icon id from resources + * @property isMainNetwork flag that determines if the network is the main network for the token + * @property isAdded flag that determines if the user has saved the network + * @property address contract address + * @property decimals decimal count + * @property onToggleClick lambda be invoked when switch is been toggled + */ + @Suppress("LongParameterList") + class Toggleable( + override val name: String, + override val protocolName: String, + override val id: String, + override val blockchain: Blockchain, + val iconResId: MutableState, + val isMainNetwork: Boolean, + val isAdded: MutableState, + val address: String?, + val decimals: Int?, + val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit, + ) : NetworkItemState { + + /** + * Change toggle state [isAdded]. + * + * It is a hack that helps us to change element of flow + */ + fun changeToggleState() { + val reverseState = !isAdded.value + isAdded.value = reverseState + iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) + } + } + + /** + * Network item state that can be selected + * + * @property name network name + * @property protocolName network protocol name + * @property iconResId network icon id from resources + * @property id network id + * @property blockchain blockchain + * @property onNetworkClick lambda be invoked when network item is been clicked + * + */ + class Selectable( + override val name: String, + override val protocolName: String, + val iconResId: Int, + override val id: String, + override val blockchain: Blockchain, + val onNetworkClick: (NetworkItemState) -> Unit, + ) : NetworkItemState +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt new file mode 100644 index 0000000000..6af48736a4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt @@ -0,0 +1,8 @@ +package com.tangem.managetokens.presentation.common.state + +internal data class WalletState( + val walletId: String, + val artworkUrl: String?, + val walletName: String, + val onSelected: (String) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt new file mode 100644 index 0000000000..f231654b96 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt @@ -0,0 +1,27 @@ +package com.tangem.managetokens.presentation.common.state.previewdata + +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import kotlinx.collections.immutable.persistentListOf + +internal object ChooseWalletStatePreviewData { + + val state: ChooseWalletState.Choose + get() = ChooseWalletState.Choose( + wallets = persistentListOf( + walletState, + walletState.copy(walletId = "2"), + ), + selectedWallet = walletState, + onChooseWalletClick = {}, + onCloseChoosingWalletClick = {}, + ) + + private val walletState: WalletState + get() = WalletState( + walletName = "My wallet", + walletId = "1", + artworkUrl = "", + onSelected = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt new file mode 100644 index 0000000000..d0dab333d8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -0,0 +1,151 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData + +@Composable +internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Box( + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44), + ) { + IconButton( + onClick = state.onCloseChoosingWalletClick, + modifier = Modifier.align(Alignment.CenterStart), + ) { + Icon( + painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + } + Text( + text = stringResource(id = R.string.manage_tokens_wallet_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + ) + } + } + items( + count = state.wallets.count(), + key = { index -> state.wallets[index].walletId }, + ) { index -> + WalletItem( + wallet = state.wallets[index], + selectedWallet = state.selectedWallet, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.wallets.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + } +} + +@Composable +private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clickable { wallet.onSelected(wallet.walletId) } + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size72) + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50), + + model = ImageRequest.Builder(context = LocalContext.current) + .data(wallet.artworkUrl) + .crossfade(enable = true) + .build(), + loading = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + error = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + contentDescription = null, + ) + SpacerW12() + Text( + text = wallet.walletName, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + if (selectedWallet == wallet) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Light() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Dark() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt new file mode 100644 index 0000000000..18de0a171a --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -0,0 +1,157 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkIcon(model = state) + SpacerW(width = TangemTheme.dimens.spacing12) + Text( + text = state.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + SpacerW(width = TangemTheme.dimens.spacing6) + Text( + text = state.protocolName, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier + .weight(1f), + ) + if (state is NetworkItemState.Toggleable) { + TangemSwitch( + onCheckedChange = { + state.onToggleClick(tokenState!!, state) + }, + checked = state.isAdded.value, + ) + } + } +} + +@Composable +internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) { + val isAdded = when (model) { + is NetworkItemState.Selectable -> true + is NetworkItemState.Toggleable -> model.isAdded.value + } + + if (!isAdded) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(CircleShape) + .background(TangemTheme.colors.control.unchecked), + ) + } + Icon( + painter = painterResource(id = model.iconRes), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size36), + tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, + ) + + if (model is NetworkItemState.Toggleable && model.isMainNetwork) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size10) + .clip(CircleShape) + .background(TangemTheme.colors.stroke.transparency), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent), + ) + } + } + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = false) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = true) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +private class NetworkItemStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + decimals = 0, + ), + NetworkItemState.Selectable( + name = "Ethereum", + protocolName = "ETH", + iconResId = R.drawable.img_polygon_22, + id = "", + onNetworkClick = { }, + blockchain = Blockchain.Ethereum, + ), + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt new file mode 100644 index 0000000000..c136cb1e28 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun SimpleSelectionBlock( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + roundedCorners: Boolean = true, +) { + Column( + modifier = modifier + .then( + if (roundedCorners) { + Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) + } else { + Modifier + }, + ) + .background(color = TangemTheme.colors.background.action) + .clickable { onClick() } + .padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) { + Text( + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + SpacerH(height = TangemTheme.dimens.spacing4) + Text( + text = subtitle, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Light() { + TangemTheme(isDark = false) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Dark() { + TangemTheme(isDark = true) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt new file mode 100644 index 0000000000..df2a7f015f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseNetworkState( + val nativeNetworks: ImmutableList, + val nonNativeNetworks: ImmutableList, + val onNonNativeNetworkHintClick: () -> Unit, + val onCloseChooseNetworkScreen: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt index 8856ab58c5..0cfdfa16d1 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt @@ -8,7 +8,8 @@ import com.tangem.features.managetokens.impl.R data class DerivationNotificationState( val totalNeeded: Int, - val missingAddressesCount: Int, + val totalWallets: Int, + val walletsToDerive: Int, val onGenerateClick: () -> Unit, ) { val config = NotificationConfig( @@ -25,8 +26,8 @@ data class DerivationNotificationState( onClick = onGenerateClick, additionalText = pluralReference( id = R.plurals.manage_tokens_number_of_wallets_android, - count = totalNeeded, - formatArgs = wrappedList(missingAddressesCount, totalNeeded), + count = totalWallets, + formatArgs = wrappedList(walletsToDerive, totalWallets), ), ), ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt index ae386cc7c1..7262bbe337 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt @@ -1,5 +1,7 @@ package com.tangem.managetokens.presentation.managetokens.state +import androidx.compose.runtime.MutableState + internal sealed class TokenItemState { abstract val id: String @@ -9,11 +11,13 @@ internal sealed class TokenItemState { data class Loaded( override val id: String, val name: String, - val currencyId: String, + val currencySymbol: String, + val tokenId: String, val tokenIcon: TokenIconState, val quotes: QuotesState, val rate: String?, - val availableAction: TokenButtonType, - val onButtonClick: (String) -> Unit, + val availableAction: MutableState, + val chooseNetworkState: ChooseNetworkState, + val onButtonClick: (Loaded) -> Unit, ) : TokenItemState() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt new file mode 100644 index 0000000000..12be7db588 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.compose.runtime.mutableStateOf +import com.tangem.blockchain.common.Blockchain +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import kotlinx.collections.immutable.toImmutableList + +internal object ChooseNetworkStatePreviewData { + + val state = ChooseNetworkState( + nativeNetworks = nativeNetworks.toImmutableList(), + nonNativeNetworks = nonNativeNetworks.toImmutableList(), + onNonNativeNetworkHintClick = {}, + onCloseChooseNetworkScreen = {}, + ) +} + +internal val nativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), +) + +internal val nonNativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_kusama_22), + isMainNetwork = false, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + address = "", + decimals = 0, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt index 207ea75cb8..b5d88b814a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt @@ -4,8 +4,9 @@ import com.tangem.managetokens.presentation.managetokens.state.DerivationNotific object DerivationNotificationStatePreviewData { val state = DerivationNotificationState( - totalNeeded = 3, - missingAddressesCount = 2, + totalNeeded = 5, + totalWallets = 3, + walletsToDerive = 2, onGenerateClick = {}, ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt index bdfedacd3e..01ed9bdda9 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.state.previewdata +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import com.tangem.managetokens.presentation.managetokens.state.* import kotlinx.collections.immutable.persistentListOf @@ -13,7 +14,8 @@ internal object TokenItemStatePreviewData { get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -21,15 +23,17 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), ), rate = "31 285.72$", - availableAction = TokenButtonType.ADD, + availableAction = mutableStateOf(TokenButtonType.ADD), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) val loadedPriceUp: TokenItemState get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -37,8 +41,9 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), ), rate = "31 285.72$", - availableAction = TokenButtonType.NOT_AVAILABLE, + availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) private val tokenIconState: TokenIconState diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt new file mode 100644 index 0000000000..ebc600073f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -0,0 +1,205 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.WarningCardTitleOnly +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData +import com.tangem.managetokens.presentation.common.ui.components.NetworkItem +import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun ChooseNetworkScreen( + state: TokenItemState.Loaded, + walletState: ChooseWalletState, + modifier: Modifier = Modifier, +) { + val networkState = state.chooseNetworkState + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + } + + when (walletState) { + is ChooseWalletState.Choose -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + SimpleSelectionBlock( + title = stringResource(id = R.string.manage_tokens_network_selector_wallet), + subtitle = walletState.selectedWallet?.walletName ?: "", + onClick = walletState.onChooseWalletClick, + ) + } + } + ChooseWalletState.NoSelection -> Unit + is ChooseWalletState.Warning -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + WarningCardTitleOnly( + title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title), + ) + } + } + } + + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + + item { + if (networkState.nativeNetworks.isNotEmpty()) { + NativeNetworks(networkState = networkState, tokenState = state) + } + } + + if (networkState.nonNativeNetworks.isNotEmpty()) { + item { + NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick) + } + item { + SpacerH(height = TangemTheme.dimens.spacing8) + } + item { + this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state) + } + } + } +} + +@Composable +private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + Column { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + SpacerH(height = TangemTheme.dimens.spacing8) + + networkState.nativeNetworks.forEachIndexed { index, network -> + NetworkItem( + state = network, + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + items( + count = networkState.nonNativeNetworks.count(), + key = { index -> networkState.nonNativeNetworks[index].id }, + ) { index -> + NetworkItem( + state = networkState.nonNativeNetworks[index], + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nonNativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { + Column { + Row { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerW(width = TangemTheme.dimens.spacing2) + Icon( + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable { onNonNativeNetworkHintClick() }, + ) + } + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Light() { + TangemTheme(isDark = false) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Dark() { + TangemTheme(isDark = true) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index 139e4f9652..12a403e3e4 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -54,7 +54,7 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M modifier = Modifier .weight(weight = 1f), ) { - TokenName(name = state.name, currencyId = state.currencyId) + TokenName(name = state.name, currencyId = state.currencySymbol) TokenPriceData(price = state.rate, quotesState = state.quotes) } SpacerW24() @@ -67,8 +67,8 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M } TokenButton( - type = state.availableAction, - onClick = { state.onButtonClick(state.currencyId) }, + type = state.availableAction.value, + onClick = { state.onButtonClick(state) }, ) } } diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index bb59a8e295..1af2bafaee 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Domain modules */ implementation(projects.domain.models) @@ -58,6 +59,8 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.card) + implementation(projects.domain.demo) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 8bf19609d8..7f38cacd35 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.di +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -17,7 +18,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(): SendRouter { - return DefaultSendRouter() + fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { + return DefaultSendRouter(reduxNavController) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 4db1a5a674..c72ca92ae7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -1,9 +1,17 @@ package com.tangem.features.send.impl.navigation import androidx.fragment.app.Fragment -import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.impl.presentation.SendFragment -internal class DefaultSendRouter : SendRouter { +internal class DefaultSendRouter( + private val reduxNavController: ReduxNavController, +) : InnerSendRouter { + override fun getEntryFragment(): Fragment = SendFragment.create() + + override fun openUrl(url: String) { + reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt new file mode 100644 index 0000000000..b7cd7f934a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.impl.navigation + +import com.tangem.features.send.api.navigation.SendRouter + +interface InnerSendRouter : SendRouter { + + /** Open website by [url] */ + fun openUrl(url: String) +} \ No newline at end of file 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 2790caab93..cd47d6c8fe 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 @@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.InnerSendRouter 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 @@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder + @Inject + lateinit var router: SendRouter + private val viewModel by viewModels() + private val innerSendRouter: InnerSendRouter + get() = requireNotNull(router as? InnerSendRouter) { + "innerSendRouter should be instance of InnerSendRouter" + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) viewModel.setRouter( + innerSendRouter, StateRouter( fragmentManager = WeakReference(parentFragmentManager), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt new file mode 100644 index 0000000000..720270d86d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.impl.presentation.domain + +sealed class SendNotification { + + sealed class Info(val message: String) : SendNotification() + + sealed class Critical(val message: String) : SendNotification() + + sealed class Error(val message: String) : SendNotification() +} \ 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 38583d0676..bae35722ae 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 @@ -86,6 +86,7 @@ internal class SendStateFactory( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), feeState = feeStateConverter.convert(Unit), + sendState = SendStates.SendState(), ) //endregion @@ -122,15 +123,15 @@ internal class SendStateFactory( val recipientState = state.recipientState ?: return state val isValidMemo = validateMemo( - memo = value, + memo = recipientState.addressTextField.value.value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) val isAddressInWallet = isNotAddressInWallet( + address = value, walletAddresses = walletAddressesProvider(), - address = recipientState.addressTextField.value.value, ) val isValidAddress = verifyAddress( - address = recipientState.addressTextField.value.value, + address = value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) @@ -138,8 +139,7 @@ internal class SendStateFactory( it.copy( value = value, error = when { - !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error) - !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) + !isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) else -> null }, isError = !isValidAddress || !isAddressInWallet, @@ -169,11 +169,9 @@ internal class SendStateFactory( 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, ) } 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 207e63064d..1d7474d6b4 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 @@ -23,6 +23,7 @@ internal data class SendUiState( val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, + val sendState: SendStates.SendState? = null, val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, ) @@ -65,11 +66,14 @@ internal sealed class SendStates { val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() - // todo [REDACTED_JIRA] /** Send state */ data class SendState( - val isSuccess: Boolean, - ) + override val type: SendUiStateType = SendUiStateType.Send, + val isSending: MutableStateFlow = MutableStateFlow(false), + val isSuccess: MutableStateFlow = MutableStateFlow(false), + val transactionDate: MutableStateFlow = MutableStateFlow(0L), + val txUrl: MutableStateFlow = MutableStateFlow(""), + ) : SendStates() } enum class SendUiStateType { @@ -77,5 +81,4 @@ enum class SendUiStateType { 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 index e50ae05ae8..e92791bba9 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 @@ -9,28 +9,61 @@ internal class StateRouter( private val fragmentManager: WeakReference, ) { var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount) + private set + + private var isFromSend: Boolean = false + + fun popBackStack() { + fragmentManager.get()?.popBackStack() + } fun onBackClick() { - fragmentManager.get()?.popBackStack() + if (isFromSend) { + showSend() + } else { + when (currentState.value) { + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> showFee() + } + } } fun onNextClick() { when (currentState.value) { - 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() + SendUiStateType.Amount -> showRecipient() + SendUiStateType.Recipient -> showFee() + SendUiStateType.Fee -> showSend() + SendUiStateType.Send -> onBackClick() } } fun onPrevClick() { when (currentState.value) { - SendUiStateType.Amount -> onBackClick() - SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } - SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } - SendUiStateType.Done -> onBackClick() + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> popBackStack() } } + + fun showAmount(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Amount } + } + + fun showRecipient(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Recipient } + } + + fun showFee(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Fee } + } + + fun showSend() { + currentState.update { SendUiStateType.Send } + } } \ 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 index d1890038cb..a1c0bd5603 100644 --- 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 @@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter( ), placeholder = TextReference.Res(R.string.send_optional_field), label = TextReference.Res(value), + error = TextReference.Res(R.string.send_memo_destination_tag_error), ), ) } 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 30ca727485..e7c0df4249 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 @@ -1,23 +1,30 @@ package com.tangem.features.send.impl.presentation.ui +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth 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.State import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.shareText 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 @@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) { @Composable private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { - val currentState = uiState.currentState.collectAsState() + val currentState = uiState.currentState.collectAsStateWithLifecycle() + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false + val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false + val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty() - val buttonTextId = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Recipient, - SendUiStateType.Fee, - -> R.string.common_next - SendUiStateType.Send -> R.string.common_send - else -> R.string.common_close - } + val (buttonTextId, buttonClick) = getButtonData( + currentState = currentState, + isSuccess = isSuccess, + uiState = uiState, + ) val isButtonEnabled = when (currentState.value) { SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false @@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier label = "Update send screen state", modifier = modifier, ) { textId -> - if (currentState.value == SendUiStateType.Send) { - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) - } else { - PrimaryButton( - text = stringResource(textId), - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) + when { + currentState.value == SendUiStateType.Send && !isSuccess -> { + PrimaryButtonIconEnd( + text = stringResource(textId), + iconResId = R.drawable.ic_tangem_24, + enabled = isButtonEnabled, + onClick = buttonClick, + showProgress = isSending, + ) + } + currentState.value == SendUiStateType.Send && isSuccess -> { + PrimaryButtonsDone( + textRes = textId, + txUrl = txUrl, + onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) }, + onDoneClick = buttonClick, + modifier = Modifier, + ) + } + else -> { + PrimaryButton( + text = stringResource(textId), + enabled = isButtonEnabled, + onClick = buttonClick, + ) + } } } +} + +@Composable +private fun PrimaryButtonsDone( + @StringRes textRes: Int, + txUrl: String, + onExploreClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + Column(modifier = modifier) { + if (txUrl.isNotBlank()) { + Row { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + }, + modifier = Modifier.weight(1f), + ) + } + SpacerH12() + } + PrimaryButton( + text = stringResource(id = textRes), + enabled = true, + onClick = onDoneClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private fun getButtonData( + uiState: SendUiState, + currentState: State, + isSuccess: Boolean, +): Pair Unit> { + return when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + SendUiStateType.Fee, + -> R.string.common_next to uiState.clickIntents::onNextClick + SendUiStateType.Send -> if (isSuccess) { + R.string.common_close + } else { + R.string.common_send + } to uiState.clickIntents::onSendClick + } } \ 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 c8f289df54..1c73c25a04 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 @@ -22,11 +22,13 @@ 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 +import com.tangem.features.send.impl.presentation.ui.send.SendContent @Composable internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() - BackHandler { uiState.clickIntents.onPrevClick() } + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle() + BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier .fillMaxSize() @@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) { horizontalAlignment = Alignment.CenterHorizontally, ) { val titleRes = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Send, - -> R.string.common_send + SendUiStateType.Amount -> R.string.common_send SendUiStateType.Recipient -> R.string.send_recipient SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Done -> null + SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null } val iconRes = when (currentState.value) { SendUiStateType.Amount, @@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) { AppBarWithBackButtonAndIcon( text = titleRes?.let { stringResource(it) }, - onBackClick = uiState.clickIntents::onBackClick, + onBackClick = uiState.clickIntents::popBackStack, onIconClick = uiState.clickIntents::onQrCodeScanClick, backIconRes = R.drawable.ic_close_24, iconRes = iconRes, @@ -94,7 +94,7 @@ private fun SendScreenContent( uiState.feeState, uiState.clickIntents, ) - else -> { /* [REDACTED_TODO_COMMENT]*/ } + SendUiStateType.Send -> SendContent(uiState) } } } \ 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 2fedacc5c1..f6471c020d 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 @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme 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 index a96d8c5f1b..98b24927f0 100644 --- 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 @@ -1,17 +1,21 @@ 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.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.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.components.inputrow.InputRowEnter +import com.tangem.core.ui.components.inputrow.InputRowEnterInfo +import com.tangem.core.ui.extensions.TextReference 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 +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer private const val ETHEREUM_UNIT = "GWEI" @@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum( 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()) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { - TextFieldWithInfo( - value = fee.value, - label = stringResource(R.string.send_max_fee), + FooterContainer( 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), + ) { + InputRowEnterInfo( + text = fee.value, + title = TextReference.Res(R.string.send_max_fee), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( 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), + ) { + InputRowEnter( + text = gasPrice.value, + title = TextReference.Res(R.string.send_gas_price), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( footer = stringResource(R.string.send_gas_limit_footer), - onValueChange = gasLimit.onValueChange, - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - ) + ) { + InputRowEnter( + text = gasLimit.value, + title = TextReference.Res(R.string.send_gas_limit), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } } } \ No newline at end of file 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 index 30563155b4..24319091e1 100644 --- 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 @@ -18,11 +18,13 @@ 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.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" @@ -45,18 +47,26 @@ internal fun SendRecipientContent( .padding(horizontal = TangemTheme.dimens.spacing16), ) { item(key = ADDRESS_FIELD_KEY) { - TextFieldWithPasteAndIcon( - value = address.value, - label = address.label, - placeholder = address.placeholder, + FooterContainer( 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, - ) + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = address.onValueChange, + onPasteClick = clickIntents::onRecipientAddressValueChange, + singleLine = true, + isError = address.isError, + error = address.error, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } memo?.let { memoField -> item(key = MEMO_FIELD_KEY) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt new file mode 100644 index 0000000000..8b3c83bfb0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.inputrow.inner.PasteButton +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.presentation.ui.common.FooterContainer + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + 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 + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body2, + color = color, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6), + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding(end = TangemTheme.dimens.spacing16), + ) + } + } +} \ 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 deleted file mode 100644 index f2a792f03e..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ /dev/null @@ -1,385 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.recipient - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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 -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -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 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( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - onPasteClick: (String) -> Unit, - 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 - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - ), - ) - Row { - IdentIcon( - address = value, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) - .size(TangemTheme.dimens.size40) - .background(TangemTheme.colors.background.tertiary), - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - singleLine = singleLine, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .weight(1f) - .align(CenterVertically), - ) - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding( - start = TangemTheme.dimens.spacing4, - end = TangemTheme.dimens.spacing16, - ), - ) - } - } - } -} - -@Composable -internal fun TextFieldWithPaste( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - 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 - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Column( - modifier = Modifier - .weight(1f) - .padding(TangemTheme.dimens.spacing12), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6), - ) - } - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding(end = TangemTheme.dimens.spacing16), - ) - } - } -} - -@Composable -internal fun TextFieldWithInfo( - value: String, - label: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - info: TextReference? = null, - footer: String? = null, - isSingleLine: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - FooterContainer( - footer = footer, - footerTopPadding = TangemTheme.dimens.spacing6, - modifier = modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing14, - ), - ) { - Text( - text = label, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Row { - SimpleTextField( - value = value, - onValueChange = onValueChange, - visualTransformation = visualTransformation, - singleLine = isSingleLine, - keyboardOptions = keyboardOptions, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6) - .weight(1f), - ) - info?.let { - Text( - text = it.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .align(Alignment.Bottom), - ) - } - } - } - } -} - -@Composable -private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { - val clipboardManager = LocalClipboardManager.current - val hapticFeedback = LocalHapticFeedback.current - - if (isPasteButtonVisible) { - Box(modifier = modifier) { - Text( - text = "Paste", - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary2, - modifier = Modifier - .background( - color = TangemTheme.colors.button.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - horizontal = TangemTheme.dimens.spacing10, - vertical = TangemTheme.dimens.spacing2, - ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius8), - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onClick( - clipboardManager - .getText() - ?.toString() - .orEmpty(), - ) - }, - ), - ) - } - } else { - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = stringResource(R.string.common_close), - modifier = modifier - .size(TangemTheme.dimens.size20) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius10), - onClick = { onClick("") }, - ), - ) - } -} - -@Composable -private fun SimpleTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - 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.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) { - Text( - text = placeholder.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.disabled, - modifier = Modifier, - ) - } - textValue() - } - }, - modifier = modifier - .focusRequester(focusRequester), - ) -} - -//region preview -@Preview -@Composable -private fun TextFieldPreview_Light() { - TangemTheme { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} - -@Preview -@Composable -private fun TextFieldPreview_Dark() { - TangemTheme(isDark = true) { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} -//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt new file mode 100644 index 0000000000..037913ef69 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -0,0 +1,193 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.toBigDecimalOrDefault +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImage +import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType + +@Suppress("LongMethod") +@Composable +internal fun SendContent(uiState: SendUiState) { + val amountState = uiState.amountState ?: return + val recipientState = uiState.recipientState ?: return + val feeState = uiState.feeState ?: return + val sendState = uiState.sendState ?: return + + val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle() + val timestamp = sendState.transactionDate.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AnimatedVisibility(visible = isSuccess.value) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = timestamp.value, + ) + } + AnimatedVisibility(visible = !isSuccess.value) { + FromWallet( + walletName = amountState.walletName, + walletBalance = amountState.walletBalance, + ) + } + AmountBlock( + amountState = amountState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showAmount, + ) + RecipientBlock( + recipientState = recipientState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showRecipient, + ) + FeeBlock( + feeState = feeState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showFee, + ) + } +} + +@Composable +private fun FromWallet(walletName: String, walletBalance: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.button.disabled) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = buildAnnotatedString { + append(stringResource(R.string.send_from_wallet_android)) + append(" ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(walletName) + } + }, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = walletBalance, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing8, + ), + ) + } +} + +@Composable +private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State, onClick: () -> Unit) { + val amount = amountState.amountTextField.collectAsStateWithLifecycle() + + val cryptoAmount = formatCryptoAmount( + cryptoCurrency = amountState.cryptoCurrencyStatus.currency, + cryptoAmount = amount.value.value.toBigDecimalOrDefault(), + ) + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(), + fiatCurrencyCode = amountState.appCurrency.code, + fiatCurrencySymbol = amountState.appCurrency.symbol, + ) + InputRowImage( + title = TextReference.Res(R.string.send_amount_label), + subtitle = TextReference.Str(cryptoAmount), + caption = TextReference.Str(fiatAmount), + tokenIconState = amountState.tokenIconState, + showNetworkIcon = true, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} + +@Composable +private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State, onClick: () -> Unit) { + val address = recipientState.addressTextField.collectAsStateWithLifecycle() + val memo = recipientState.memoTextField?.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) { + val showMemo = memo != null && memo.value.value.isNotBlank() + InputRowRecipientDefault( + title = TextReference.Res(R.string.send_recipient), + value = address.value.value, + showDivider = showMemo, + ) + if (showMemo) { + InputRowDefault( + title = TextReference.Res(R.string.send_extras_hint_memo), + text = TextReference.Str(memo?.value?.value.orEmpty()), + ) + } + } +} + +@Composable +private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State, onClick: () -> Unit) { + val feeSelector = + feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return + val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0) + + val feeValue = formatCryptoAmount( + cryptoCurrency = feeState.cryptoCurrencyStatus.currency, + cryptoAmount = when (val selectedFee = feeSelector.fees) { + is TransactionFee.Single -> selectedFee.normal.amount.value + is TransactionFee.Choosable -> when (feeSelector.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault() + } + }, + ) + InputRowDefault( + title = TextReference.Res(R.string.send_network_fee_title), + text = TextReference.Str(feeValue), + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} \ 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 index a27ce13f23..ae24d705fd 100644 --- 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 @@ -7,6 +7,7 @@ import java.math.BigInteger internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { if (cryptoCurrency == null) return false + if (memo.isEmpty()) return true return when (cryptoCurrency.network.id.value) { Blockchain.XRP.id -> { val tag = memo.toLongOrNull() 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 35df0a6614..d29ab95bee 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 @@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType interface SendClickIntents { + fun popBackStack() + fun onBackClick() fun onNextClick() @@ -33,4 +35,16 @@ interface SendClickIntents { fun onSubtractSelect(value: Boolean) // endregion + + // region Send + fun onSendClick() + + fun showAmount() + + fun showRecipient() + + fun showFee() + + fun onExploreClick(txUrl: 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 02f2caefc6..4fdc321d9f 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,6 +9,7 @@ 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.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter @@ -18,8 +19,11 @@ 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.tokens.utils.convertToAmount import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,6 +32,7 @@ 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.navigation.InnerSendRouter 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 @@ -48,7 +53,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor( private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getFeeUseCase: GetFeeUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var innerRouter: StateRouter by Delegates.notNull() + private var inneRrouter: InnerSendRouter by Delegates.notNull() + private var stateRouter: StateRouter by Delegates.notNull() private val stateFactory = SendStateFactory( clickIntents = this, @@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor( getFee() } - fun setRouter(router: StateRouter) { - innerRouter = router - uiState = uiState.copy(currentState = router.currentState) + fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { + inneRrouter = router + this.stateRouter = stateRouter + uiState = uiState.copy(currentState = stateRouter.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { @@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor( } // region screen state navigation - override fun onBackClick() = innerRouter.onBackClick() - override fun onNextClick() = innerRouter.onNextClick() - override fun onPrevClick() = innerRouter.onPrevClick() + override fun popBackStack() = stateRouter.popBackStack() + override fun onBackClick() = stateRouter.onBackClick() + override fun onNextClick() = stateRouter.onNextClick() + override fun onPrevClick() = stateRouter.onPrevClick() override fun onQrCodeScanClick() { // TODO Add QR code scanning @@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor( } private fun checkIfXrpAddressValue(value: String): Boolean { - if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) { + if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) { viewModelScope.launch(dispatchers.io) { val result = XrpAddressService.decodeXAddress(value) onRecipientAddressValueChange(result?.address.orEmpty()) @@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor( } //endregion + // region send state clicks + override fun onSendClick() { + val sendState = uiState.sendState ?: return + + if (sendState.isSuccess.value) popBackStack() + sendState.isSending.update { true } + viewModelScope.launch(dispatchers.io) { + verifyAndSendTransaction() + } + } + + private suspend fun verifyAndSendTransaction() { + val sendState = uiState.sendState ?: return + val amount = uiState.amountState?.amountTextField?.value ?: return + val recipient = uiState.recipientState?.addressTextField?.value ?: return + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return + val memo = uiState.recipientState?.memoTextField?.value + val fee = getFee(feeState) ?: return + + val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency) + + // todo add notifications [[REDACTED_JIRA]] + // val transactionErrors = walletManagersFacade.validateTransaction( + // amount = amountToSend, + // fee = fee.amount, + // userWalletId = userWalletId, + // network = cryptoCurrency.network, + // ) + + val txData = walletManagersFacade.createTransaction( + amount = amountToSend, + fee = fee, + memo = memo?.value, + destination = recipient.value, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) ?: return + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + sendState.isSending.update { false } + // todo add notifications [[REDACTED_JIRA]] + }, + ifRight = { + sendState.transactionDate.update { + txData.date?.timeInMillis ?: System.currentTimeMillis() + } + sendState.isSuccess.update { true } + sendState.txUrl.update { + getTxUrl(txData.hash.orEmpty()) + } + }, + ) + } + + private fun getFee(feeState: FeeSelectorState.Content): Fee? { + return when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum + FeeType.MARKET -> selectedFee.normal + FeeType.FAST -> selectedFee.priority + FeeType.CUSTOM -> { + val feeAmount = feeState.customValues.value.firstOrNull()?.value + ?.let { BigDecimal(it) } ?: return null + Fee.Common(feeAmount.convertToAmount(cryptoCurrency)) + } + } + } + is TransactionFee.Single -> selectedFee.normal + } + } + + override fun showAmount() = stateRouter.showAmount(isFromSend = true) + + override fun showRecipient() = stateRouter.showRecipient(isFromSend = true) + + override fun showFee() = stateRouter.showFee(isFromSend = true) + + override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl) + + private fun getTxUrl(hash: String): String { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + // TODO: Fix ton tx urls [REDACTED_TASK_KEY] + return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) { + EMPTY + } else { + getExplorerTransactionUrlUseCase( + txHash = hash, + networkId = cryptoCurrency.network.id, + ) + } + } + // endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" + private const val EMPTY = "" } } \ No newline at end of file