Updated on 2026-08-14
This commit is contained in:
commit
8dec159cd3
126 changed files with 3168 additions and 996 deletions
|
|
@ -62,6 +62,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.libs.auth)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import androidx.lifecycle.flowWithLifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
|
|
@ -36,6 +36,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
|||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
|
|
@ -57,8 +58,6 @@ import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsL
|
|||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.features.intentHandler.IntentProcessor
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import com.tangem.tap.features.main.MainViewModel
|
||||
import com.tangem.tap.features.main.model.Toast
|
||||
|
|
@ -138,6 +137,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var qrScanningRouter: QrScanningRouter
|
||||
|
||||
@Inject
|
||||
lateinit var deepLinksRegistry: DeepLinksRegistry
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
||||
|
|
@ -167,6 +169,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
checkForNotificationPermission()
|
||||
observeStateUpdates()
|
||||
|
||||
if (intent != null) {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeStateUpdates() {
|
||||
|
|
@ -293,8 +299,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true }
|
||||
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
|
||||
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
|
||||
intentProcessor.addHandler(BuyCurrencyIntentHandler())
|
||||
intentProcessor.addHandler(SellCurrencyIntentHandler())
|
||||
}
|
||||
|
||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||
|
|
@ -332,6 +336,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lifecycleScope.launch {
|
||||
intentProcessor.handleIntent(intent)
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showSnackbar(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -20,6 +21,11 @@ internal object LegacyMiddleware {
|
|||
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup),
|
||||
)
|
||||
}
|
||||
is LegacyAction.SendEmailTransactionFailed -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(
|
||||
SendTransactionFailedEmail(action.errorMessage),
|
||||
)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,4 +310,22 @@ internal object TokensDomainModule {
|
|||
): GetNetworksSupportedByWallet {
|
||||
return GetNetworksSupportedByWallet(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetBalanceNotEnoughForFeeWarningUseCase {
|
||||
return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideIsAmountSubtractAvailableUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): IsAmountSubtractAvailableUseCase {
|
||||
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
|||
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
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,24 +19,21 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetFeeUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade, dispatchers)
|
||||
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
transactionRepository = transactionRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.tap.features.intentHandler.handlers
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BuyCurrencyIntentHandler : IntentHandler {
|
||||
|
||||
override fun handleIntent(intent: Intent?): Boolean {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// val data = intent?.data ?: return false
|
||||
// val currency = store.state.walletState.selectedCurrency ?: return false
|
||||
//
|
||||
// val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
|
||||
// return if (data.host == successUri.host && data.authority == successUri.authority) {
|
||||
// val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
|
||||
// Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
|
||||
// true
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.tap.features.intentHandler.handlers
|
||||
|
||||
import android.content.Intent
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SellCurrencyIntentHandler : IntentHandler {
|
||||
|
||||
override fun handleIntent(intent: Intent?): Boolean {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// return try {
|
||||
// val intentData = intent?.data ?: return false
|
||||
// val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
|
||||
// val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
|
||||
// val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
|
||||
// val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
|
||||
//
|
||||
// Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
|
||||
// store.dispatchOnMain(
|
||||
// TradeCryptoAction.SendCrypto(
|
||||
// currencyId = currency,
|
||||
// amount = amount,
|
||||
// destinationAddress = destinationAddress,
|
||||
// transactionId = transactionID,
|
||||
// ),
|
||||
// )
|
||||
// true
|
||||
// } catch (exception: Exception) {
|
||||
// Timber.d("Not MoonPay URL")
|
||||
// false
|
||||
// }
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// private companion object {
|
||||
// private const val TRANSACTION_ID_PARAM = "transactionId"
|
||||
// private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
|
||||
// private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
|
||||
// private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
|
||||
// }
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.tap.domain.TapError
|
|||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -50,19 +51,18 @@ object TradeCryptoMiddleware {
|
|||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
when (action) {
|
||||
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen()
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||
is TradeCryptoAction.New.Swap -> openSwap(
|
||||
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
|
||||
is TradeCryptoAction.Sell -> proceedSellAction(action)
|
||||
is TradeCryptoAction.Swap -> openSwap(
|
||||
currency = action.cryptoCurrency,
|
||||
)
|
||||
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
|
||||
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
|
||||
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
|
||||
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
|
||||
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
|
|
@ -119,7 +119,7 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
|
||||
private fun proceedSellAction(action: TradeCryptoAction.Sell) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
|
|
@ -139,33 +139,6 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun preconfigureAndOpenSendScreen() = scope.launch {
|
||||
// FIXME: [REDACTED_JIRA]
|
||||
// val selectedWalletData = store.state.walletState.selectedWalletData ?: return
|
||||
//
|
||||
// Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
|
||||
// val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard {
|
||||
// FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null"))
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// store.dispatchOnMain(
|
||||
// PrepareSendScreen(
|
||||
// walletManager = walletManager,
|
||||
// coinAmount = walletManager.wallet.amounts[AmountType.Coin],
|
||||
// coinRate = selectedWalletData.fiatRate,
|
||||
// ),
|
||||
// )
|
||||
// store.dispatchOnMain(
|
||||
// SendAction.SendSpecificTransaction(
|
||||
// sendAmount = action.amount,
|
||||
// destinationAddress = action.destinationAddress,
|
||||
// transactionId = action.transactionId,
|
||||
// ),
|
||||
// )
|
||||
// store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
|
||||
}
|
||||
|
||||
private fun openReceiptUrl(transactionId: String) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
|
||||
|
|
@ -182,7 +155,7 @@ object TradeCryptoMiddleware {
|
|||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
|
||||
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
|
||||
val currency = action.tokenCurrency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
||||
|
|
@ -221,6 +194,17 @@ object TradeCryptoMiddleware {
|
|||
),
|
||||
)
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
|
|
@ -229,7 +213,7 @@ object TradeCryptoMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) {
|
||||
private fun handleSendCoin(action: TradeCryptoAction.SendCoin) {
|
||||
val cryptoStatus = action.coinStatus
|
||||
val currency = cryptoStatus.currency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
|
@ -278,6 +262,17 @@ object TradeCryptoMiddleware {
|
|||
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
|
||||
}
|
||||
|
||||
val txInfo = action.transactionInfo
|
||||
if (txInfo != null) {
|
||||
store.dispatchOnMain(
|
||||
SendAction.SendSpecificTransaction(
|
||||
sendAmount = txInfo.amount,
|
||||
destinationAddress = txInfo.destinationAddress,
|
||||
transactionId = txInfo.transactionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val bundle = bundleOf(
|
||||
SendRouter.CRYPTO_CURRENCY_KEY to currency,
|
||||
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -13,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
|
|||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensRouter
|
||||
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ data class ExpressErrorValue(
|
|||
@Json(name = "minAmount")
|
||||
val minAmount: String?,
|
||||
|
||||
@Json(name = "maxAmount")
|
||||
val maxAmount: String?,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int?,
|
||||
|
||||
|
|
|
|||
1
core/deep-links/.gitignore
vendored
Normal file
1
core/deep-links/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
core/deep-links/build.gradle.kts
Normal file
23
core/deep-links/build.gradle.kts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Libs - AndroidX */
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/* Libs - Other */
|
||||
implementation(deps.timber)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
1
core/deep-links/global/.gitignore
vendored
Normal file
1
core/deep-links/global/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
core/deep-links/global/build.gradle.kts
Normal file
15
core/deep-links/global/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.deeplink.global"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project */
|
||||
implementation(projects.core.deepLinks)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink {
|
||||
|
||||
override val uri: String = "tangem://success.tangem.com"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
onReceive()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.core.deeplink.global
|
||||
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
|
||||
class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
||||
|
||||
override val uri: String = "tangem://sell-request.tangem.com"
|
||||
|
||||
override fun onReceive(params: Map<String, String>) {
|
||||
val data = Data(
|
||||
transactionId = params["transactionId"] ?: return,
|
||||
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
||||
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
||||
)
|
||||
|
||||
onReceive(data)
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val transactionId: String,
|
||||
val baseCurrencyAmount: String,
|
||||
val depositWalletAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
/**
|
||||
* Represents a deep link.
|
||||
*/
|
||||
interface DeepLink {
|
||||
|
||||
/**
|
||||
* ID of the deep link.
|
||||
*
|
||||
* By default, it is the same as the [uri].
|
||||
* */
|
||||
val id: String get() = uri
|
||||
|
||||
/**
|
||||
* URI of the deep link.
|
||||
*
|
||||
* **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.**
|
||||
*
|
||||
* Query parameters will be received automatically.
|
||||
*
|
||||
* Path parameters can be added using the following syntax:
|
||||
* ```kotlin
|
||||
* "tangem://link" // Without parameters
|
||||
* "tangem://link/{param1}/{param2}" // With path parameters
|
||||
* ```
|
||||
* */
|
||||
val uri: String
|
||||
|
||||
/**
|
||||
* Method to be called when this deep link is received.
|
||||
*
|
||||
* @param params Map of parameters received from the deep link.
|
||||
* */
|
||||
fun onReceive(params: Map<String, String>)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
// TODO: Add tests
|
||||
/**
|
||||
* Provides functionality to handle deep links.
|
||||
*
|
||||
* Allows deep links to be launched, registered, or unregistered.
|
||||
*/
|
||||
interface DeepLinksRegistry {
|
||||
|
||||
/**
|
||||
* Finds matches registered deep links for the given [intent] and launches them.
|
||||
*
|
||||
* @return `true` if any deep link was received, `false` otherwise.
|
||||
*/
|
||||
fun launch(intent: Intent): Boolean
|
||||
|
||||
/**
|
||||
* Registers the given [deepLink].
|
||||
*
|
||||
* @see registerWithLifecycle
|
||||
* @see registerWithViewModel
|
||||
*/
|
||||
fun register(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Registers the given [deepLinks].
|
||||
*
|
||||
* @see registerWithLifecycle
|
||||
* @see registerWithViewModel
|
||||
*/
|
||||
fun register(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLinks].
|
||||
*/
|
||||
fun unregister(deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Unregisters the given [deepLink].
|
||||
*/
|
||||
fun unregister(deepLink: DeepLink)
|
||||
|
||||
/**
|
||||
* Unregisters deep links with the given [ids].
|
||||
* */
|
||||
fun unregisterByIds(ids: Collection<String>)
|
||||
|
||||
/**
|
||||
* Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is
|
||||
* stopped.
|
||||
*/
|
||||
fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>)
|
||||
|
||||
/**
|
||||
* Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed.
|
||||
*/
|
||||
fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.deeplink.di
|
||||
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object DeepLinksModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDeepLinksRegistry(): DeepLinksRegistry {
|
||||
return DefaultDeepLinksRegistry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package com.tangem.core.deeplink.impl
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
|
||||
|
||||
private var registries: List<DeepLink> = emptyList()
|
||||
|
||||
override fun launch(intent: Intent): Boolean {
|
||||
val received = intent.data ?: return false
|
||||
var hasMatch = false
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Received deep link intent
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
registries.forEach { deepLink ->
|
||||
val expected = deepLink.uri.toUri()
|
||||
|
||||
if (!isMatches(expected, received)) return@forEach
|
||||
hasMatch = true
|
||||
|
||||
val params = getParams(expected, received)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Matched deep link
|
||||
|- Expected URI: $expected
|
||||
|- Received URI: $received
|
||||
|- Params: $params
|
||||
""".trimIndent(),
|
||||
)
|
||||
deepLink.onReceive(params)
|
||||
}
|
||||
|
||||
if (!hasMatch) {
|
||||
Timber.d(
|
||||
"""
|
||||
No match found for deep link
|
||||
|- Received URI: $received
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
return hasMatch
|
||||
}
|
||||
|
||||
override fun register(deepLinks: Collection<DeepLink>) {
|
||||
registries = (registries + deepLinks).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun register(deepLink: DeepLink) {
|
||||
registries = (registries + deepLink).distinctBy(DeepLink::id)
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Registered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLinks: Collection<DeepLink>) {
|
||||
registries = registries.filter { it !in deepLinks }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregister(deepLink: DeepLink) {
|
||||
registries = registries.filter { it.id != deepLink.id }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep link
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregisterByIds(ids: Collection<String>) {
|
||||
registries = registries.filter { it.id !in ids }
|
||||
|
||||
Timber.d(
|
||||
"""
|
||||
Unregistered deep links
|
||||
|- Registries: $registries
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>) {
|
||||
val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks)
|
||||
owner.lifecycle.addObserver(observer)
|
||||
}
|
||||
|
||||
override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>) {
|
||||
viewModel.addCloseable {
|
||||
unregister(deepLinks)
|
||||
}
|
||||
|
||||
register(deepLinks)
|
||||
}
|
||||
|
||||
private fun isMatches(received: Uri, expected: Uri): Boolean {
|
||||
if (received == expected) return true
|
||||
if (received.authority != expected.authority ||
|
||||
received.pathSegments.size != expected.pathSegments.size
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
!(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}"))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getParams(received: Uri, expected: Uri): Map<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
received.pathSegments.forEachIndexed { index, receivedSegment ->
|
||||
val expectedSegment = expected.pathSegments[index]
|
||||
if (receivedSegment != expectedSegment &&
|
||||
receivedSegment.startsWith(prefix = "{") &&
|
||||
receivedSegment.endsWith(suffix = "}")
|
||||
) {
|
||||
val path = receivedSegment
|
||||
.replace(oldValue = "{", newValue = "")
|
||||
.replace(oldValue = "}", newValue = "")
|
||||
|
||||
params[path] = expectedSegment
|
||||
}
|
||||
}
|
||||
|
||||
expected.queryParameterNames.forEach { paramName ->
|
||||
expected.getQueryParameter(paramName)?.let { param ->
|
||||
params[paramName] = param
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.core.deeplink.utils
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.tangem.core.deeplink.DeepLink
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
|
||||
internal class DeepLinksLifecycleObserver(
|
||||
private val deepLinksRegistry: DeepLinksRegistry,
|
||||
private val deepLinks: Collection<DeepLink>,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
deepLinksRegistry.register(deepLinks)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
deepLinksRegistry.unregister(deepLinks)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Erhalt der Gebühr fehlgeschlagen</string>
|
||||
<string name="no_account_bnb">Senden Sie Geld an diese Andresse um ein Konto zu erstellen</string>
|
||||
<string name="send_error_dust_amount_format">Minimaler Betrag ist %s</string>
|
||||
<string name="send_error_dust_change">Restbestand zu klein</string>
|
||||
<string name="send_error_invalid_fee_value">Falsche Gebühr</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Échec de réception des commissions</string>
|
||||
<string name="no_account_bnb">Pour créer un compte, envoyez des fonds monétaires à cette adresse</string>
|
||||
<string name="send_error_dust_amount_format">Le montant minimal est de %s</string>
|
||||
<string name="send_error_dust_change">Le reste est trop petit</string>
|
||||
<string name="send_error_invalid_fee_value">Commission non valide</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Impossibile ottenere la commissione</string>
|
||||
<string name="no_account_bnb">Per creare un account, invia fondi a questo indirizzo</string>
|
||||
<string name="send_error_dust_amount_format">L\'importo minimo è di %s</string>
|
||||
<string name="send_error_dust_change">L\'importo residuo è molto basso</string>
|
||||
<string name="send_error_invalid_fee_value">Commissione non valida</string>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
<string name="common_utxo_validate_withdrawal_message_warning">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="eth_gas_required_exceeds_allowance">Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт.</string>
|
||||
<string name="generic_error_code">Произошла ошибка. Код: %s.</string>
|
||||
<string name="kaspa_withdrawal_message_warning">Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе.</string>
|
||||
<string name="no_account_bnb">Для создания аккаунта отправьте средства на этот адрес</string>
|
||||
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе</string>
|
||||
<string name="no_account_polkadot">Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.</string>
|
||||
<string name="send_error_dust_amount_format">Минимальная сумма: %s</string>
|
||||
<string name="send_error_dust_change">Сдача слишком мала</string>
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@
|
|||
<string name="common_transfer">Перевод</string>
|
||||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_unreachable">Недоступно</string>
|
||||
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
||||
<string name="common_yes">Да</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||
|
|
@ -231,6 +232,7 @@
|
|||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</string>
|
||||
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||
<string name="express_provider_max_amount">Доступно до %s</string>
|
||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="express_terms_of_use">Условиями использования</string>
|
||||
|
|
@ -448,8 +450,9 @@
|
|||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_amount_substract">Вычесть из суммы отправки</string>
|
||||
<string name="send_amount_substract_footer">Сумма к получению %s</string>
|
||||
<string name="send_alert_button_request_support">Поддержка</string>
|
||||
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\Код:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
||||
<string name="send_date_format">%1$s в %2$s</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_tag_field">Код назначения</string>
|
||||
|
|
@ -477,8 +480,10 @@
|
|||
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
|
||||
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
|
||||
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
|
||||
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Оставить %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">Отправить все</string>
|
||||
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
|
||||
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
|
||||
|
|
@ -528,7 +533,6 @@
|
|||
<string name="swapping_approve_information_title">Подтвердить</string>
|
||||
<string name="swapping_error_wrapper">Ошибка: %s</string>
|
||||
<string name="swapping_from_title">Вы отправляете</string>
|
||||
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
|
||||
<string name="swapping_give_permission">Дать разрешение</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
|
|
@ -690,8 +694,9 @@
|
|||
<string name="warning_express_not_enough_fee_for_token_tx_description">Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Невозможно покрыть комиссию %s</string>
|
||||
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
|
||||
<string name="warning_express_too_maximum_amount_title">Сумма для обмена должна быть не более %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
||||
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
||||
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<string name="address_type_legacy">遺留資產</string>
|
||||
<string name="common_fee_error">獲取費用失敗</string>
|
||||
<string name="kaspa_withdrawal_message_warning">由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。</string>
|
||||
<string name="no_account_bnb">要創建帳戶,請將資金發送到此地址</string>
|
||||
<string name="no_account_polkadot">目標帳戶未激活。發送 %s 或更多以激活帳戶</string>
|
||||
<string name="send_error_dust_amount_format">最小數量是 %s</string>
|
||||
<string name="send_error_dust_change">更動太小</string>
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@
|
|||
<string name="swapping_approve_information_text">批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。</string>
|
||||
<string name="swapping_approve_information_title">批准</string>
|
||||
<string name="swapping_error_wrapper">錯誤: %s</string>
|
||||
<string name="swapping_generic_error">有錯誤。請再試一遍</string>
|
||||
<string name="swapping_give_permission">賦予權限</string>
|
||||
<string name="swapping_high_price_impact_description">在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量</string>
|
||||
<string name="swapping_insufficient_funds">餘額不足</string>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
<string name="eth_gas_required_exceeds_allowance">Not enough funds for the transaction. Please top up your account.</string>
|
||||
<string name="generic_error_code">An error occurred. Code: %s.</string>
|
||||
<string name="kaspa_withdrawal_message_warning">Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount.</string>
|
||||
<string name="no_account_generic">To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely.</string>
|
||||
<string name="no_account_bnb">To create account send funds to this address</string>
|
||||
<string name="no_account_generic">To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely</string>
|
||||
<string name="no_account_polkadot">Destination account is not active. Send %s or more to activate the account.</string>
|
||||
<string name="send_error_dust_amount_format">Minimum amount is %s</string>
|
||||
<string name="send_error_dust_change">Change is too small</string>
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@
|
|||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_unreachable">Unreachable</string>
|
||||
<string name="common_unknown_error">There was an error. Please try again.</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
|
|
@ -234,6 +235,7 @@
|
|||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
<string name="express_provider_max_amount">Available up to %s</string>
|
||||
<string name="express_provider_not_available">Unavailable for this pair</string>
|
||||
<string name="express_provider_permission_needed">Permission Required</string>
|
||||
<string name="express_terms_of_use">Terms of Use</string>
|
||||
|
|
@ -447,8 +449,9 @@
|
|||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_amount_substract">Subtract from send amount</string>
|
||||
<string name="send_amount_substract_footer">The recipient will receive %s</string>
|
||||
<string name="send_alert_button_request_support">Support</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode:%2$s</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_confirm_label">Confirm</string>
|
||||
<string name="send_date_format">%1$s at %2$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
|
|
@ -482,8 +485,10 @@
|
|||
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
|
||||
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
|
||||
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_high_fee_title">Fee is increased</string>
|
||||
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
|
||||
<string name="send_notification_fee_too_high_accept">Reduce by %s XTZ</string>
|
||||
<string name="send_notification_fee_too_high_ignore">No, send all</string>
|
||||
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
|
||||
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
|
||||
|
|
@ -519,6 +524,7 @@
|
|||
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
|
||||
<string name="send_validation_invalid_address">Invalid address</string>
|
||||
<string name="sent_transaction_sent_title">Transaction sent</string>
|
||||
<string name="send_wallet_balance_format">%s (%s)</string>
|
||||
<string name="shop_buy_now">Buy now</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
|
|
@ -545,7 +551,6 @@
|
|||
<string name="swapping_approve_information_title">Approve</string>
|
||||
<string name="swapping_error_wrapper">Error: %s</string>
|
||||
<string name="swapping_from_title">You swap</string>
|
||||
<string name="swapping_generic_error">There was an error. Please try again.</string>
|
||||
<string name="swapping_give_permission">Give Permission</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
|
|
@ -705,8 +710,9 @@
|
|||
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
|
||||
<string name="warning_express_refresh_required_title">Service temporarily unavailable</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_wrong_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_too_minimal_amount_title">The amount to swap must be at least %s</string>
|
||||
<string name="warning_express_too_maximum_amount_title">The amount of tokens to be swapped must not exceed %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
||||
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
||||
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
import androidx.compose.ui.Alignment.Companion.TopStart
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.*
|
||||
import java.text.DecimalFormat
|
||||
|
||||
/**
|
||||
* Simple text field for amount input.
|
||||
* Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation].
|
||||
* Can display aligned placeholder and currency symbol [symbol].
|
||||
*
|
||||
* @param value initial text
|
||||
* @param decimals number of decimal places
|
||||
* @param onValueChange callback
|
||||
* @param textStyle text and placeholder styles
|
||||
* @param modifier modifier
|
||||
* @param symbol currency symbol
|
||||
* @param color text color
|
||||
* @param placeholderAlignment alignment of placeholder
|
||||
* @param showPlaceholder show placeholder
|
||||
* @param keyboardOptions keyboard options
|
||||
*
|
||||
* @see [SimpleTextField] for standard text field
|
||||
*/
|
||||
@Composable
|
||||
fun AmountTextField(
|
||||
value: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
placeholderAlignment: Alignment = TopStart,
|
||||
showPlaceholder: Boolean = true,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
|
||||
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
|
||||
TextAlign.Center
|
||||
} else {
|
||||
TextAlign.Start
|
||||
}
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
if (decimalFormat.isValidSymbols(newText)) {
|
||||
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
|
||||
onValueChange(trimmed)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = textStyle,
|
||||
color = color,
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank() && showPlaceholder) {
|
||||
val placeholder = if (symbol != null) {
|
||||
decimalFormat.defaultFormat().plus(" $symbol")
|
||||
} else {
|
||||
decimalFormat.defaultFormat()
|
||||
}
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = placeholderTextAlign,
|
||||
modifier = Modifier
|
||||
.align(placeholderAlignment),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(locale = "en", showBackground = true, name = "English")
|
||||
@Preview(locale = "ru", showBackground = true, name = "Russian")
|
||||
@Composable
|
||||
private fun AmountTextFieldPreview(
|
||||
@PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData,
|
||||
) {
|
||||
var text by remember { mutableStateOf(amount.value.orEmpty()) }
|
||||
TangemTheme {
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = amount.decimals,
|
||||
symbol = amount.symbol,
|
||||
placeholderAlignment = amount.placeholderAlignment,
|
||||
showPlaceholder = amount.showPlaceholder,
|
||||
onValueChange = { text = it },
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTextFieldPreviewData> {
|
||||
override val values = sequenceOf(
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "USD",
|
||||
value = "1000000,123123",
|
||||
decimals = 3,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = true,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = "1000000.123123",
|
||||
decimals = 6,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = false,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "$",
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopCenter,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopStart,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class AmountTextFieldPreviewData(
|
||||
val symbol: String? = "$",
|
||||
val value: String? = null,
|
||||
val decimals: Int = 2,
|
||||
val showPlaceholder: Boolean,
|
||||
val placeholderAlignment: Alignment,
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
/**
|
||||
* Simple text field with placeholder
|
||||
*/
|
||||
@Suppress("ReusedModifierInstance")
|
||||
@Composable
|
||||
fun SimpleTextField(
|
||||
value: String,
|
||||
|
|
@ -29,32 +34,72 @@ fun SimpleTextField(
|
|||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
readOnly: Boolean = false,
|
||||
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = TangemTheme.typography.body2.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
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),
|
||||
var textFieldValueState by remember {
|
||||
mutableStateOf(
|
||||
TextFieldValue(
|
||||
text = value,
|
||||
selection = when {
|
||||
value.isEmpty() -> TextRange.Zero
|
||||
else -> TextRange(value.length, value.length)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester.Default }
|
||||
val customTextSelectionColors = TextSelectionColors(
|
||||
handleColor = TangemTheme.colors.text.secondary,
|
||||
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
|
||||
)
|
||||
|
||||
val textFieldValue = textFieldValueState.copy(text = value)
|
||||
|
||||
SideEffect {
|
||||
if (textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition
|
||||
) {
|
||||
textFieldValueState = textFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
textFieldValueState = newTextFieldValueState
|
||||
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) {
|
||||
onValueChange(newTextFieldValueState.text)
|
||||
}
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
singleLine = singleLine,
|
||||
readOnly = readOnly,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
decorationBox = decorationBox ?: { textValue ->
|
||||
Box {
|
||||
if (value.isBlank() && placeholder != null) {
|
||||
Text(
|
||||
text = placeholder.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
)
|
||||
}
|
||||
textValue()
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,28 +5,49 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
private val decimals: Int,
|
||||
private val symbol: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val formattedText = decimalFormat.formatWithThousands(
|
||||
text.text,
|
||||
decimals,
|
||||
)
|
||||
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
|
||||
return TransformedText(
|
||||
text = buildAnnotatedString {
|
||||
append(formattedText)
|
||||
if (formattedText.isNotEmpty() && symbol != null) {
|
||||
append(" $symbol")
|
||||
}
|
||||
},
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
|
||||
)
|
||||
}
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: String,
|
||||
private val gropingSymbol: Char,
|
||||
) : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
|
||||
}
|
||||
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol }
|
||||
return (offset - noneDigitCount).coerceIn(0, text.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Input row for entering amount. Manages correct amount format and validation
|
||||
*
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param iconRes action icon
|
||||
* @param iconTint action icon tint
|
||||
* @param onIconClick click on action icon
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowDefault] for read only version
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&
|
||||
* t=IQ5lBJEkFGU4WSvi-4>InputRowEnter</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
iconRes: Int? = null,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
iconRes?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing10,
|
||||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
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.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
|
||||
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param isSingleLine text
|
||||
* @param visualTransformation applied transformation to text
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowEnterInfo]
|
||||
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode
|
||||
* =design&t=IQ5lBJEkFGU4WSvi-4>Input Row Enter</a>
|
||||
* @see <a href=https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node
|
||||
* * -id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4>Input Row Enter Info</a>
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowEnterInfoAmount(
|
||||
title: TextReference,
|
||||
text: String,
|
||||
decimals: Int,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
info: TextReference? = null,
|
||||
titleColor: Color = TangemTheme.colors.text.secondary,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
infoColor: Color = TangemTheme.colors.text.tertiary,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
showDivider = showDivider,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = titleColor,
|
||||
)
|
||||
Row {
|
||||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
)
|
||||
info?.let {
|
||||
Text(
|
||||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = infoColor,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.align(Alignment.Bottom),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.img_decimal_22
|
||||
"xdc", "xdc/test" -> R.drawable.img_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.img_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +98,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.img_decimal_22
|
||||
"xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.img_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -143,6 +145,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
|
|||
"decimal" -> R.drawable.img_decimal_22
|
||||
"xdce-crowd-sale" -> R.drawable.img_xdc_22
|
||||
"vechain" -> R.drawable.img_vechain_22
|
||||
"aptos" -> R.drawable.img_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +195,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
|
||||
"xdc", "xdc/test" -> R.drawable.ic_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -241,6 +245,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
|
|||
"decimal", "decimal/test" -> R.drawable.ic_decimal_22
|
||||
"xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22
|
||||
"vechain", "vechain/test" -> R.drawable.ic_vechain_22
|
||||
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
|
||||
private const val TEXT_CHUNK_THOUSAND = 3
|
||||
private const val POINT_SEPARATOR = '.'
|
||||
|
||||
@Composable
|
||||
fun rememberDecimalFormat(): DecimalFormat {
|
||||
val locale = LocalConfiguration.current.locale
|
||||
val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) }
|
||||
|
||||
return remember {
|
||||
DecimalFormat().apply {
|
||||
decimalFormatSymbols = decimalSymbols
|
||||
isParseBigDecimal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [String] for InputField, to remove wrong symbols, letters etc
|
||||
* Use [decimals] for cut this number symbols after floating point
|
||||
*
|
||||
* Example (with 8 decimals):
|
||||
* input string - ab123.46377372ab53
|
||||
* result string 123.46377372
|
||||
*/
|
||||
fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
|
||||
val lastChar = text.lastOrNull()
|
||||
val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) {
|
||||
text.dropLast(1) + decimalSeparator
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
|
||||
val isOneOrZeroPoint =
|
||||
c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1
|
||||
val isIndexPointIndex =
|
||||
c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index
|
||||
c.isDigit() || isIndexPointIndex || isOneOrZeroPoint
|
||||
}
|
||||
// If dot is present, take first digits before decimal and first decimals digits after decimal
|
||||
return if (filteredChars.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
|
||||
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
filteredChars
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input [text] with grouping and decimal separators.
|
||||
* Takes into account [decimals] number of digits after floating point.
|
||||
*/
|
||||
fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
|
||||
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
|
||||
val decimalSeparator = decimalFormatSymbols.decimalSeparator
|
||||
val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString())
|
||||
return if (localizedText.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = localizedText.substringBefore(decimalSeparator)
|
||||
.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
val afterDecimal = localizedText.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
localizedText.reversed()
|
||||
.chunked(TEXT_CHUNK_THOUSAND)
|
||||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
fun DecimalFormat.defaultFormat(): String {
|
||||
return "0${decimalFormatSymbols.decimalSeparator}00"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains extra decimal separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: number can contain only one decimal separator.
|
||||
*/
|
||||
fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount <= 1 // only one decimal separator
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if text input contains grouping separators.
|
||||
* If so, it will return false, otherwise true.
|
||||
*
|
||||
* Note: grouping separators are used only for VisualTransformations.
|
||||
*/
|
||||
fun DecimalFormat.checkGroupingSeparator(text: String): Boolean {
|
||||
val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(text).count()
|
||||
return decimalSeparatorCount == 0 // no grouping separator
|
||||
}
|
||||
|
||||
fun String.parseToBigDecimal(decimals: Int): BigDecimal {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = decimals
|
||||
}
|
||||
return try {
|
||||
decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO
|
||||
} catch (e: Exception) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String {
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
isParseBigDecimal = true
|
||||
isGroupingUsed = false
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = 0
|
||||
this.roundingMode = roundingMode
|
||||
}
|
||||
|
||||
return try {
|
||||
decimalFormat.format(this)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.utils
|
|||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt")
|
||||
class InputNumberFormatter(
|
||||
numberFormat: DecimalFormat,
|
||||
) {
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_aptos_22.xml
Normal file
10
core/ui/src/main/res/drawable/ic_aptos_22.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M12.624,7.396C12.72,7.396 12.812,7.357 12.879,7.288L13.39,6.757C13.453,6.691 13.542,6.653 13.634,6.653H13.655C13.752,6.653 13.844,6.694 13.908,6.767L14.338,7.251C14.42,7.343 14.538,7.396 14.662,7.396H15.815C14.717,5.941 12.969,5 11,5C9.032,5 7.285,5.941 6.186,7.396H12.624ZM13.232,9.014H14.294V9.015H16.684C16.849,9.485 16.957,9.981 17,10.496H12.523C12.399,10.496 12.282,10.443 12.2,10.351L11.769,9.866C11.705,9.794 11.613,9.753 11.516,9.753H11.495C11.403,9.753 11.315,9.79 11.251,9.857L10.74,10.387C10.674,10.457 10.582,10.496 10.485,10.496H5C5.043,9.981 5.151,9.485 5.316,9.015H11.126C11.308,9.015 11.482,8.937 11.603,8.801L11.972,8.385C12.036,8.312 12.129,8.271 12.225,8.271C12.322,8.271 12.415,8.313 12.479,8.385L12.909,8.869C12.991,8.961 13.109,9.014 13.232,9.014ZM8.575,13.5C8.508,13.57 8.416,13.609 8.32,13.609V13.609H5.576C5.349,13.142 5.182,12.64 5.083,12.114H8.988C9.171,12.114 9.344,12.036 9.465,11.9L9.835,11.484C9.899,11.411 9.991,11.37 10.088,11.37C10.184,11.37 10.277,11.412 10.341,11.484L10.771,11.968C10.853,12.06 10.971,12.113 11.094,12.113H16.918C16.819,12.639 16.652,13.141 16.425,13.609H10.358C10.234,13.609 10.117,13.556 10.034,13.464L9.604,12.98C9.54,12.907 9.448,12.866 9.351,12.866H9.33C9.238,12.866 9.149,12.903 9.086,12.97L8.575,13.5ZM8.728,15.092H10.306V15.092H15.403C14.303,16.266 12.738,17 11,17C9.262,17 7.697,16.266 6.597,15.092H6.622C6.805,15.092 6.978,15.014 7.099,14.878L7.468,14.462C7.532,14.39 7.625,14.349 7.722,14.349C7.818,14.349 7.911,14.39 7.975,14.462L8.405,14.947C8.487,15.039 8.605,15.092 8.728,15.092Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
||||
16
core/ui/src/main/res/drawable/img_aptos_22.xml
Normal file
16
core/ui/src/main/res/drawable/img_aptos_22.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<group>
|
||||
<clip-path android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#000000" />
|
||||
<path
|
||||
android:pathData="M12.624,7.396C12.72,7.396 12.812,7.357 12.879,7.288L13.39,6.757C13.453,6.691 13.542,6.653 13.634,6.653H13.655C13.752,6.653 13.844,6.694 13.908,6.767L14.338,7.251C14.42,7.343 14.538,7.396 14.662,7.396H15.815C14.717,5.941 12.969,5 11,5C9.032,5 7.285,5.941 6.186,7.396H12.624ZM13.232,9.014H14.294V9.015H16.684C16.849,9.485 16.957,9.981 17,10.496H12.523C12.399,10.496 12.282,10.443 12.2,10.351L11.769,9.866C11.705,9.794 11.613,9.753 11.516,9.753H11.495C11.403,9.753 11.315,9.79 11.251,9.857L10.74,10.387C10.674,10.457 10.582,10.496 10.485,10.496H5C5.043,9.981 5.151,9.485 5.316,9.015H11.126C11.308,9.015 11.482,8.937 11.603,8.801L11.972,8.385C12.036,8.312 12.129,8.271 12.225,8.271C12.322,8.271 12.415,8.313 12.479,8.385L12.909,8.869C12.991,8.961 13.109,9.014 13.232,9.014ZM8.575,13.5C8.508,13.57 8.416,13.609 8.32,13.609V13.609H5.576C5.349,13.142 5.182,12.64 5.083,12.114H8.988C9.171,12.114 9.344,12.036 9.465,11.9L9.835,11.484C9.899,11.411 9.991,11.37 10.088,11.37C10.184,11.37 10.277,11.412 10.341,11.484L10.771,11.968C10.853,12.06 10.971,12.113 11.094,12.113H16.918C16.819,12.639 16.652,13.141 16.425,13.609H10.358C10.234,13.609 10.117,13.556 10.034,13.464L9.604,12.98C9.54,12.907 9.448,12.866 9.351,12.866H9.33C9.238,12.866 9.149,12.903 9.086,12.97L8.575,13.5ZM8.728,15.092H10.306V15.092H15.403C14.303,16.266 12.738,17 11,17C9.262,17 7.697,16.266 6.597,15.092H6.622C6.805,15.092 6.978,15.014 7.099,14.878L7.468,14.462C7.532,14.39 7.625,14.349 7.722,14.349C7.818,14.349 7.911,14.39 7.975,14.462L8.405,14.947C8.487,15.039 8.605,15.092 8.728,15.092Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -10,12 +10,16 @@ class PeriodicTask<T>(
|
|||
private val task: suspend () -> Result<T>,
|
||||
private val onSuccess: (T) -> Unit,
|
||||
private val onError: (Throwable) -> Unit,
|
||||
private val isDelayFirst: Boolean = false,
|
||||
) {
|
||||
|
||||
private var isActive: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
suspend fun runTaskWithDelay() {
|
||||
isActive.set(true)
|
||||
if (isDelayFirst) {
|
||||
delay(delay)
|
||||
}
|
||||
while (isActive.get()) {
|
||||
task.invoke()
|
||||
.onSuccess {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
|
||||
import com.tangem.data.tokens.utils.NetworkStatusFactory
|
||||
|
|
@ -7,6 +8,7 @@ import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory
|
|||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -68,6 +70,11 @@ internal class DefaultNetworksRepository(
|
|||
networksStatusesStore.getSyncOrNull(userWalletId).orEmpty()
|
||||
}
|
||||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromNetworkId(network.id.value)
|
||||
return blockchain == Blockchain.Aptos
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
|||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -44,6 +41,21 @@ internal class DefaultTransactionRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
) = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
(walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
if (memo == null) return null
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ interface CardTypesResolver {
|
|||
|
||||
fun isBadWallet(): Boolean
|
||||
|
||||
fun isJrWallet(): Boolean
|
||||
|
||||
fun isGrimWallet(): Boolean
|
||||
|
||||
fun isSatoshiFriendsWallet(): Boolean
|
||||
|
||||
fun isWhiteWallet(): Boolean
|
||||
|
||||
fun isWallet2(): Boolean
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ internal class TangemCardTypesResolver(
|
|||
|
||||
override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID
|
||||
|
||||
override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID
|
||||
|
||||
override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID
|
||||
|
||||
override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID
|
||||
|
||||
override fun isWhiteWallet(): Boolean {
|
||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||
}
|
||||
|
|
@ -138,6 +144,9 @@ internal class TangemCardTypesResolver(
|
|||
const val TRON_WALLET_BATCH_ID = "AF07"
|
||||
const val KASPA_WALLET_BATCH_ID = "AF08"
|
||||
const val BAD_WALLET_BATCH_ID = "AF09"
|
||||
const val JR_WALLET_BATCH_ID = "AF14"
|
||||
const val GRIM_WALLET_BATCH_ID = "AF13"
|
||||
const val SATOSHI_WALLET_BATCH_ID = "AF19"
|
||||
const val WHITE_WALLET2_BATCH_ID = "AF15"
|
||||
const val TRILLIANT_WALLET_BATCH_ID = "AF16"
|
||||
const val AVRORA_WALLET_BATCH_ID = "AF18"
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? {
|
|||
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE
|
||||
Blockchain.XRP -> BigDecimal.TEN
|
||||
Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal()
|
||||
Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,9 @@ sealed interface LegacyAction : Action {
|
|||
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
|
||||
*/
|
||||
data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction
|
||||
|
||||
/**
|
||||
* Sending an email to support when sending transaction failed
|
||||
*/
|
||||
data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class Amount(
|
||||
val currencySymbol: String,
|
||||
val value: BigDecimal? = null,
|
||||
val decimals: Int,
|
||||
val type: AmountType = AmountType.CoinType,
|
||||
)
|
||||
|
||||
sealed class AmountType {
|
||||
object CoinType : AmountType()
|
||||
object ReserveType : AmountType()
|
||||
data class TokenType(val token: CryptoCurrency.Token) : AmountType()
|
||||
data class FiatType(val code: String) : AmountType()
|
||||
}
|
||||
|
||||
/** 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.CoinType
|
||||
is CryptoCurrency.Token -> AmountType.TokenType(
|
||||
token = cryptoCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -30,6 +30,8 @@ sealed class CryptoCurrencyWarning {
|
|||
val amountCurrency: CryptoCurrency,
|
||||
) : CryptoCurrencyWarning()
|
||||
|
||||
object TopUpWithoutReserve : CryptoCurrencyWarning()
|
||||
|
||||
/**
|
||||
* Represents wallet blockchain rent
|
||||
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for getting balance not enough warning to cover fee.
|
||||
*
|
||||
* This warning is shown when current currency is not paying fee and paying fee currency balance is not enough
|
||||
*
|
||||
* Current | Paying fee | Warning
|
||||
* Coin | Coin | -
|
||||
* Token | Coin | +
|
||||
* Coin | PToken | + (VTO - VTHO)
|
||||
* Token | PToken | + (Other VeChainToken - VTHO)
|
||||
* PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken)
|
||||
*/
|
||||
class GetBalanceNotEnoughForFeeWarningUseCase(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
fee: BigDecimal,
|
||||
userWalletId: UserWalletId,
|
||||
tokenStatus: CryptoCurrencyStatus,
|
||||
coinStatus: CryptoCurrencyStatus,
|
||||
): Either<Throwable, CryptoCurrencyWarning?> = Either.catch {
|
||||
withContext(dispatchers.io) {
|
||||
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
|
||||
val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token
|
||||
val isFeePaidByToken =
|
||||
feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId
|
||||
|
||||
val warning = when {
|
||||
feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> {
|
||||
CryptoCurrencyWarning.BalanceNotEnoughForFee(
|
||||
tokenCurrency = tokenStatus.currency,
|
||||
coinCurrency = coinStatus.currency,
|
||||
)
|
||||
}
|
||||
feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> {
|
||||
constructTokenBalanceNotEnoughWarning(
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = tokenStatus,
|
||||
feePaidToken = feePaidCurrency,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
warning
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if fee paying token [feePaidToken] is added to wallet [userWalletId]
|
||||
*/
|
||||
private suspend fun constructTokenBalanceNotEnoughWarning(
|
||||
userWalletId: UserWalletId,
|
||||
tokenStatus: CryptoCurrencyStatus,
|
||||
feePaidToken: FeePaidCurrency.Token,
|
||||
): CryptoCurrencyWarning {
|
||||
val token = currenciesRepository
|
||||
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
|
||||
.find {
|
||||
it is CryptoCurrency.Token &&
|
||||
it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) &&
|
||||
it.network.derivationPath == tokenStatus.currency.network.derivationPath
|
||||
}
|
||||
return if (token != null) {
|
||||
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
|
||||
currency = tokenStatus.currency,
|
||||
feeCurrency = token,
|
||||
networkName = token.network.name,
|
||||
feeCurrencyName = feePaidToken.name,
|
||||
feeCurrencySymbol = feePaidToken.symbol,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyWarning.CustomTokenNotEnoughForFee(
|
||||
currency = tokenStatus.currency,
|
||||
feeCurrency = null,
|
||||
networkName = tokenStatus.currency.network.name,
|
||||
feeCurrencyName = feePaidToken.name,
|
||||
feeCurrencySymbol = feePaidToken.symbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
|
|
@ -22,7 +22,7 @@ class GetCryptoCurrencyStatusSyncUseCase(
|
|||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
): Either<TokenListError, CryptoCurrencyStatus> {
|
||||
): Either<CurrencyStatusError, CryptoCurrencyStatus> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -31,6 +31,18 @@ class GetCryptoCurrencyStatusSyncUseCase(
|
|||
)
|
||||
|
||||
return operations.getCurrencyStatusSync(cryptoCurrencyId)
|
||||
.mapLeft { error -> error.mapToTokenListError() }
|
||||
.mapLeft { error -> error.mapToCurrencyError() }
|
||||
}
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<CurrencyStatusError, CryptoCurrencyStatus> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
userWalletId = userWalletId,
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
)
|
||||
|
||||
return operations.getPrimaryCurrencyStatusSync()
|
||||
.mapLeft { error -> error.mapToCurrencyError() }
|
||||
}
|
||||
}
|
||||
|
|
@ -254,10 +254,14 @@ class GetCurrencyWarningsUseCase(
|
|||
|
||||
private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? {
|
||||
return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let {
|
||||
CryptoCurrencyWarning.SomeNetworksNoAccount(
|
||||
amountToCreateAccount = it.amountToCreateAccount,
|
||||
amountCurrency = currencyStatus.currency,
|
||||
)
|
||||
if (networksRepository.isNeedToCreateAccountWithoutReserve(network = currencyStatus.currency.network)) {
|
||||
CryptoCurrencyWarning.TopUpWithoutReserve
|
||||
} else {
|
||||
CryptoCurrencyWarning.SomeNetworksNoAccount(
|
||||
amountToCreateAccount = it.amountToCreateAccount,
|
||||
amountCurrency = currencyStatus.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Use case for checking if currency amount can be subtracted.
|
||||
* Amount can be subtracted if only it is paying fee
|
||||
*/
|
||||
class IsAmountSubtractAvailableUseCase(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Boolean> =
|
||||
Either.catch {
|
||||
withContext(dispatchers.io) {
|
||||
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) {
|
||||
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
|
||||
is FeePaidCurrency.SameCurrency -> true
|
||||
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,43 +8,41 @@ import java.math.BigDecimal
|
|||
|
||||
sealed class TradeCryptoAction : Action {
|
||||
|
||||
data class SendCrypto(
|
||||
val currencyId: String,
|
||||
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
|
||||
|
||||
data class Buy(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
val checkUserLocation: Boolean = true,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class Sell(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class SendToken(
|
||||
val userWallet: UserWallet,
|
||||
val tokenCurrency: CryptoCurrency.Token,
|
||||
val tokenFiatRate: BigDecimal?,
|
||||
val coinFiatRate: BigDecimal?,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val transactionInfo: TransactionInfo? = null,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class SendCoin(
|
||||
val userWallet: UserWallet,
|
||||
val coinStatus: CryptoCurrencyStatus,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
val transactionInfo: TransactionInfo? = null,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction()
|
||||
|
||||
data class TransactionInfo(
|
||||
val amount: String,
|
||||
val destinationAddress: String,
|
||||
val transactionId: String,
|
||||
) : TradeCryptoAction()
|
||||
|
||||
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
|
||||
|
||||
sealed class New : TradeCryptoAction() {
|
||||
|
||||
data class Buy(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
val checkUserLocation: Boolean = true,
|
||||
) : New()
|
||||
|
||||
data class Sell(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrencyCode: String,
|
||||
) : New()
|
||||
|
||||
data class SendToken(
|
||||
val userWallet: UserWallet,
|
||||
val tokenCurrency: CryptoCurrency.Token,
|
||||
val tokenFiatRate: BigDecimal?,
|
||||
val coinFiatRate: BigDecimal?,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
) : New()
|
||||
|
||||
data class SendCoin(
|
||||
val userWallet: UserWallet,
|
||||
val coinStatus: CryptoCurrencyStatus,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
) : New()
|
||||
|
||||
data class Swap(val cryptoCurrency: CryptoCurrency) : New()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
// FIXME: Refactor - [REDACTED_JIRA]
|
||||
@Suppress("LargeClass")
|
||||
internal class CurrenciesStatusesOperations(
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
|
|
@ -107,6 +109,27 @@ internal class CurrenciesStatusesOperations(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getPrimaryCurrencyStatusSync(): Either<Error, CryptoCurrencyStatus> = either {
|
||||
val currency = catch(
|
||||
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
|
||||
catch = { raise(Error.DataError(it)) },
|
||||
)
|
||||
val quotes = catch(
|
||||
block = { quotesRepository.getQuoteSync(currency.id).right() },
|
||||
catch = { Error.DataError(it).left() },
|
||||
)
|
||||
val networkStatus = catch(
|
||||
block = {
|
||||
networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network))
|
||||
.firstOrNull { it.network == currency.network }
|
||||
.right()
|
||||
},
|
||||
catch = { Error.DataError(it).left() },
|
||||
)
|
||||
|
||||
return createCurrencyStatus(currency, quotes, networkStatus)
|
||||
}
|
||||
|
||||
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
|
||||
return flow {
|
||||
val nonEmptyCurrencies = recover(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ interface NetworksRepository {
|
|||
suspend fun getNetworkStatusesSync(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
refresh: Boolean = false,
|
||||
): Set<NetworkStatus>
|
||||
|
||||
fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean
|
||||
}
|
||||
|
|
@ -32,4 +32,6 @@ internal class MockNetworksRepository(
|
|||
): Set<NetworkStatus> {
|
||||
return getNetworkStatusesUpdates(userWalletId, networks).first()
|
||||
}
|
||||
|
||||
override fun isNeedToCreateAccountWithoutReserve(network: Network) = false
|
||||
}
|
||||
|
|
@ -13,9 +13,13 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -17,4 +19,11 @@ interface TransactionRepository {
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData?
|
||||
|
||||
suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): SimpleResult
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
sealed class SendTransactionError {
|
||||
|
||||
object DemoCardError : SendTransactionError()
|
||||
|
|
@ -8,9 +10,12 @@ sealed class SendTransactionError {
|
|||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||
|
||||
object UserCancelledError : SendTransactionError()
|
||||
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError()
|
||||
|
||||
data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError()
|
||||
|
||||
data class UnknownError(val ex: Exception? = null) : SendTransactionError()
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -23,16 +17,15 @@ import java.math.BigDecimal
|
|||
*/
|
||||
class GetFeeUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
amount: BigDecimal,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<Either<GetFeeError, TransactionFee>> {
|
||||
return flow {
|
||||
try {
|
||||
) = either {
|
||||
catch(
|
||||
block = {
|
||||
val result = requireNotNull(
|
||||
walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
|
|
@ -43,14 +36,15 @@ class GetFeeUseCase(
|
|||
) { "Fee is null" }
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
is Result.Failure -> GetFeeError.DataError(result.error).left()
|
||||
is Result.Success -> result.data
|
||||
is Result.Failure -> raise(GetFeeError.DataError(result.error))
|
||||
}
|
||||
emit(maybeFee)
|
||||
} catch (e: Exception) {
|
||||
emit(GetFeeError.DataError(e.cause).left())
|
||||
}
|
||||
}.flowOn(dispatcher.io)
|
||||
maybeFee
|
||||
},
|
||||
catch = {
|
||||
raise(GetFeeError.DataError(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||
|
|
|
|||
|
|
@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.ResultChecker
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
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.R
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val transactionRepository: TransactionRepository,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txData: TransactionData,
|
||||
|
|
@ -37,7 +41,7 @@ class SendTransactionUseCase(
|
|||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
} else {
|
||||
walletManagersFacade.sendTransaction(
|
||||
transactionRepository.sendTransaction(
|
||||
txData = txData,
|
||||
signer = signer,
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -64,29 +68,28 @@ class SendTransactionUseCase(
|
|||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
|
||||
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
|
||||
when (error) {
|
||||
return when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
val errorByCode = mapErrorByCode(error)
|
||||
if (errorByCode != null) {
|
||||
return errorByCode
|
||||
if (error.code == USER_CANCELLED_ERROR_CODE) {
|
||||
SendTransactionError.UserCancelledError
|
||||
} else {
|
||||
val tangemError = error.tangemError
|
||||
if (tangemError is TangemSdkError) {
|
||||
val resource = tangemError.localizedDescriptionRes()
|
||||
val resId = resource.resId ?: R.string.common_unknown_error
|
||||
val resArgs = resource.args.map { it.value }
|
||||
val textReference = resourceReference(resId, wrappedList(resArgs))
|
||||
SendTransactionError.TangemSdkError(tangemError.code, textReference)
|
||||
} else {
|
||||
SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage)
|
||||
}
|
||||
}
|
||||
val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError()
|
||||
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError
|
||||
return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
|
||||
}
|
||||
else -> {
|
||||
return SendTransactionError.TangemSdkError(error.code, error.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? {
|
||||
return when (error.code) {
|
||||
USER_CANCELLED_ERROR_CODE -> {
|
||||
return SendTransactionError.UserCancelledError
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
SendTransactionError.BlockchainSdkError(
|
||||
code = error.code,
|
||||
message = error.customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ internal class DefaultSendRouter(
|
|||
}
|
||||
|
||||
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
reduxNavController.popBackStack()
|
||||
reduxNavController.navigate(
|
||||
action = NavigationAction.NavigateTo(
|
||||
screen = AppScreen.WalletDetails,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendAlertState {
|
||||
|
||||
abstract val title: TextReference?
|
||||
abstract val message: TextReference
|
||||
open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
open val onConfirmClick: (() -> Unit)? = null
|
||||
|
||||
data class GenericError(
|
||||
override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.send_alert_button_request_support)
|
||||
}
|
||||
|
||||
data class TransactionError(
|
||||
val code: String,
|
||||
val cause: String?,
|
||||
val causeTextReference: TextReference? = null,
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
|
||||
override val message: TextReference = resourceReference(
|
||||
id = R.string.send_alert_transaction_failed_text,
|
||||
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
|
||||
)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.send_alert_button_request_support)
|
||||
}
|
||||
|
||||
data class DemoMode(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
|
||||
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
|
||||
}
|
||||
|
||||
object FeeIncreased : SendAlertState() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendEvent {
|
||||
|
||||
data class ShowSnackBar(val text: TextReference) : SendEvent()
|
||||
|
||||
data class ShowAlert(val alert: SendAlertState) : SendEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.getFee
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce event state for [SendUiState]
|
||||
*
|
||||
* @param currentStateProvider [Provider] of [SendUiState]
|
||||
* @param clickIntents [SendClickIntents]
|
||||
* @param feeStateFactory [FeeStateFactory]
|
||||
*/
|
||||
internal class SendEventStateFactory(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) }
|
||||
|
||||
fun onConsumeEventState(): SendUiState {
|
||||
return currentStateProvider().copy(event = consumedEvent())
|
||||
}
|
||||
|
||||
fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val event = error?.let {
|
||||
sendTransactionErrorConverter.convert(error)?.let {
|
||||
triggeredEvent<SendEvent>(SendEvent.ShowAlert(it), onConsume)
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
event = event ?: consumedEvent(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val newFee = when (fee) {
|
||||
is TransactionFee.Single -> fee.normal
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeSelector.selectedFee) {
|
||||
FeeType.SLOW -> fee.minimum
|
||||
FeeType.MARKET -> fee.normal
|
||||
FeeType.FAST -> fee.priority
|
||||
FeeType.CUSTOM -> return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO
|
||||
val oldFeeValue = feeSelector.getFee().amount.value ?: BigDecimal.ZERO
|
||||
val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee)
|
||||
return if (newFeeValue > oldFeeValue) {
|
||||
updateFeeState.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(SendAlertState.FeeIncreased),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onFeeNotIncreased()
|
||||
updateFeeState
|
||||
}
|
||||
}
|
||||
|
||||
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.GenericError(
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) },
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,16 +57,28 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
|||
sealed class Warning(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
) : SendNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
data class HighFeeError(val amount: String) : Warning(
|
||||
data class HighFeeError(
|
||||
val amount: String,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val onDismissClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
|
||||
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
|
||||
primaryText = resourceReference(R.string.send_notification_fee_too_high_accept, wrappedList(amount)),
|
||||
onPrimaryClick = onConfirmClick,
|
||||
secondaryText = resourceReference(R.string.send_notification_fee_too_high_ignore),
|
||||
onSecondaryClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class ExistentialDeposit(val deposit: String) : Warning(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -21,6 +22,7 @@ internal class SendNotificationFactory(
|
|||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
fun create(): Flow<ImmutableList<SendNotification>> = currentStateProvider().currentState
|
||||
|
|
@ -30,19 +32,33 @@ internal class SendNotificationFactory(
|
|||
val feeState = state.feeState ?: return@map persistentListOf()
|
||||
val recipientState = state.recipientState ?: return@map persistentListOf()
|
||||
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val amountValue = state.amountState?.amountTextField?.value?.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
val sendAmount = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue
|
||||
buildList {
|
||||
// errors
|
||||
addExceedBalanceNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addInvalidAmountNotification(feeState.isSubtract, feeState.receivedAmountValue)
|
||||
addMinimumAmountErrorNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addExceedBalanceNotification(feeAmount, sendAmount)
|
||||
addInvalidAmountNotification(feeState.isSubtract, sendAmount)
|
||||
addMinimumAmountErrorNotification(feeAmount, sendAmount)
|
||||
addReserveAmountErrorNotification(recipientState.addressTextField.value)
|
||||
addTransactionLimitErrorNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addTransactionLimitErrorNotification(feeAmount, sendAmount)
|
||||
// warnings
|
||||
addExistentialWarningNotification(feeAmount, feeState.receivedAmountValue)
|
||||
addHighFeeWarningNotification()
|
||||
addExistentialWarningNotification(feeAmount, sendAmount)
|
||||
addHighFeeWarningNotification(amountValue, state.sendState.ignoreAmountReduce)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun dismissHighFeeWarningState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState
|
||||
val updatedNotifications = sendState.notifications.filterNot { it is SendNotification.Warning.HighFeeError }
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
ignoreAmountReduce = true,
|
||||
notifications = updatedNotifications.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
|
|
@ -173,16 +189,30 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification() {
|
||||
// TODO Move Blockchain check elsewhere
|
||||
if (cryptoCurrencyStatusProvider().currency.network.id.value == Blockchain.Tezos.id) {
|
||||
add(SendNotification.Warning.HighFeeError(TEZOS_FEE_THRESHOLD))
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
||||
sendAmount: BigDecimal,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
|
||||
if (!ignoreAmountReduce && sendAmount == balance && isTezos) {
|
||||
add(
|
||||
SendNotification.Warning.HighFeeError(
|
||||
amount = TEZOS_FEE_THRESHOLD.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString()
|
||||
clickIntents.onAmountReduceClick(reduceTo)
|
||||
},
|
||||
onDismissClick = clickIntents::onAmountReduceIgnoreClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CARDANO_MINIMUM = "1"
|
||||
private const val DOGECOIN_MINIMUM = "0.01"
|
||||
private const val TEZOS_FEE_THRESHOLD = "0.01"
|
||||
private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01")
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,9 @@ package com.tangem.features.send.impl.presentation.state
|
|||
import androidx.paging.PagingData
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
|
@ -15,23 +14,21 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountCurrencyConverter
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
|
|
@ -40,25 +37,28 @@ internal class SendStateFactory(
|
|||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
|
||||
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
|
||||
private val customFeeFieldConverter by lazy {
|
||||
SendFeeCustomFieldConverter(
|
||||
private val amountFieldConverter by lazy {
|
||||
SendAmountFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider,
|
||||
userWalletProvider = userWalletProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
private val amountFieldChangeConverter by lazy {
|
||||
SendAmountFieldChangeConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val amountCurrencyConverter by lazy {
|
||||
SendAmountCurrencyConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val amountStateConverter by lazy {
|
||||
SendAmountStateConverter(
|
||||
|
|
@ -75,11 +75,7 @@ internal class SendStateFactory(
|
|||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val feeStateConverter by lazy {
|
||||
SendFeeStateConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val feeStateConverter by lazy { SendFeeStateConverter() }
|
||||
|
||||
private val recipientListStateConverter by lazy {
|
||||
SendRecipientListConverter(
|
||||
|
|
@ -92,6 +88,7 @@ internal class SendStateFactory(
|
|||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
currentState = MutableStateFlow(SendUiStateType.Amount),
|
||||
event = consumedEvent(),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState {
|
||||
|
|
@ -107,16 +104,7 @@ internal class SendStateFactory(
|
|||
//region amount state clicks
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
|
||||
return if (amountState.isFiatValue == isFiat) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(amountState = amountState.copy(isFiatValue = isFiat))
|
||||
}
|
||||
}
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
||||
//endregion
|
||||
|
||||
//region recipient
|
||||
|
|
@ -218,144 +206,6 @@ internal class SendStateFactory(
|
|||
}
|
||||
//endregion
|
||||
|
||||
//region fee
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Loading
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(
|
||||
customValues = feeSelectorState.customValues.toMutableList().apply {
|
||||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract)
|
||||
|
||||
val updatedState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(),
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubtractSelect(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee, value)
|
||||
val updatedState = feeState.copy(
|
||||
isSubtract = value,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = if (value) {
|
||||
getFormattedValue(receivedAmount)
|
||||
} else {
|
||||
feeState.receivedAmount
|
||||
},
|
||||
)
|
||||
return state.copy(
|
||||
feeState = updatedState.copy(
|
||||
notifications = feeNotificationFactory(feeState = updatedState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.isPrimaryButtonEnabled(): Boolean {
|
||||
return when (this) {
|
||||
is FeeSelectorState.Loading -> false
|
||||
is FeeSelectorState.Content -> {
|
||||
val customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = getFee().amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM
|
||||
val isNotCustom = selectedFee != FeeType.CUSTOM
|
||||
fee < balance && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedValue(value: BigDecimal): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = value,
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region send
|
||||
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
|
|
@ -375,6 +225,7 @@ internal class SendStateFactory(
|
|||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
isSuccess = true,
|
||||
txUrl = txUrl,
|
||||
notifications = persistentListOf(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendTransactionAlertConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<SendTransactionError, SendAlertState?> {
|
||||
override fun convert(value: SendTransactionError): SendAlertState? {
|
||||
return when (value) {
|
||||
SendTransactionError.DemoCardError -> SendAlertState.DemoMode(
|
||||
onConfirmClick = { clickIntents.popBackStack() },
|
||||
)
|
||||
is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = null,
|
||||
causeTextReference = value.messageReference,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) },
|
||||
)
|
||||
is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError(
|
||||
code = value.code.toString(),
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
|
||||
)
|
||||
is SendTransactionError.DataError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.NetworkError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.UnknownError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.ex?.localizedMessage,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
|
|
@ -31,6 +31,7 @@ internal data class SendUiState(
|
|||
val sendState: SendStates.SendState = SendStates.SendState(),
|
||||
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
|
||||
val currentState: MutableStateFlow<SendUiStateType>,
|
||||
val event: StateEvent<SendEvent>,
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
@ -44,14 +45,11 @@ internal sealed class SendStates {
|
|||
data class AmountState(
|
||||
override val type: SendUiStateType = SendUiStateType.Amount,
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val walletBalance: TextReference,
|
||||
val tokenIconState: TokenIconState,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
val amountTextField: SendTextField.AmountField,
|
||||
) : SendStates()
|
||||
|
||||
/** Recipient state */
|
||||
|
|
@ -69,13 +67,14 @@ internal sealed class SendStates {
|
|||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading,
|
||||
val isSubtract: Boolean = false,
|
||||
val fee: Fee? = null,
|
||||
val receivedAmountValue: BigDecimal = BigDecimal.ZERO,
|
||||
val receivedAmount: String = "",
|
||||
val notifications: ImmutableList<SendFeeNotification> = persistentListOf(),
|
||||
val feeSelectorState: FeeSelectorState,
|
||||
val isSubtractAvailable: Boolean,
|
||||
val isSubtract: Boolean,
|
||||
val isUserSubtracted: Boolean,
|
||||
val fee: Fee?,
|
||||
val receivedAmountValue: BigDecimal,
|
||||
val receivedAmount: String,
|
||||
val notifications: ImmutableList<SendFeeNotification>,
|
||||
) : SendStates()
|
||||
|
||||
/** Send state */
|
||||
|
|
@ -86,6 +85,7 @@ internal sealed class SendStates {
|
|||
val isSuccess: Boolean = false,
|
||||
val transactionDate: Long = 0L,
|
||||
val txUrl: String = "",
|
||||
val ignoreAmountReduce: Boolean = false,
|
||||
val notifications: ImmutableList<SendNotification> = persistentListOf(),
|
||||
) : SendStates()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,22 +11,16 @@ internal class StateRouter(
|
|||
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Recipient)
|
||||
private set
|
||||
|
||||
private var isFromSend: Boolean = false
|
||||
|
||||
fun popBackStack() {
|
||||
fragmentManager.get()?.popBackStack()
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
if (isFromSend) {
|
||||
showSend()
|
||||
} else {
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Recipient -> popBackStack()
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Fee -> showAmount()
|
||||
SendUiStateType.Send -> showFee()
|
||||
}
|
||||
when (currentState.value) {
|
||||
SendUiStateType.Recipient -> popBackStack()
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Fee -> showAmount()
|
||||
SendUiStateType.Send -> showFee()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,18 +42,15 @@ internal class StateRouter(
|
|||
}
|
||||
}
|
||||
|
||||
fun showAmount(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
fun showAmount() {
|
||||
currentState.update { SendUiStateType.Amount }
|
||||
}
|
||||
|
||||
fun showRecipient(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
fun showRecipient() {
|
||||
currentState.update { SendUiStateType.Recipient }
|
||||
}
|
||||
|
||||
fun showFee(isFromSend: Boolean = false) {
|
||||
this.isFromSend = isFromSend
|
||||
fun showFee() {
|
||||
currentState.update { SendUiStateType.Fee }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
||||
internal class SendAmountCurrencyConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Boolean, SendUiState> {
|
||||
override fun convert(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
|
||||
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
|
||||
state
|
||||
} else {
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
isFiatValue = value,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -29,13 +32,10 @@ internal class SendAmountStateConverter(
|
|||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||
|
||||
return SendStates.AmountState(
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = status,
|
||||
walletName = userWallet.name,
|
||||
walletBalance = "$crypto ($fiat)",
|
||||
walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)),
|
||||
tokenIconState = iconStateConverter.convert(status),
|
||||
amountTextField = sendAmountFieldConverter.convert(Unit),
|
||||
isFiatValue = false,
|
||||
isPrimaryButtonEnabled = false,
|
||||
segmentedButtonConfig = persistentListOf(
|
||||
SendAmountSegmentedButtonsConfig(
|
||||
|
|
|
|||
|
|
@ -2,20 +2,17 @@ package com.tangem.features.send.impl.presentation.state.fee
|
|||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Calculate receiving amount when fee is subtracted from sending amount
|
||||
*/
|
||||
internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee, isSubtract: Boolean): BigDecimal {
|
||||
val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO
|
||||
internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal {
|
||||
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
||||
val fee = feeAmount.amount.value ?: return BigDecimal.ZERO
|
||||
return if (isSubtract) {
|
||||
amount.toBigDecimal().minus(fee)
|
||||
} else {
|
||||
amount.toBigDecimal()
|
||||
}
|
||||
return amountValue.minus(fee)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,8 +26,7 @@ internal fun FeeSelectorState.Content.getFee(): Fee {
|
|||
FeeType.MARKET -> fees.normal
|
||||
FeeType.FAST -> fees.priority
|
||||
FeeType.CUSTOM -> {
|
||||
val feeAmount =
|
||||
customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) } ?: BigDecimal.ZERO
|
||||
val feeAmount = customValues.firstOrNull()?.value.toBigDecimalOrDefault()
|
||||
Fee.Common(
|
||||
fees.normal.amount.copy(
|
||||
value = feeAmount,
|
||||
|
|
|
|||
|
|
@ -1,35 +1,60 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
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.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class FeeNotificationFactory(
|
||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
) {
|
||||
|
||||
operator fun invoke(feeState: SendStates.FeeState): ImmutableList<SendFeeNotification> {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return persistentListOf()
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
fun create() = currentStateProvider().currentState
|
||||
.filter { it == SendUiStateType.Fee }
|
||||
.map {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return@map persistentListOf()
|
||||
buildList {
|
||||
when (val feeSelectorState = feeState.feeSelectorState) {
|
||||
FeeSelectorState.Loading -> Unit
|
||||
FeeSelectorState.Error -> {
|
||||
addFeeUnreachableNotification(feeSelectorState)
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addFeeCoverageNotification(feeState, state.amountState)
|
||||
addExceedsBalanceNotification(feeState.fee)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
return buildList {
|
||||
addTooLowNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
addFeeCoverageNotification()
|
||||
addExceedsBalanceNotification(feeSelectorState)
|
||||
}.toImmutableList()
|
||||
private fun MutableList<SendFeeNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState is FeeSelectorState.Error) {
|
||||
add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addTooLowNotification(
|
||||
|
|
@ -59,35 +84,62 @@ internal class FeeNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification() {
|
||||
// TODO add fee coverage condition [REDACTED_JIRA]
|
||||
add(SendFeeNotification.Warning.NetworkCoverage)
|
||||
private fun MutableList<SendFeeNotification>.addFeeCoverageNotification(
|
||||
feeState: SendStates.FeeState,
|
||||
amountState: SendStates.AmountState?,
|
||||
) {
|
||||
if (!feeState.isSubtractAvailable) return
|
||||
|
||||
val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return
|
||||
val feeValue = feeState.fee?.amount?.value ?: return
|
||||
val value = amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) {
|
||||
add(SendFeeNotification.Warning.NetworkCoverage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(
|
||||
feeSelectorState: FeeSelectorState.Content,
|
||||
) {
|
||||
val coinCryptoCurrency = coinCryptoCurrencyStatusProvider()
|
||||
val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO
|
||||
val choosableFee = feeSelectorState.fees as? TransactionFee.Choosable
|
||||
val fee = when (feeSelectorState.selectedFee) {
|
||||
FeeType.SLOW -> choosableFee?.minimum?.amount?.value
|
||||
FeeType.MARKET -> feeSelectorState.fees.normal.amount.value
|
||||
FeeType.FAST -> choosableFee?.priority?.amount?.value
|
||||
FeeType.CUSTOM -> feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
} ?: return
|
||||
private suspend fun MutableList<SendFeeNotification>.addExceedsBalanceNotification(fee: Fee?) {
|
||||
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
if (fee > cryptoAmount) {
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
coinCryptoCurrency.currency.networkIconResId,
|
||||
) {
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletProvider().walletId,
|
||||
coinCryptoCurrency.currency,
|
||||
)
|
||||
},
|
||||
)
|
||||
val warning = getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = feeValue,
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = coinCryptoCurrencyStatusProvider(),
|
||||
).fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { it },
|
||||
) ?: return
|
||||
|
||||
when (warning) {
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
warning.coinCurrency.networkIconResId,
|
||||
) {
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletProvider().walletId,
|
||||
warning.coinCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
|
||||
val currency = warning.feeCurrency ?: warning.currency
|
||||
add(
|
||||
SendFeeNotification.Error.ExceedsBalance(
|
||||
currency.networkIconResId,
|
||||
) {
|
||||
clickIntents.onTokenDetailsClick(
|
||||
userWalletId,
|
||||
currency,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ internal sealed class FeeSelectorState {
|
|||
val selectedFee: FeeType = FeeType.MARKET,
|
||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
object Error : FeeSelectorState()
|
||||
}
|
||||
|
||||
enum class FeeType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce fee state for [SendUiState]
|
||||
*/
|
||||
internal class FeeStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) {
|
||||
private val customFeeFieldConverter by lazy {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
notifications = persistentListOf(),
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
) ?: FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
isSubtractAvailable = isSubtractAvailable,
|
||||
feeSelectorState = feeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val updatedFeeSelector = feeSelectorState.copy(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
val fee = updatedFeeSelector.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelector,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnErrorState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
feeState = state.feeState?.copy(
|
||||
feeSelectorState = FeeSelectorState.Error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
fee = fee,
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(
|
||||
customValues = feeSelectorState.customValues.toMutableList().apply {
|
||||
set(index, feeSelectorState.customValues[index].copy(value = value))
|
||||
}.toImmutableList(),
|
||||
)
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val fee = updatedFeeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
isSubtract = checkAutoSubtract(state, fee, balance),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onSubtractSelect(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val fee = feeSelectorState.getFee()
|
||||
val receivedAmount = calculateReceiveAmount(state, fee)
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
isSubtract = value,
|
||||
isUserSubtracted = true,
|
||||
receivedAmountValue = receivedAmount,
|
||||
receivedAmount = getFormattedValue(receivedAmount),
|
||||
fee = fee,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeNotificationState(notifications: ImmutableList<SendFeeNotification>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
feeState = state.feeState?.copy(
|
||||
notifications = notifications,
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isPrimaryButtonEnabled(
|
||||
feeState: SendStates.FeeState,
|
||||
notifications: ImmutableList<SendFeeNotification>,
|
||||
): Boolean {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
val customValue = feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull()
|
||||
val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||
val fee = feeSelectorState.getFee()
|
||||
val feeValue = fee.amount.value ?: BigDecimal.ZERO
|
||||
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM
|
||||
val isNotEmptyCustom = !customValue.isNullOrZero() && !isNotCustom
|
||||
val noErrors = notifications.none { it is SendFeeNotification.Error }
|
||||
val isSubtractRequired = when {
|
||||
!feeState.isSubtractAvailable -> true // current currency is not fee currency
|
||||
feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract
|
||||
else -> feeValue + feeState.receivedAmountValue <= balance
|
||||
}
|
||||
|
||||
return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
|
||||
private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean {
|
||||
val feeState = state.feeState ?: return false
|
||||
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
||||
val feeAmount = fee.amount.value ?: BigDecimal.ZERO
|
||||
return if (feeState.isUserSubtracted) {
|
||||
feeState.isSubtract
|
||||
} else {
|
||||
amountValue + feeAmount >= balance
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedValue(value: BigDecimal): String {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
return BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = value,
|
||||
cryptoCurrency = cryptoCurrency.symbol,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,16 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toFormattedString
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -31,16 +34,24 @@ internal class SendFeeCustomFieldConverter(
|
|||
|
||||
return persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.amount.value.toString(),
|
||||
value = ethereumFee.amount.value?.toFormattedString(ethereumFee.amount.decimals).orEmpty(),
|
||||
decimals = ethereumFee.amount.decimals,
|
||||
symbol = ethereumFee.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(0, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
label = TextReference.Str(maxFeeFiat),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_max_fee_footer),
|
||||
label = stringReference(maxFeeFiat),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasPrice.toString(),
|
||||
decimals = 0,
|
||||
symbol = ETHEREUM_UNIT,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(1, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
|
|
@ -49,6 +60,10 @@ internal class SendFeeCustomFieldConverter(
|
|||
),
|
||||
SendTextField.CustomFee(
|
||||
value = ethereumFee.gasLimit.toString(),
|
||||
decimals = 0,
|
||||
symbol = null,
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(2, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
|
|
@ -57,4 +72,8 @@ internal class SendFeeCustomFieldConverter(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ETHEREUM_UNIT = "GWEI"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
sealed class SendFeeNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Informational(
|
||||
|
|
@ -29,11 +27,13 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
|
|||
sealed class Warning(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
) : SendFeeNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
data class TooHigh(
|
||||
|
|
@ -47,6 +47,15 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
|
|||
title = resourceReference(id = R.string.send_network_fee_warning_title),
|
||||
subtitle = resourceReference(id = R.string.send_network_fee_warning_content),
|
||||
)
|
||||
|
||||
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
|
||||
title = resourceReference(R.string.send_fee_unreachable_error_title),
|
||||
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(R.string.warning_button_refresh),
|
||||
onClick = onRefresh,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SendFeeStateConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.FeeState> {
|
||||
internal class SendFeeStateConverter : Converter<Unit, SendStates.FeeState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
return SendStates.FeeState(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
isSubtractAvailable = false,
|
||||
isSubtract = false,
|
||||
isUserSubtracted = false,
|
||||
fee = null,
|
||||
receivedAmountValue = BigDecimal.ZERO,
|
||||
receivedAmount = "",
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,48 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
import java.text.NumberFormat
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class SendAmountFieldChangeConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<String, SendUiState> {
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val feeState = state.feeState ?: return state
|
||||
|
||||
if (value.checkDecimalSeparatorDuplicate()) return state
|
||||
if (value.isEmpty()) return state.emptyState()
|
||||
|
||||
val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
|
||||
val trimmedValue = value.trim()
|
||||
val cryptoValue = if (amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals)
|
||||
val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals)
|
||||
val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals)
|
||||
val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals)
|
||||
|
||||
val fiatValue = if (!amountState.isFiatValue) {
|
||||
if (value.isNotBlank()) {
|
||||
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
|
||||
} else {
|
||||
DEFAULT_VALUE
|
||||
}
|
||||
} else {
|
||||
trimmedValue
|
||||
}
|
||||
|
||||
val isExceedBalance = cryptoValue.checkExceedBalance(amountState)
|
||||
val isMaxAmount = cryptoValue.checkMaxAmount(amountState)
|
||||
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
|
||||
val isExceedBalance = checkValue.checkExceedBalance(amountTextField)
|
||||
val isMaxAmount = checkValue.checkMaxAmount(amountTextField)
|
||||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance,
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
isError = isExceedBalance,
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
),
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
|
|
@ -58,58 +51,63 @@ internal class SendAmountFieldChangeConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun String.getCryptoValue(isFiatValue: Boolean, decimals: Int): String {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
return if (isFiatValue && fiatRate != null) {
|
||||
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
|
||||
.parseBigDecimal(decimals)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.getFiatValue(isFiatValue: Boolean, decimals: Int): String {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
return if (!isFiatValue && fiatRate != null) {
|
||||
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun SendUiState.emptyState(): SendUiState {
|
||||
return copy(
|
||||
amountState = amountState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
isError = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
|
||||
val regex = TRIM_REGEX.toRegex()
|
||||
val decimalSeparatorCount = regex.findAll(this).count()
|
||||
|
||||
return decimalSeparatorCount > 1
|
||||
}
|
||||
|
||||
private fun String.checkExceedBalance(state: SendStates.AmountState): Boolean {
|
||||
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() > currencyFiatAmount
|
||||
private fun String.checkExceedBalance(amountTextField: SendTextField.AmountField): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (amountTextField.isFiatValue) {
|
||||
parseToBigDecimal(amountTextField.fiatAmount.decimals) > currencyFiatAmount
|
||||
} else {
|
||||
toBigDecimal() > currencyCryptoAmount
|
||||
parseToBigDecimal(amountTextField.cryptoAmount.decimals) > currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.checkMaxAmount(state: SendStates.AmountState): Boolean {
|
||||
private fun String.checkMaxAmount(amountTextField: SendTextField.AmountField): Boolean {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
// If current currency is Token
|
||||
if (state.cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false
|
||||
if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false
|
||||
|
||||
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (state.isFiatValue) {
|
||||
toBigDecimal() == currencyFiatAmount
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
|
||||
return if (amountTextField.isFiatValue) {
|
||||
parseToBigDecimal(amountTextField.fiatAmount.decimals) == currencyFiatAmount
|
||||
} else {
|
||||
toBigDecimal() == currencyCryptoAmount
|
||||
parseToBigDecimal(amountTextField.cryptoAmount.decimals) == currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.trim(): String {
|
||||
var trimmedValue = this
|
||||
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
|
||||
|
||||
return trimmedValue.replace(TRIM_REGEX.toRegex(), ".")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
private const val TRIM_REGEX = "[.,]"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,31 +4,47 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.convertToAmount
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.text.NumberFormat
|
||||
import java.math.BigDecimal
|
||||
|
||||
private const val FIAT_DECIMALS = 2
|
||||
|
||||
internal class SendAmountFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<Unit, SendTextField.Amount> {
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Unit, SendTextField.AmountField> {
|
||||
|
||||
override fun convert(value: Unit): SendTextField.Amount {
|
||||
return SendTextField.Amount(
|
||||
override fun convert(value: Unit): SendTextField.AmountField {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
return SendTextField.AmountField(
|
||||
value = "",
|
||||
fiatValue = DEFAULT_VALUE,
|
||||
fiatValue = "",
|
||||
onValueChange = clickIntents::onAmountValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
placeholder = TextReference.Str(DEFAULT_VALUE),
|
||||
isFiatValue = false,
|
||||
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency),
|
||||
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
|
||||
isError = false,
|
||||
error = TextReference.Res(R.string.swapping_insufficient_funds),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
|
||||
}
|
||||
private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount(
|
||||
currencySymbol = appCurrency.symbol,
|
||||
value = BigDecimal.ZERO,
|
||||
decimals = FIAT_DECIMALS,
|
||||
type = AmountType.FiatType(appCurrency.code),
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fields
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendTextField {
|
||||
|
|
@ -16,14 +17,13 @@ internal sealed class SendTextField {
|
|||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
// /** Placeholder (hint) */
|
||||
// abstract val placeholder: TextReference
|
||||
|
||||
data class Amount(
|
||||
data class AmountField(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val cryptoAmount: Amount,
|
||||
val fiatAmount: Amount,
|
||||
val isFiatValue: Boolean,
|
||||
val fiatValue: String,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
|
|
@ -53,6 +53,10 @@ internal sealed class SendTextField {
|
|||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val symbol: String?,
|
||||
val decimals: Int,
|
||||
val title: TextReference,
|
||||
val footer: TextReference,
|
||||
val label: TextReference? = null,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendAlertState
|
||||
import com.tangem.features.send.impl.presentation.state.SendEvent
|
||||
|
||||
@Composable
|
||||
internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: SnackbarHostState) {
|
||||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<SendAlertState?>(value = null) }
|
||||
|
||||
alertConfig?.let {
|
||||
SendAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is SendEvent.ShowSnackBar -> {
|
||||
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
|
||||
}
|
||||
is SendEvent.ShowAlert -> {
|
||||
alertConfig = value.alert
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButton
|
||||
val dismissButton: DialogButton?
|
||||
|
||||
val onActionClick = state.onConfirmClick
|
||||
if (onActionClick != null) {
|
||||
confirmButton = DialogButton(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
onActionClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
dismissButton = DialogButton(
|
||||
title = stringResource(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
} else {
|
||||
confirmButton = DialogButton(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
dismissButton = null
|
||||
}
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismiss,
|
||||
title = state.title?.resolveReference(),
|
||||
dismissButton = dismissButton,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
|||
internal fun SendScreen(uiState: SendUiState) {
|
||||
val currentState = uiState.currentState.collectAsStateWithLifecycle()
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
BackHandler { uiState.clickIntents.onBackClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) {
|
|||
)
|
||||
SendNavigationButtons(uiState)
|
||||
}
|
||||
|
||||
SendEventEffect(
|
||||
event = uiState.event,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -3,48 +3,49 @@ package com.tangem.features.send.impl.presentation.ui.amount
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
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.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.AmountField(
|
||||
sendField: SendTextField.Amount,
|
||||
cryptoSymbol: String,
|
||||
fiatSymbol: String,
|
||||
isFiat: Boolean,
|
||||
) {
|
||||
val value = if (isFiat) sendField.fiatValue else sendField.value
|
||||
val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value
|
||||
val symbol = if (isFiat) fiatSymbol else cryptoSymbol
|
||||
val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol
|
||||
internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val (primaryValue, secondaryValue) = if (isFiat) {
|
||||
sendField.fiatValue to sendField.value
|
||||
} else {
|
||||
sendField.value to sendField.fiatValue
|
||||
}
|
||||
|
||||
AmountFieldInner(
|
||||
value = value,
|
||||
placeholder = sendField.placeholder,
|
||||
symbol = symbol,
|
||||
val (primaryAmount, secondaryAmount) = if (!isFiat) {
|
||||
sendField.cryptoAmount to sendField.fiatAmount
|
||||
} else {
|
||||
sendField.fiatAmount to sendField.cryptoAmount
|
||||
}
|
||||
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
onValueChange = sendField.onValueChange,
|
||||
keyboardOptions = sendField.keyboardOptions,
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
placeholderAlignment = TopCenter,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -54,15 +55,15 @@ internal fun ColumnScope.AmountField(
|
|||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
|
||||
Text(
|
||||
text = "$secondaryValue $secondarySymbol",
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -80,47 +81,6 @@ internal fun ColumnScope.AmountField(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldInner(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
symbol: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(symbol),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank()) {
|
||||
Text(
|
||||
text = "${placeholder.resolveReference()} $symbol",
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
|
|
@ -33,29 +35,24 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier:
|
|||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
.padding(top = TangemTheme.dimens.spacing14),
|
||||
)
|
||||
Text(
|
||||
text = amountState.walletBalance,
|
||||
text = amountState.walletBalance.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
TokenIcon(
|
||||
state = amountState.tokenIconState,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
isFiat = amountState.isFiatValue,
|
||||
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
|
||||
fiatSymbol = amountState.appCurrency.symbol,
|
||||
isFiat = amountState.amountTextField.isFiatValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,86 +5,64 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.components.inputrow.InputRowEnterAmount
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val ETHEREUM_UNIT = "GWEI"
|
||||
|
||||
@Composable
|
||||
internal fun SendCustomFeeEthereum(
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
selectedFee: FeeType,
|
||||
symbol: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) {
|
||||
val fee = customValues[0]
|
||||
val gasPrice = customValues[1]
|
||||
val gasLimit = customValues[2]
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = modifier,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = stringResource(R.string.send_max_fee_footer),
|
||||
) {
|
||||
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),
|
||||
) {
|
||||
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),
|
||||
) {
|
||||
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,
|
||||
),
|
||||
)
|
||||
repeat(customValues.size) { index ->
|
||||
val value = customValues[index]
|
||||
FooterContainer(
|
||||
footer = value.footer.resolveReference(),
|
||||
) {
|
||||
if (value.label != null) {
|
||||
InputRowEnterInfoAmount(
|
||||
text = value.value,
|
||||
decimals = value.decimals,
|
||||
symbol = value.symbol,
|
||||
title = value.title,
|
||||
info = value.label,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
onValueChange = value.onValueChange,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
InputRowEnterAmount(
|
||||
text = value.value,
|
||||
decimals = value.decimals,
|
||||
title = value.title,
|
||||
symbol = value.symbol,
|
||||
onValueChange = value.onValueChange,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -16,7 +15,6 @@ import com.tangem.core.ui.components.notifications.Notification
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -24,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
|
||||
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
|
||||
if (state == null) return
|
||||
|
|
@ -36,7 +35,6 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
|
|
@ -44,17 +42,15 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
|
|||
SendSpeedSelector(
|
||||
state = feeSendState,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
customFee(feeSendState)
|
||||
notifications(notifications)
|
||||
customFee(
|
||||
feeSendState = feeSendState,
|
||||
cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol,
|
||||
)
|
||||
subtractButton(
|
||||
feeSendState = feeSendState,
|
||||
receivedAmount = state.receivedAmount,
|
||||
isSubtract = state.isSubtract,
|
||||
isSubtractAvailable = state.isSubtractAvailable,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
|
@ -69,10 +65,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotificat
|
|||
itemContent = {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
modifier = modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12)
|
||||
.animateItemPlacement(),
|
||||
containerColor = when (it) {
|
||||
is SendFeeNotification.Error.ExceedsBalance,
|
||||
is SendFeeNotification.Warning.NetworkFeeUnreachable,
|
||||
-> TangemTheme.colors.background.primary
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (it) {
|
||||
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is SendFeeNotification.Error.ExceedsBalance -> {
|
||||
if (it.config.buttonsState == null) {
|
||||
TangemTheme.colors.icon.warning
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
|
|
@ -81,11 +91,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotificat
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.customFee(
|
||||
feeSendState: FeeSelectorState,
|
||||
cryptoCurrencySymbol: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: Modifier = Modifier) {
|
||||
item(
|
||||
key = FEE_CUSTOM_KEY,
|
||||
) {
|
||||
|
|
@ -101,7 +107,7 @@ internal fun LazyListScope.customFee(
|
|||
SendCustomFeeEthereum(
|
||||
customValues = customValues,
|
||||
selectedFee = fee.selectedFee,
|
||||
symbol = cryptoCurrencySymbol,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -110,31 +116,21 @@ internal fun LazyListScope.customFee(
|
|||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.subtractButton(
|
||||
feeSendState: FeeSelectorState,
|
||||
receivedAmount: String,
|
||||
isSubtract: Boolean,
|
||||
isSubtractAvailable: Boolean,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
(feeSendState as? FeeSelectorState.Content)?.let { state ->
|
||||
if (isSubtractAvailable) {
|
||||
item {
|
||||
val selectedFeeValue = state.selectedFee
|
||||
val topPadding = if (selectedFeeValue != FeeType.CUSTOM) {
|
||||
TangemTheme.dimens.spacing8
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
|
||||
SendSpeedSubtract(
|
||||
receivingAmount = receivedAmount,
|
||||
isSubtract = isSubtract,
|
||||
onSelectClick = clickIntents::onSubtractSelect,
|
||||
modifier = modifier
|
||||
.animateItemPlacement()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
.padding(vertical = TangemTheme.dimens.spacing12)
|
||||
.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.ui.fee
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -13,6 +14,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
|
@ -26,12 +28,19 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private val DEFAULT_FEE_OPTIONS = listOf(
|
||||
R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24,
|
||||
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24,
|
||||
R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendSpeedSelector(
|
||||
|
|
@ -50,10 +59,11 @@ internal fun SendSpeedSelector(
|
|||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
when (state) {
|
||||
FeeSelectorState.Error -> {
|
||||
SendSpeedSelectorItemError()
|
||||
}
|
||||
FeeSelectorState.Loading -> {
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
SendSpeedSelectorItemLoading()
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
when (state.fees) {
|
||||
|
|
@ -84,7 +94,10 @@ internal fun SendSpeedSelector(
|
|||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) },
|
||||
showDivider = state.fees.normal is Fee.Ethereum,
|
||||
)
|
||||
if (state.fees.normal is Fee.Ethereum) {
|
||||
AnimatedVisibility(
|
||||
visible = state.fees.normal is Fee.Ethereum,
|
||||
label = "Custom fee appearance animation",
|
||||
) {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
|
|
@ -114,34 +127,52 @@ internal fun SendSpeedSelector(
|
|||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItemLoading() {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size50,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
SpacerWMax()
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size90,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
repeat(DEFAULT_FEE_OPTIONS.size) {
|
||||
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
SelectorTitleContent(
|
||||
titleRes = text,
|
||||
iconRes = iconRes,
|
||||
)
|
||||
SpacerWMax()
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing18,
|
||||
bottom = TangemTheme.dimens.spacing18,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
width = TangemTheme.dimens.size90,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSpeedSelectorItemError() {
|
||||
repeat(DEFAULT_FEE_OPTIONS.size) {
|
||||
val (text, iconRes) = DEFAULT_FEE_OPTIONS[it]
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
SelectorTitleContent(
|
||||
titleRes = text,
|
||||
iconRes = iconRes,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,27 +208,11 @@ private fun SendSpeedSelectorItem(
|
|||
.clickable { onSelect() },
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
SelectorTitleContent(
|
||||
titleRes = titleRes,
|
||||
iconRes = iconRes,
|
||||
iconTint = iconTint,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
if (amount != null && symbol != null) {
|
||||
SelectorValueContent(
|
||||
|
|
@ -220,6 +235,37 @@ private fun SendSpeedSelectorItem(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectorTitleContent(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
textStyle: TextStyle = TangemTheme.typography.body2,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.constraintlayout.compose.Visibility
|
|||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
|
|
@ -49,10 +50,11 @@ fun ListItemWithIcon(
|
|||
subtitleEndOffset: Int = 0,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
) {
|
||||
val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick)
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.clickable { hapticFeedback() }
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val (iconRef, titleRef, subtitleRef, subtitleIconRef) = createRefs()
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ internal fun SendRecipientContent(
|
|||
placeholder = address.placeholder,
|
||||
onValueChange = address.onValueChange,
|
||||
onPasteClick = clickIntents::onRecipientAddressValueChange,
|
||||
singleLine = true,
|
||||
isError = isError,
|
||||
isLoading = isValidating,
|
||||
error = address.error,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -55,6 +56,7 @@ internal fun TextFieldWithPaste(
|
|||
placeholder = placeholder,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,17 @@ 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 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.notifications.Notification
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.domain.tokens.model.AmountType
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
|
|
@ -61,7 +62,7 @@ internal fun SendContent(uiState: SendUiState) {
|
|||
AnimatedVisibility(visible = !isSuccess) {
|
||||
FromWallet(
|
||||
walletName = amountState.walletName,
|
||||
walletBalance = amountState.walletBalance,
|
||||
walletBalance = amountState.walletBalance.resolveReference(),
|
||||
)
|
||||
}
|
||||
AmountBlock(
|
||||
|
|
@ -122,13 +123,14 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
|
|||
val amount = amountState.amountTextField
|
||||
|
||||
val cryptoAmount = formatCryptoAmount(
|
||||
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
|
||||
cryptoAmount = amount.value.toBigDecimalOrDefault(),
|
||||
cryptoAmount = amount.cryptoAmount.value,
|
||||
cryptoCurrency = amount.cryptoAmount.currencySymbol,
|
||||
decimals = amount.cryptoAmount.decimals,
|
||||
)
|
||||
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount.fiatValue.toBigDecimalOrDefault(),
|
||||
fiatCurrencyCode = amountState.appCurrency.code,
|
||||
fiatCurrencySymbol = amountState.appCurrency.symbol,
|
||||
fiatAmount = amount.fiatAmount.value,
|
||||
fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code,
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
)
|
||||
InputRowImage(
|
||||
title = TextReference.Res(R.string.send_amount_label),
|
||||
|
|
@ -197,7 +199,10 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendNotification
|
|||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
containerColor = TangemTheme.colors.button.disabled,
|
||||
containerColor = when (it) {
|
||||
is SendNotification.Warning.HighFeeError -> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (it) {
|
||||
is SendNotification.Error -> TangemTheme.colors.icon.warning
|
||||
is SendNotification.Warning -> null
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface SendClickIntents {
|
||||
|
||||
fun popBackStack()
|
||||
|
|
@ -16,6 +17,8 @@ interface SendClickIntents {
|
|||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
|
||||
fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
// region Amount
|
||||
|
|
@ -33,6 +36,8 @@ interface SendClickIntents {
|
|||
// endregion
|
||||
|
||||
// region Fee
|
||||
fun feeReload()
|
||||
|
||||
fun onFeeSelectorClick(feeType: FeeType)
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
|
|
@ -50,5 +55,9 @@ interface SendClickIntents {
|
|||
fun showFee()
|
||||
|
||||
fun onExploreClick(txUrl: String)
|
||||
|
||||
fun onAmountReduceClick(reducedAmount: String)
|
||||
|
||||
fun onAmountReduceIgnoreClick()
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -5,17 +5,23 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.*
|
||||
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.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
|
|
@ -31,23 +37,14 @@ 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.*
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotificationFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.getFee
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
|
@ -58,6 +55,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
|
|
@ -70,8 +68,11 @@ internal class SendViewModel @Inject constructor(
|
|||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
|
|
@ -93,23 +94,48 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
)
|
||||
|
||||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
)
|
||||
|
||||
private val eventStateFactory = SendEventStateFactory(
|
||||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
feeStateFactory = feeStateFactory,
|
||||
)
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
clickIntents = this,
|
||||
getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase,
|
||||
)
|
||||
|
||||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
// todo convert to StateFlow
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var isAmountSubtractAvailable: Boolean = false
|
||||
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
|
||||
|
|
@ -118,11 +144,14 @@ internal class SendViewModel @Inject constructor(
|
|||
private var feeJobHolder = JobHolder()
|
||||
private var addressValidationJobHolder = JobHolder()
|
||||
private var sendNotificationsJobHolder = JobHolder()
|
||||
private var feeNotificationsJobHolder = JobHolder()
|
||||
private var qrScannerJobHolder = JobHolder()
|
||||
|
||||
private var sendIdleTimer = 0L
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
getFee()
|
||||
onStateActive()
|
||||
}
|
||||
|
||||
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
|
||||
|
|
@ -136,10 +165,13 @@ internal class SendViewModel @Inject constructor(
|
|||
getUserWalletUseCase(userWalletId).fold(
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
checkIfSubtractAvailable()
|
||||
getCurrenciesStatusUpdates(owner, wallet)
|
||||
},
|
||||
ifLeft = {
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
|
|
@ -280,37 +312,16 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState.currentState
|
||||
.filter { it == SendUiStateType.Fee }
|
||||
.onEach {
|
||||
val amountState = uiState.amountState ?: return@onEach
|
||||
val recipientState = uiState.recipientState ?: return@onEach
|
||||
val amount = amountState.amountTextField.value.toBigDecimal()
|
||||
|
||||
uiState = stateFactory.onFeeOnLoadingState()
|
||||
getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeFee ->
|
||||
maybeFee.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.onFeeOnLoadedState(it)
|
||||
},
|
||||
ifLeft = {
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}.launchIn(viewModelScope)
|
||||
}.saveIn(feeJobHolder)
|
||||
private fun onStateActive() {
|
||||
uiState.currentState
|
||||
.onEach {
|
||||
when (it) {
|
||||
SendUiStateType.Fee -> loadFee()
|
||||
SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun updateNotifications() {
|
||||
|
|
@ -323,6 +334,16 @@ internal class SendViewModel @Inject constructor(
|
|||
.saveIn(sendNotificationsJobHolder)
|
||||
}
|
||||
|
||||
private fun updateFeeNotifications() {
|
||||
feeNotificationFactory.create()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(feeNotificationsJobHolder)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() = stateRouter.onBackClick()
|
||||
|
|
@ -331,6 +352,10 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name)
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage))
|
||||
}
|
||||
|
||||
override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
// endregion
|
||||
|
|
@ -346,12 +371,15 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onMaxValueClick() {
|
||||
val amountState = uiState.amountState ?: return
|
||||
val amount = if (amountState.isFiatValue) {
|
||||
amountState.cryptoCurrencyStatus.value.fiatAmount
|
||||
val amountTextField = amountState.amountTextField
|
||||
val (amount, decimals) = if (amountTextField.isFiatValue) {
|
||||
cryptoCurrencyStatus.value.fiatAmount to amountTextField.fiatAmount.decimals
|
||||
} else {
|
||||
amountState.cryptoCurrencyStatus.value.amount
|
||||
cryptoCurrencyStatus.value.amount to amountTextField.cryptoAmount.decimals
|
||||
}
|
||||
if (amount != null && !amount.isZero()) {
|
||||
onAmountValueChange(amount.parseBigDecimal(decimals))
|
||||
}
|
||||
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -415,16 +443,56 @@ internal class SendViewModel @Inject constructor(
|
|||
// endregion
|
||||
|
||||
// region fee
|
||||
override fun feeReload() = loadFee()
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = stateFactory.onFeeSelectedState(feeType)
|
||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||
updateFeeNotifications()
|
||||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState = stateFactory.onCustomFeeValueChange(index, value)
|
||||
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
updateFeeNotifications()
|
||||
}
|
||||
|
||||
override fun onSubtractSelect(value: Boolean) {
|
||||
uiState = stateFactory.onSubtractSelect(value)
|
||||
uiState = feeStateFactory.onSubtractSelect(value)
|
||||
updateFeeNotifications()
|
||||
}
|
||||
|
||||
private fun loadFee() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
||||
uiState = callFeeUseCase()?.fold(
|
||||
ifRight = { fees ->
|
||||
feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable)
|
||||
},
|
||||
ifLeft = {
|
||||
feeStateFactory.onFeeOnErrorState()
|
||||
},
|
||||
) ?: feeStateFactory.onFeeOnErrorState()
|
||||
updateFeeNotifications()
|
||||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun checkIfSubtractAvailable() {
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold(
|
||||
ifRight = { it },
|
||||
ifLeft = { false },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
||||
val amountState = uiState.amountState ?: return null
|
||||
val recipientState = uiState.recipientState ?: return null
|
||||
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
|
||||
|
||||
return getFeeUseCase.invoke(
|
||||
amount = amount,
|
||||
destination = recipientState.addressTextField.value,
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -433,68 +501,145 @@ internal class SendViewModel @Inject constructor(
|
|||
val sendState = uiState.sendState
|
||||
if (sendState.isSuccess) popBackStack()
|
||||
|
||||
uiState = stateFactory.getSendingStateUpdate(true)
|
||||
viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() }
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
if (System.currentTimeMillis() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
||||
verifyAndSendTransaction()
|
||||
} else {
|
||||
onCheckFeeUpdate()
|
||||
}
|
||||
sendIdleTimer = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
|
||||
override fun showAmount() = stateRouter.showAmount()
|
||||
|
||||
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
|
||||
override fun showRecipient() = stateRouter.showRecipient()
|
||||
|
||||
override fun showFee() = stateRouter.showFee(isFromSend = true)
|
||||
override fun showFee() = stateRouter.showFee()
|
||||
|
||||
override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl)
|
||||
|
||||
private suspend fun verifyAndSendTransaction() {
|
||||
override fun onAmountReduceClick(reducedAmount: String) {
|
||||
uiState = stateFactory.getOnAmountValueChange(reducedAmount)
|
||||
uiState = sendNotificationFactory.dismissHighFeeWarningState()
|
||||
loadFee()
|
||||
}
|
||||
|
||||
override fun onAmountReduceIgnoreClick() {
|
||||
uiState = sendNotificationFactory.dismissHighFeeWarningState()
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction() {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val fee = feeSelectorState.getFee()
|
||||
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) {
|
||||
feeState.receivedAmountValue
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
|
||||
val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
createTransactionUseCase(
|
||||
amount = amountToSend.convertToAmount(cryptoCurrency),
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = recipient,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
error = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransaction(txData)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
// val transactionErrors = walletManagersFacade.validateTransaction(
|
||||
// amount = amountToSend,
|
||||
// fee = fee.amount,
|
||||
// userWalletId = userWalletId,
|
||||
// network = cryptoCurrency.network,
|
||||
// )
|
||||
|
||||
createTransactionUseCase(
|
||||
amount = amountToSend,
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = recipient,
|
||||
userWalletId = userWalletId,
|
||||
private suspend fun sendTransaction(txData: TransactionData) {
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
uiState = stateFactory.getSendingStateUpdate(false)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = stateFactory.getTransactionSendState(txData)
|
||||
scheduleBalanceUpdate()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleBalanceUpdate() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
delay(BALANCE_UPDATE_DELAY)
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCheckFeeUpdate() {
|
||||
val isSending = uiState.sendState.isSending
|
||||
val isSuccess = uiState.sendState.isSuccess
|
||||
val noErrorNotifications = uiState.sendState.notifications.none { it is SendNotification.Error }
|
||||
|
||||
if (!isSending && !isSuccess && noErrorNotifications) {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val feeUpdatedState = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUpdatedAlert(
|
||||
fee = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onFeeNotIncreased = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
verifyAndSendTransaction()
|
||||
},
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getGenericErrorState(
|
||||
error = (it as? GetFeeError.DataError)?.cause,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
uiState = if (feeUpdatedState != null) {
|
||||
feeUpdatedState
|
||||
} else {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val DEFAULT_VALUE = "0.00"
|
||||
private const val CHECK_FEE_UPDATE_DELAY = 60_000L
|
||||
private const val BALANCE_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ internal class ErrorsDataConverter(
|
|||
private val jsonAdapter: JsonAdapter<ExpressErrorResponse>,
|
||||
) : Converter<String, DataError> {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod")
|
||||
override fun convert(value: String): DataError {
|
||||
try {
|
||||
val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError
|
||||
|
|
@ -23,6 +23,7 @@ internal class ErrorsDataConverter(
|
|||
2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code)
|
||||
2240 -> DataError.ExchangeNotPossibleError(code = error.code)
|
||||
2250 -> tryParseExchangeTooSmallAmountError(error = error)
|
||||
2251 -> tryParseExchangeTooBigAmountError(error = error)
|
||||
2260 -> tryParseExchangeNotEnoughAllowanceError(error = error)
|
||||
2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code)
|
||||
2280 -> DataError.ExchangeInvalidAddressError(code = error.code)
|
||||
|
|
@ -44,6 +45,16 @@ internal class ErrorsDataConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeTooBigAmountError(error: ExpressError): DataError {
|
||||
val minAmount = error.value?.maxAmount ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
return DataError.ExchangeTooBigAmountError(
|
||||
code = error.code,
|
||||
amount = createFromAmountWithOffset(minAmount, decimals),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError {
|
||||
val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code)
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue