From 2a9a80414d770ee9a218e789de1d24fe90762166 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 16:33:38 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 20 +- .../appbar/AppBarWithBackButtonAndIcon.kt | 8 +- .../AmountVisualTransformation.kt | 2 +- .../transactions/TransactionDoneTitle.kt | 87 ++++ .../core/ui/utils/BigDecimalFormatter.kt | 5 + .../res/drawable/ic_empty_in_process_64.xml | 10 + core/ui/src/main/res/drawable/ic_web_24.xml | 13 + data/card/build.gradle.kts | 4 + .../card/DefaultCardSdkConfigRepository.kt | 13 + domain/card/build.gradle.kts | 4 + .../repository/CardSdkConfigRepository.kt | 10 + .../DefaultWalletManagersFacade.kt | 54 +++ .../walletmanager/WalletManagersFacade.kt | 57 ++- domain/tokens/models/build.gradle.kts | 3 + .../domain/tokens/utils/BigDecimalUtils.kt | 24 ++ domain/transaction/build.gradle.kts | 5 + .../transaction/error/SendTransactionError.kt | 10 + .../usecase/SendTransactionUseCase.kt | 61 +++ features/send/impl/build.gradle.kts | 3 + .../features/send/impl/di/SendRouterModule.kt | 5 +- .../send/impl/navigation/DefaultSendRouter.kt | 12 +- .../send/impl/navigation/InnerSendRouter.kt | 9 + .../send/impl/presentation/SendFragment.kt | 10 + .../presentation/domain/SendNotification.kt | 10 + .../presentation/state/SendStateFactory.kt | 12 +- .../impl/presentation/state/SendUiState.kt | 11 +- .../impl/presentation/state/StateRouter.kt | 55 ++- .../SendRecipientMemoFieldConverter.kt | 1 + .../presentation/ui/SendNavigationButtons.kt | 128 ++++-- .../send/impl/presentation/ui/SendScreen.kt | 14 +- .../presentation/ui/amount/AmountField.kt | 2 +- .../ui/fee/SendCustomFeeEthereum.kt | 88 ++-- .../ui/recipient/SendRecipientContent.kt | 32 +- .../ui/recipient/TextFieldWithPaste.kt | 70 ++++ .../presentation/ui/recipient/TextFields.kt | 385 ------------------ .../impl/presentation/ui/send/SendContent.kt | 193 +++++++++ .../viewmodel/MemoVerification.kt | 1 + .../viewmodel/SendClickIntents.kt | 14 + .../presentation/viewmodel/SendViewModel.kt | 128 +++++- 39 files changed, 1074 insertions(+), 499 deletions(-) rename core/ui/src/main/java/com/tangem/core/ui/components/fields/{ => visualtransformations}/AmountVisualTransformation.kt (93%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt create mode 100644 core/ui/src/main/res/drawable/ic_empty_in_process_64.xml create mode 100644 core/ui/src/main/res/drawable/ic_web_24.xml create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt 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/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/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/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