Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-11 19:47:53 +03:00
commit 8a9547827b
19 changed files with 157 additions and 19 deletions

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -37,6 +37,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color
Blockchain.Dash -> R.drawable.ic_dash_no_color
Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color
Blockchain.TON, Blockchain.TONTestnet -> R.drawable.ic_ton_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -50,7 +50,7 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,
is WalletDataModel.NoAccount,
-> if (walletData.isCustom || walletData.fiatRate == null) {
-> if (walletData.isCustom && walletData.fiatRate == null) {
TotalFiatBalanceStatus.Error
} else {
TotalFiatBalanceStatus.Loaded
@ -65,7 +65,7 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
private fun Sequence<WalletDataModel>.calculateTotalFiatAmount(): BigDecimal? {
return this
.filterNot { it.isCustom }
.filterNot { it.isCustom && it.fiatRate == null }
.map { walletData ->
walletData.fiatRate
?.takeUnless { walletData.status.isErrorStatus }

View file

@ -65,6 +65,10 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
sealed class XrpDestinationTag : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : XrpDestinationTag()
}
sealed class TonMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : TonMemo()
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {

View file

@ -4,6 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
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.BlockchainSdkError
@ -170,6 +171,7 @@ private fun sendTransaction(
transactionExtras.xrpDestinationTag?.tag?.let {
txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it))
}
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
scope.launch {
val updateWalletResult = walletManager.safeUpdate()

View file

@ -6,11 +6,13 @@ import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction.BinanceMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.Prepare
import com.tangem.tap.features.send.redux.TransactionExtrasAction.Release
import com.tangem.tap.features.send.redux.TransactionExtrasAction.TonMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.XlmMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.XrpDestinationTag
import com.tangem.tap.features.send.redux.states.BinanceMemoState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.TonMemoState
import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.XlmMemoState
@ -28,6 +30,7 @@ class TransactionExtrasReducer : SendInternalReducer {
is XlmMemo -> handleXlmMemo(action, sendState, sendState.transactionExtrasState)
is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
}
@ -56,6 +59,7 @@ class TransactionExtrasReducer : SendInternalReducer {
}
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState())
Blockchain.TON, Blockchain.TONTestnet -> TransactionExtrasState(tonMemoState = TonMemoState())
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
@ -140,4 +144,15 @@ class TransactionExtrasReducer : SendInternalReducer {
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleTonMemo(action: TonMemo, sendState: SendState, infoState: TransactionExtrasState): SendState {
val result = when (action) {
is TonMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(tonMemoState = TonMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
}

View file

@ -30,6 +30,7 @@ data class TransactionExtrasState(
val xlmMemo: XlmMemoState? = null,
val binanceMemo: BinanceMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null,
val tonMemoState: TonMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
}
@ -98,6 +99,12 @@ data class XrpDestinationTagState(
}
}
data class TonMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
enum class TransactionExtraError {
INVALID_DESTINATION_TAG,
INVALID_XLM_MEMO,

View file

@ -60,7 +60,6 @@ import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@ -173,6 +172,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
.launchIn(mainScope)
etTonMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.tonMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
.launchIn(mainScope)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -331,7 +339,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
@ExperimentalCoroutinesApi
fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = addTextChangedListener { editable -> trySend(editable?.toString() ?: "") }
awaitClose { removeTextChangedListener(watcher) }

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -22,7 +21,7 @@ object KaspaWarningDialog {
dialog.onOk()
}
setOnDismissListener {
store.dispatchDialogHide()
store.dispatch(SendAction.Dialog.Hide)
}
}
.create()

View file

@ -46,8 +46,8 @@ import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class SendStateSubscriber(fragment: BaseStoreFragment) :
FragmentStateSubscriber<SendState>(fragment) {
@Suppress("LargeClass")
class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber<SendState>(fragment) {
private var dialog: Dialog? = null
@ -79,6 +79,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
showView(xlmMemoContainer, infoState.xlmMemo)
showView(xrpDestinationTagContainer, infoState.xrpDestinationTag)
showView(binanceMemoContainer, infoState.binanceMemo)
showView(tonMemoContainer, infoState.tonMemoState)
infoState.xlmMemo?.let {
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
@ -115,6 +116,16 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
etBinanceMemo.setText(it.viewFieldValue.value)
}
}
infoState.tonMemoState?.let {
if (infoState.tonMemoState.error != null) {
tilTonMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etTonMemo.setText(it.viewFieldValue.value)
}
}
}
private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) {

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#EBEBEB"
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:fillColor="#C9C9C9"
android:fillType="evenOdd"
android:pathData="M7.596,6.5L14.457,6.5C14.7,6.5 14.943,6.536 15.196,6.654C15.5,6.795 15.661,7.019 15.774,7.184C15.783,7.197 15.791,7.21 15.799,7.224C15.932,7.46 16,7.715 16,7.989C16,8.25 15.938,8.534 15.799,8.782C15.797,8.784 15.796,8.787 15.795,8.789L11.46,16.236C11.364,16.4 11.188,16.501 10.998,16.5C10.808,16.499 10.633,16.397 10.538,16.232L6.283,8.802C6.282,8.8 6.28,8.797 6.279,8.795C6.182,8.635 6.031,8.387 6.005,8.067C5.981,7.772 6.047,7.477 6.195,7.221C6.343,6.965 6.566,6.761 6.834,6.635C7.121,6.501 7.412,6.5 7.596,6.5ZM10.468,7.564H7.596C7.407,7.564 7.335,7.575 7.284,7.599C7.214,7.632 7.155,7.686 7.116,7.754C7.076,7.822 7.059,7.9 7.065,7.979C7.069,8.025 7.087,8.076 7.196,8.256C7.198,8.259 7.201,8.263 7.203,8.267L10.468,13.969V7.564ZM11.532,7.564V13.997L14.873,8.258C14.911,8.189 14.936,8.09 14.936,7.989C14.936,7.907 14.919,7.836 14.881,7.763C14.841,7.705 14.817,7.675 14.797,7.654C14.779,7.636 14.766,7.627 14.747,7.618C14.668,7.581 14.587,7.564 14.457,7.564H11.532Z" />
</vector>

View file

@ -228,6 +228,37 @@
</FrameLayout>
<FrameLayout
android:id="@+id/tonMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilTonMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etTonMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Ton memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -33,6 +33,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22
"DASH" -> R.drawable.img_dash_22
"KAS" -> R.drawable.img_kaspa_22
"The-Open-Network", "The-Open-Network/test" -> R.drawable.img_ton_22
else -> R.drawable.ic_alert_24
}
}
@ -72,6 +73,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int {
"kusama" -> R.drawable.img_kusama_22
"dash" -> R.drawable.img_dash_22
"kaspa" -> R.drawable.img_kaspa_22
"ton" -> R.drawable.img_ton_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#0088CC"
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:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M7.596,6.5L14.457,6.5C14.7,6.5 14.943,6.536 15.196,6.654C15.5,6.795 15.661,7.019 15.774,7.184C15.783,7.197 15.791,7.21 15.799,7.224C15.932,7.46 16,7.715 16,7.989C16,8.25 15.938,8.534 15.799,8.782C15.797,8.784 15.796,8.787 15.795,8.789L11.46,16.236C11.364,16.4 11.188,16.501 10.998,16.5C10.808,16.499 10.633,16.397 10.538,16.232L6.283,8.802C6.282,8.8 6.28,8.797 6.279,8.795C6.182,8.635 6.031,8.387 6.005,8.067C5.981,7.772 6.047,7.477 6.195,7.221C6.343,6.965 6.566,6.761 6.834,6.635C7.121,6.501 7.412,6.5 7.596,6.5ZM10.468,7.564H7.596C7.407,7.564 7.335,7.575 7.284,7.599C7.214,7.632 7.155,7.686 7.116,7.754C7.076,7.822 7.059,7.9 7.065,7.979C7.069,8.025 7.087,8.076 7.196,8.256C7.198,8.259 7.201,8.263 7.203,8.267L10.468,13.969V7.564ZM11.532,7.564V13.997L14.873,8.258C14.911,8.189 14.936,8.09 14.936,7.989C14.936,7.907 14.919,7.836 14.881,7.763C14.841,7.705 14.817,7.675 14.797,7.654C14.779,7.636 14.766,7.627 14.747,7.618C14.668,7.581 14.587,7.564 14.457,7.564H11.532Z" />
</vector>

View file

@ -44,11 +44,11 @@ platform :android do
})
end
desc "Build internal and release APKs"
desc "Build external and release APKs"
lane :build do |options|
gradle(
task: "clean assemble",
build_type: "Internal",
build_type: "External",
properties: {
'versionCode' => options[:versionCode],
'versionName' => options[:versionName],

View file

@ -244,7 +244,7 @@ internal class SwapInteractorImpl @Inject constructor(
return repository.getTangemFee()
}
fun getTokenDecimals(token: Currency): Int {
private fun getTokenDecimals(token: Currency): Int {
return if (token is Currency.NonNativeToken) {
token.decimalCount
} else {
@ -351,15 +351,15 @@ internal class SwapInteractorImpl @Inject constructor(
swapStateData = null,
formattedFee = null,
)
return updatePermissionState(
val quotesState = updatePermissionState(
networkId = networkId,
fromToken = fromToken,
quotesLoadedState = swapState,
).copy(
preparedSwapConfigState = PreparedSwapConfigState(
)
return quotesState.copy(
preparedSwapConfigState = quotesState.preparedSwapConfigState.copy(
isAllowedToSpend = isAllowedToSpend,
isBalanceEnough = isBalanceWithoutFeeEnough,
isFeeEnough = true,
),
)
} else {
@ -533,7 +533,14 @@ internal class SwapInteractorImpl @Inject constructor(
decimals = transactionManager.getNativeTokenDecimals(networkId),
currency = userWalletManager.getNetworkCurrency(networkId),
) + feeFiat
val isFeeEnough = checkFeeIsEnough(
fee = feeData.fee.value,
spendAmount = SwapAmount.zeroSwapAmount(),
networkId = networkId,
fromToken = fromToken,
)
return quotesLoadedState.copy(
fee = formattedFee,
permissionState = PermissionDataState.PermissionReadyForRequest(
currency = fromToken.symbol,
amount = INFINITY_SYMBOL,
@ -546,6 +553,9 @@ internal class SwapInteractorImpl @Inject constructor(
approveModel = transactionData,
),
),
preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy(
isFeeEnough = isFeeEnough,
),
)
}

View file

@ -6,7 +6,14 @@ import java.math.BigDecimal
data class SwapAmount(
val value: BigDecimal,
val decimals: Int,
)
) {
companion object {
fun zeroSwapAmount(): SwapAmount {
return SwapAmount(BigDecimal.ZERO, 0)
}
}
}
fun SwapAmount.formatToUIRepresentation(): String {
return value.toFormattedString(decimals = decimals)

View file

@ -1,5 +1,12 @@
package com.tangem.feature.swap.domain.models.domain
/**
* Prepared swap config state that contains flags to determine
*
* @property isAllowedToSpend shows is token allowed to spend
* @property isBalanceEnough shows is balance of token enough
* @property isFeeEnough shows is amount of main coin enough for fee
*/
data class PreparedSwapConfigState(
val isAllowedToSpend: Boolean,
val isBalanceEnough: Boolean,

View file

@ -71,9 +71,9 @@ kotlinSerialization = "1.4.1"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-185"
tangemBlockchainSdk = "develop-189"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-208"
tangemCardSdk = "develop-209"
# tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem