Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-13 20:27:45 +08:00
commit e77eff8935
27 changed files with 567 additions and 333 deletions

View file

@ -205,4 +205,4 @@
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View file

@ -49,6 +49,7 @@
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -58,40 +59,25 @@
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:pathPrefix="/ndef"
android:host="www.tangem.com"
android:scheme="http" />
<data
android:pathPrefix="/ndef"
android:host="www.tangem.com"
android:scheme="https" />
<data
android:pathPrefix="/ndef"
android:host="tangem.com"
android:scheme="http" />
<data
android:pathPrefix="/ndef"
android:host="tangem.com"
android:scheme="https" />
<data
android:pathPrefix="/ndef"
android:host="app.tangem.com"
android:scheme="https" />
</intent-filter>
<data android:scheme="http" />
<data android:host="www.tangem.com" />
<data android:pathPrefix="/ndef" />
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<data android:scheme="https" />
<data android:host="www.tangem.com" />
<data android:pathPrefix="/ndef" />
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
<data android:scheme="http" />
<data android:host="tangem.com" />
<data android:pathPrefix="/ndef" />
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
<data android:host="tangem.com" />
<data android:pathPrefix="/ndef" />
<data android:scheme="https" />
<data android:host="app.tangem.com" />
<data android:pathPrefix="/ndef" />
</intent-filter>
<intent-filter>

View file

@ -87,12 +87,54 @@ sealed class AnalyticsParam {
}
sealed class TxSentFrom(val value: String) {
object Send : TxSentFrom("Send")
object Swap : TxSentFrom("Swap")
data class Send(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Swap"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
object WalletConnect : TxSentFrom("WalletConnect")
object Sell : TxSentFrom("Sell")
}
sealed interface TxData {
val blockchain: String
val token: String
val feeType: FeeType
}
sealed class FeeType(val value: String) {
object Fixed : FeeType("Fixed")
object Min : FeeType("Min")
object Normal : FeeType("Normal")
object Max : FeeType("Max")
companion object {
fun fromString(feeType: String): FeeType {
return when (feeType) {
Min.value -> Min
Normal.value -> Normal
Max.value -> Max
Fixed.value -> Fixed
else -> Fixed
}
}
}
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
@ -100,9 +142,13 @@ sealed class AnalyticsParam {
}
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token"
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val CURRENCY = "Currency"

View file

@ -51,7 +51,17 @@ sealed class Basic(
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom) : Basic(
event = "Transaction sent",
params = mapOf(AnalyticsParam.SOURCE to sentFrom.value),
params = buildMap {
this[AnalyticsParam.SOURCE] = sentFrom.value
if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.TOKEN] = sentFrom.token
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType
}
},
)
class ScanError(error: Throwable) : Basic(

View file

@ -77,12 +77,6 @@ sealed class Token(
class ButtonPaste : Send("Button - Paste")
class ButtonQRCode : Send("Button - QR Code")
class ButtonSwapCurrency : Send("Button - Swap Currency")
class TransactionSent(type: CurrencyType, error: Throwable? = null) : Send(
event = "Transaction Sent",
params = mapOf("Token" to type.value),
error = error,
)
}
sealed class Topup(

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.disclaimer.ui
import android.os.Bundle
import android.view.View
import android.view.View.OVER_SCROLL_NEVER
import android.webkit.WebView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.ui.fragments.setStatusBarColor
@ -27,21 +28,6 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
private val webViewClient = DisclaimerWebViewClient()
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (store.state.disclaimerState.showedFromScreen) {
AppScreen.Home -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
AppScreen.Details -> {
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
else -> {}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(handler = this)
@ -86,11 +72,29 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
super.onStop()
}
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (store.state.disclaimerState.showedFromScreen) {
AppScreen.Home -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
AppScreen.Details -> {
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
else -> {}
}
}
override fun handleOnBackPressed() {
store.dispatch(DisclaimerAction.OnBackPressed)
}
override fun newState(state: DisclaimerState) = with(binding) {
if (activity == null || view == null) return
updateUiVisibility(state.disclaimer, state.progressState)
binding.webView.loadUrl(state.disclaimer.getUri().toString())
}
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
@ -111,16 +115,19 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
}
ProgressState.Error -> {
root.beginDelayedTransition()
webView.hide()
groupAccept.hide()
webView.show()
groupAccept.show()
groupLoading.hide()
groupError.show()
groupError.hide()
webView.loadLocalTermsOfServices()
}
else -> {
webView.loadUrl(disclaimer.getUri().toString())
}
else -> {}
}
}
override fun handleOnBackPressed() {
store.dispatch(DisclaimerAction.OnBackPressed)
private fun WebView.loadLocalTermsOfServices() {
loadData(localTermsOfServices, "text/html", "UTF-8")
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.tap.features.disclaimer.ui
internal val localTermsOfServices = """
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Legal Disclaimer</title>
<style>
@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Light.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Light.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Light.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Light.ttf) format("truetype");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Regular.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Regular.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Regular.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Regular.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Medium.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Medium.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Medium.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Medium.ttf) format("truetype");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Semibold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Semibold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Bold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Bold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Bold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Bold.ttf) format("truetype");font-weight:700;font-style:normal;font-display:swap}*{margin:0;padding:0}body{padding:0;margin:1rem;color:rgb(0 0 0);font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}h1{font-weight:700;font-size:30px;line-height:1.2;margin-bottom:1rem}p{font-size:16px;line-height:20px;letter-spacing:-.24px;margin-bottom:1rem}
</style>
</head>
<body><h1>Legal Disclaimer</h1>
<p>&nbsp; &nbsp; 1. Tangem application (Software)</p>
<p>&nbsp; &nbsp; The Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The
Software DOES NOT:</p>
<p>&nbsp; &nbsp; a) Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain
wallets holding digital assets, including crypto-currency.</p>
<p>&nbsp; &nbsp; b) Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases
that can be used to restore or to copy private (secret) keys to blockchain wallets holding digital assets, including
crypto-currency.</p>
<p>&nbsp; &nbsp; c) Provide exchange, trading, investment services on behalf of Tangem AG.</p>
<p>&nbsp; &nbsp; 2. Risks related to the use of Software</p>
<p>&nbsp; &nbsp; Tangem will not be responsible for any losses, damages or claims arising from events falling within the
scope of the following five categories:</p>
<p>&nbsp; &nbsp; a) Mistakes made by the user of any cryptocurrency-related software or service, e.g., forgotten
passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Cards.</p>
<p>&nbsp; &nbsp; b) Problems of Software and/or any blockchain- or cryptocurrency- related software or service, e.g.,
corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.</p>
<p>&nbsp; &nbsp; c) Technical failures in the hardware of the user, including Cards, of any cryptocurrency-related
software or service, e.g., data loss due to a faulty or damaged storage device.</p>
<p>&nbsp; &nbsp; d) Security problems experienced by the user of any cryptocurrency-related software or service, e.g.,
unauthorized access to users&apos; wallets and/or accounts.</p>
<p>&nbsp; &nbsp; e) Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy
of service providers, information security attacks on service providers, and fraud conducted by third parties.</p>
<p>&nbsp; &nbsp; 3. Trading and Investment risks</p>
<p>&nbsp; &nbsp; There is considerable exposure to risk in any crypto-currency or other digital asset exchange
transaction. Any transaction involving currencies involves risks including, but not limited to, the potential for
changing economic conditions that may substantially affect the price or liquidity of a currency. Investments in
crypto-currency exchange speculation may also be susceptible to sharp rises and falls as the relevant market values
fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.</p>
<p>&nbsp; &nbsp; 4. Electronic Trading Risks</p>
<p>&nbsp; &nbsp; Before you engage in transactions using an electronic system, you should carefully review the rules and
regulations of the exchanges offering the system and/or listing the instruments you intend to trade. Online trading
has inherent risk due to system response and access times that may vary due to market conditions, system
performance, and other factors. You should understand these and additional risks before trading.</p>
<p>&nbsp; &nbsp; 5. Compliance with tax obligations &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; The users of the Software are solely responsible to determinate what, if any, taxes apply to their
crypto-currency transactions. The owners of, or contributors to, the Software are NOT responsible for determining
the taxes that apply to crypto-currency transactions. &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; 6. No warranties &nbsp; &nbsp; &nbsp; &nbsp;</p>
<p>&nbsp; &nbsp; The Software is provided on an &quot;as is&quot; basis without any warranties of any kind regarding the
Software and/or any content, data, materials and/or services provided on the Software. &nbsp; &nbsp; &nbsp;
&nbsp;</p>
<p>&nbsp; &nbsp; 7. Limitation of liability</p>
<p>&nbsp; &nbsp; Unless otherwise required by law, in no event shall the owners of, or contributors to, the Software be
liable for any damages of any kind, including, but not limited to, loss of use, loss of profits, or loss of data
arising out of or in any way connected with the use of the Software. In no way are the owners of, or contributors
to, the Software responsible for the actions, decisions, or other behavior taken or not taken by you in reliance
upon the Software.</p>
<p>&nbsp; &nbsp; 8. Last amendment</p>
<p>&nbsp; &nbsp; This disclaimer was amended for the last time on October 1st, 2020.</p></body>
</html>
""".trimIndent()

View file

@ -7,28 +7,19 @@ 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
import com.tangem.blockchain.common.TransactionError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -39,25 +30,11 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -65,7 +42,7 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.*
import java.util.EnumSet
/**
[REDACTED_AUTHOR]
@ -134,9 +111,16 @@ private fun verifyAndSendTransaction(
},
sendAllCallback = {
sendTransaction(
action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, sendState.externalTransactionData,
dispatch,
action = action,
walletManager = walletManager,
amountToSend = amountToSend,
feeAmount = feeAmount,
feeType = sendState.feeState.selectedFeeType,
destinationAddress = destinationAddress,
transactionExtras = sendState.transactionExtrasState,
card = card,
externalTransactionData = sendState.externalTransactionData,
dispatch = dispatch,
)
},
reduceAmount,
@ -145,8 +129,16 @@ private fun verifyAndSendTransaction(
}
else -> {
sendTransaction(
action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch,
action = action,
walletManager = walletManager,
amountToSend = amountToSend,
feeAmount = feeAmount,
feeType = sendState.feeState.selectedFeeType,
destinationAddress = destinationAddress,
transactionExtras = sendState.transactionExtrasState,
card = card,
externalTransactionData = sendState.externalTransactionData,
dispatch = dispatch,
)
}
}
@ -158,6 +150,7 @@ private fun sendTransaction(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
feeType: FeeType,
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: CardDTO,
@ -243,17 +236,23 @@ private fun sendTransaction(
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
tangemSdk.config.linkedTerminal = linkedTerminalState
val currencyType = AnalyticsParam.CurrencyType.Amount(amountToSend)
when (sendResult) {
is SimpleResult.Success -> {
Analytics.send(Token.Send.TransactionSent(currencyType))
dispatch(SendAction.SendSuccess)
if (externalTransactionData != null) {
Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Sell))
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Send))
Analytics.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Send(
blockchain = walletManager.wallet.blockchain.fullName,
token = amountToSend.currencySymbol,
feeType = feeType.convertToAnalyticsFeeType(),
),
),
)
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
@ -271,8 +270,6 @@ private fun sendTransaction(
)
val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext
Analytics.send(Token.Send.TransactionSent(currencyType, error))
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.common.Amount
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.wallet.redux.ProgressState
import java.math.BigDecimal
@ -8,7 +9,16 @@ import java.math.BigDecimal
[REDACTED_AUTHOR]
*/
enum class FeeType {
SINGLE, LOW, NORMAL, PRIORITY
SINGLE, LOW, NORMAL, PRIORITY;
}
fun FeeType.convertToAnalyticsFeeType(): AnalyticsParam.FeeType {
return when (this) {
FeeType.SINGLE -> AnalyticsParam.FeeType.Fixed
FeeType.LOW -> AnalyticsParam.FeeType.Min
FeeType.NORMAL -> AnalyticsParam.FeeType.Normal
FeeType.PRIORITY -> AnalyticsParam.FeeType.Max
}
}
data class FeeState(

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
@ -21,6 +22,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
import kotlinx.collections.immutable.ImmutableCollection
/** This const configures how many items show to user and hide more than*/
private const val MAX_VISIBLE_BRIEF_ICONS = 9
/**
[REDACTED_AUTHOR]
*/
@ -37,10 +41,25 @@ internal fun BriefNetworksList(
exit = fadeOut(),
) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
networks.forEach { network ->
key(network.name + network.protocolName) {
BriefNetworkItem(model = network)
val iterator = networks.iterator()
var index = 0
while (iterator.hasNext()) {
val network = iterator.next()
if (index < MAX_VISIBLE_BRIEF_ICONS) {
key(network.name + network.protocolName) {
BriefNetworkItem(model = network)
}
} else {
if (networks.size < MAX_VISIBLE_BRIEF_ICONS + 1) {
key(network.name + network.protocolName) {
BriefNetworkItem(model = network)
}
} else {
HasMoreItem(moreCount = networks.size - index)
break
}
}
index++
}
}
}
@ -50,8 +69,8 @@ internal fun BriefNetworksList(
[REDACTED_AUTHOR]
*/
@Composable
internal fun BriefNetworkItem(model: NetworkItemState) {
Box(modifier = Modifier.size(size = TangemTheme.dimens.size20)) {
internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) {
Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) {
Image(
painter = painterResource(id = model.iconResId.value),
contentDescription = null,
@ -76,4 +95,21 @@ internal fun BriefNetworkItem(model: NetworkItemState) {
}
}
}
}
@Composable
internal fun HasMoreItem(moreCount: Int) {
Box(
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.clip(CircleShape)
.background(TangemTheme.colors.control.unchecked),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "+$moreCount",
style = TangemTheme.typography.overline,
color = TangemTheme.colors.text.tertiary,
)
}
}

View file

@ -110,9 +110,10 @@ internal class WelcomeMiddleware {
}
private fun handleInitialIntent(intent: Intent?) {
val isBackgroundScanWasHandled = intentHandler.handleBackgroundScan(intent, hasSavedUserWallets = true)
val isBackgroundScanNotHandled = !intentHandler.handleBackgroundScan(intent, hasSavedUserWallets = true)
val hasNotIncompletedBackup = !backupService.hasIncompletedBackup
if (!isBackgroundScanWasHandled) {
if (isBackgroundScanNotHandled && hasNotIncompletedBackup) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}

View file

@ -34,14 +34,11 @@ class TransactionManagerImpl(
) : TransactionManager {
override suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
txData: ApproveTxData,
derivationPath: String?,
analyticsData: AnalyticsData,
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
walletManager.update()
val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain)
@ -49,40 +46,55 @@ class TransactionManagerImpl(
walletManager = walletManager,
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
feeAmount = txData.feeAmount,
gasLimit = txData.gasLimit,
destinationAddress = txData.destinationAddress,
dataToSign = txData.dataToSign,
successEvent = AnalyticsParam.TxSentFrom.Approve(
blockchain = blockchain.fullName,
token = analyticsData.tokenSymbol,
feeType = AnalyticsParam.FeeType.fromString(analyticsData.feeType),
permissionType = analyticsData.permissionType ?: "",
),
)
}
override suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
txData: SwapTxData,
isSwap: Boolean,
currencyToSend: Currency,
derivationPath: String?,
analyticsData: AnalyticsData,
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
walletManager.update()
val amount = if (isSwap) {
createAmountForSwap(amountToSend, currencyToSend, blockchain)
createAmountForSwap(txData.amountToSend, txData.currencyToSend, blockchain)
} else {
createAmount(amountToSend, currencyToSend, blockchain)
createAmount(txData.amountToSend, txData.currencyToSend, blockchain)
}
val successEvent = if (isSwap) {
AnalyticsParam.TxSentFrom.Swap(
blockchain = blockchain.fullName,
token = analyticsData.tokenSymbol,
feeType = AnalyticsParam.FeeType.fromString(analyticsData.feeType),
)
} else {
AnalyticsParam.TxSentFrom.Send(
blockchain = blockchain.fullName,
token = analyticsData.tokenSymbol,
feeType = AnalyticsParam.FeeType.fromString(analyticsData.feeType),
)
}
return sendTransactionInternal(
walletManager = walletManager,
amount = amount,
blockchain = blockchain,
feeAmount = feeAmount,
gasLimit = gasLimit,
destinationAddress = destinationAddress,
dataToSign = dataToSign,
feeAmount = txData.feeAmount,
gasLimit = txData.gasLimit,
destinationAddress = txData.destinationAddress,
dataToSign = txData.dataToSign,
successEvent = successEvent,
)
}
@ -95,6 +107,7 @@ class TransactionManagerImpl(
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
successEvent: AnalyticsParam.TxSentFrom,
): SendTxResult {
val txData = walletManager.createTransaction(
amount = amount,
@ -110,7 +123,10 @@ class TransactionManagerImpl(
FirebaseCrashlytics.getInstance().recordException(ex)
return SendTxResult.UnknownError(ex)
}
return handleSendResult(sendResult)
return handleSendResult(
result = sendResult,
successEvent = successEvent,
)
}
override fun getExplorerTransactionLink(networkId: String, txAddress: String): String {
@ -319,10 +335,10 @@ class TransactionManagerImpl(
}
}
private fun handleSendResult(result: SimpleResult): SendTxResult {
private fun handleSendResult(result: SimpleResult, successEvent: AnalyticsParam.TxSentFrom): SendTxResult {
when (result) {
is SimpleResult.Success -> {
analytics.send(Basic.TransactionSent(AnalyticsParam.TxSentFrom.Swap))
analytics.send(Basic.TransactionSent(sentFrom = successEvent))
return SendTxResult.Success
}
is SimpleResult.Failure -> {

View file

@ -132,7 +132,7 @@
android:paddingStart="3dp"
android:paddingTop="3dp"
android:paddingEnd="3dp"
app:layout_constraintBottom_toTopOf="@id/v_shipping_discount_background"
app:layout_constraintBottom_toTopOf="@id/ll_total"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:selectionRequired="true"
@ -156,162 +156,125 @@
app:chipMinTouchTargetSize="0dp" />
</com.google.android.material.chip.ChipGroup>
<View
android:id="@+id/v_shipping_discount_background"
<LinearLayout
android:id="@+id/ll_total"
android:layout_width="match_parent"
android:layout_height="93dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_rectangle_rounded_4"
app:layout_constraintBottom_toTopOf="@id/v_total_background" />
<ImageView
android:id="@+id/iv_delivery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:src="@drawable/ic_shipping"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<TextView
android:id="@+id/tv_delivery_worldwide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:text="@string/shop_shipping"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintStart_toEndOf="@id/iv_delivery"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<TextView
android:id="@+id/tv_delivery_cost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_free"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<View
android:id="@+id/v_divider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:background="#BEBEBE"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<ImageView
android:id="@+id/iv_promo_code"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_promo_code"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<EditText
android:id="@+id/et_promo_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/shop_i_have_a_promo_code"
android:imeOptions="actionDone"
android:inputType="textCapCharacters"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:singleLine="true"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_promo_code"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<ProgressBar
android:id="@+id/pb_promo_code"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<View
android:id="@+id/v_total_background"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="21dp"
android:background="@drawable/shape_rectangle_rounded_4"
app:layout_constraintBottom_toTopOf="@id/ll_buttons" />
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/ll_buttons">
<TextView
android:id="@+id/tv_total_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_total"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintStart_toStartOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/shape_rectangle_rounded_4">
<TextView
android:id="@+id/tv_total_before_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/shape_line"
android:textColor="@color/darkGray6"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toStartOf="@id/tv_total"
app:layout_constraintTop_toTopOf="@id/v_total_background"
tools:text="$44.20" />
<EditText
android:id="@+id/et_promo_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/shop_i_have_a_promo_code"
android:imeOptions="actionDone"
android:inputType="textCapCharacters"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="12dp"
android:textColor="@color/darkGray6"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toEndOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background"
tools:text="$44.20" />
<ProgressBar
android:id="@+id/pb_promo_code"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/pb_price"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toEndOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/v_divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#BEBEBE" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/shape_rectangle_rounded_4">
<TextView
android:id="@+id/tv_total_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_total"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_total_before_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/shape_line"
android:textColor="@color/darkGray6"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_total"
app:layout_constraintTop_toTopOf="parent"
tools:text="$44.20" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="12dp"
android:textColor="@color/darkGray6"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="$44.20" />
<ProgressBar
android:id="@+id/pb_price"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_sold_out_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:background="@drawable/shape_rectangle_rounded_4"
android:padding="16dp"
android:text="@string/shop_sold_out_description_prefix"
android:textColor="@color/text_tertiary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_buttons"

View file

@ -22,7 +22,7 @@ dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.coroutines.reactive)
implementation(deps.kotlin.coroutines.rx2)
/** Logging */
implementation(deps.timber)

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.connection
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import io.reactivex.BackpressureStrategy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -11,6 +13,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.reactive.asFlow
private const val PING_INTERVAL = 5_000
internal class RealInternetConnectionManager : NetworkConnectionManager {
private val scope =
@ -18,11 +22,23 @@ internal class RealInternetConnectionManager : NetworkConnectionManager {
SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("RealInternetConnectionManager"),
)
private val initialNetworkResult: Boolean by lazy {
ReactiveNetwork
.checkInternetConnectivity()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.blockingGet()
}
override val isOnlineFlow: StateFlow<Boolean> = ReactiveNetwork
.observeInternetConnectivity()
.observeInternetConnectivity(
InternetObservingSettings.builder()
.interval(PING_INTERVAL)
.build(),
)
.toFlowable(BackpressureStrategy.LATEST)
.asFlow()
.stateIn(scope, SharingStarted.Eagerly, initialValue = false)
.stateIn(scope, SharingStarted.Lazily, initialValue = initialNetworkResult)
override val isOnline: Boolean
get() = isOnlineFlow.value

View file

@ -333,11 +333,10 @@
<string name="send_transaction_success">Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время</string>
<string name="send_validation_invalid_address">Неверный адрес</string>
<string name="shop_buy_now">Купить сейчас</string>
<string name="shop_free">Бесплатно</string>
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Другие способы оплаты</string>
<string name="shop_shipping">Доставка</string>
<string name="shop_sold_out_description_prefix">Из-за большого количества заказов, которые мы получаем, cроки доставки могут быть увеличены.</string>
<string name="shop_total">Итого</string>
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>

View file

@ -323,11 +323,9 @@
<string name="send_transaction_success">交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新</string>
<string name="send_validation_invalid_address">無效地址</string>
<string name="shop_buy_now">現在購買</string>
<string name="shop_free">免費</string>
<string name="shop_i_have_a_promo_code">輸入折扣碼</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">其他付款方式</string>
<string name="shop_shipping">運送</string>
<string name="shop_total">總計</string>
<string name="solana_rent_warning">Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用</string>
<string name="story_awe_description">安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中</string>

View file

@ -329,11 +329,10 @@
<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="shop_buy_now">Buy now</string>
<string name="shop_free">Free</string>
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Other payment methods</string>
<string name="shop_shipping">Shipping</string>
<string name="shop_sold_out_description_prefix">Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed.</string>
<string name="shop_total">Total</string>
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>

View file

@ -4,16 +4,13 @@ import com.tangem.feature.swap.domain.cache.SwapDataCache
import com.tangem.feature.swap.domain.converters.CryptoCurrencyConverter
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapApproveType
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyFees
import com.tangem.lib.crypto.models.ProxyFiatCurrency
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.toFiatString
import java.math.BigDecimal
@ -115,12 +112,19 @@ internal class SwapInteractorImpl @Inject constructor(
permissionOptions.approveData.approveModel.data
}
val result = transactionManager.sendApproveTransaction(
networkId = networkId,
feeAmount = permissionOptions.txFee.feeValue,
gasLimit = permissionOptions.txFee.gasLimit,
destinationAddress = permissionOptions.approveData.approveModel.toAddress,
dataToSign = dataToSign,
txData = ApproveTxData(
networkId = networkId,
feeAmount = permissionOptions.txFee.feeValue,
gasLimit = permissionOptions.txFee.gasLimit,
destinationAddress = permissionOptions.approveData.approveModel.toAddress,
dataToSign = dataToSign,
),
derivationPath = derivationPath,
analyticsData = AnalyticsData(
feeType = permissionOptions.txFee.feeType.getNameForAnalytics(),
tokenSymbol = permissionOptions.fromToken.symbol,
permissionType = permissionOptions.approveType.getNameForAnalytics(),
),
)
return when (result) {
is SendTxResult.Success -> {
@ -188,15 +192,21 @@ internal class SwapInteractorImpl @Inject constructor(
): TxState {
val amount = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format, use only digits" }
val result = transactionManager.sendTransaction(
networkId = networkId,
amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
txData = SwapTxData(
networkId = networkId,
amountToSend = amount,
currencyToSend = cryptoCurrencyConverter.convert(currencyToSend),
feeAmount = fee.feeValue,
gasLimit = fee.gasLimit,
destinationAddress = swapStateData.swapModel.transaction.toWalletAddress,
dataToSign = swapStateData.swapModel.transaction.data,
),
isSwap = true,
derivationPath = derivationPath,
analyticsData = AnalyticsData(
feeType = fee.feeType.getNameForAnalytics(),
tokenSymbol = currencyToSend.symbol,
),
)
return when (result) {
is SendTxResult.Success -> {
@ -587,12 +597,14 @@ internal class SwapInteractorImpl @Inject constructor(
gasLimit = normalFeeGas,
feeFiatFormatted = normalFiatFee,
feeCryptoFormatted = normalCryptoFee,
feeType = FeeType.NORMAL,
),
priorityFee = TxFee(
feeValue = priorityFeeValue,
gasLimit = priorityFeeGas,
feeFiatFormatted = priorityFiatFee,
feeCryptoFormatted = priorityCryptoFee,
feeType = FeeType.PRIORITY,
),
)
}
@ -670,8 +682,8 @@ internal class SwapInteractorImpl @Inject constructor(
private const val DEFAULT_SLIPPAGE = 2
private const val ZERO_BALANCE = "0"
private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.4
private const val INCREASE_GAS_LIMIT_BY = 125 // 25%
private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val USDT_SYMBOL = "USDT"
private const val USDC_SYMBOL = "USDC"
private const val INFINITY_SYMBOL = ""

View file

@ -2,4 +2,11 @@ package com.tangem.feature.swap.domain.models.domain
enum class SwapApproveType {
LIMITED, UNLIMITED
}
fun SwapApproveType.getNameForAnalytics(): String {
return when (this) {
SwapApproveType.LIMITED -> "Transaction"
SwapApproveType.UNLIMITED -> "Unlimited"
}
}

View file

@ -77,4 +77,16 @@ data class TxFee(
val gasLimit: Int,
val feeFiatFormatted: String,
val feeCryptoFormatted: String,
)
val feeType: FeeType,
)
enum class FeeType {
NORMAL, PRIORITY
}
fun FeeType.getNameForAnalytics(): String {
return when (this) {
FeeType.NORMAL -> "Normal"
FeeType.PRIORITY -> "Max"
}
}

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getActiveIconResByCoinId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
@ -394,6 +395,7 @@ val stateSelectable = SelectableItemsState<TxFee>(
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
items = listOf(
@ -407,6 +409,7 @@ val stateSelectable = SelectableItemsState<TxFee>(
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
Item(
@ -419,6 +422,7 @@ val stateSelectable = SelectableItemsState<TxFee>(
gasLimit = 0,
feeFiatFormatted = "",
feeCryptoFormatted = "",
feeType = FeeType.NORMAL,
),
),
).toImmutableList(),

View file

@ -75,9 +75,9 @@ reactiveNetwork = "3.0.8"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-237"
tangemBlockchainSdk = "develop-240"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-249"
tangemCardSdk = "develop-266"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem
@ -179,7 +179,7 @@ appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer"
armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" }
kotlin-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "coroutine" }
kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" }
kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" }
desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
googlePlay-core = { module = "com.google.android.play:core", version.ref = "googlePlayCore" }

View file

@ -1,8 +1,6 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyFees
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
@ -11,26 +9,25 @@ interface TransactionManager {
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
txData: ApproveTxData,
derivationPath: String?,
analyticsData: AnalyticsData,
): SendTxResult
@Suppress("LongParameterList")
/**
* Send transaction
*
* @param txData data to build a tx
* @param derivationPath for select right walletManager
* @param analyticsData data for send analytics event
* @return result of transaction
*/
@Throws(IllegalStateException::class)
suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
gasLimit: Int,
destinationAddress: String,
dataToSign: String,
txData: SwapTxData,
isSwap: Boolean,
currencyToSend: Currency,
derivationPath: String?,
analyticsData: AnalyticsData,
): SendTxResult
/**

View file

@ -0,0 +1,14 @@
package com.tangem.lib.crypto.models
/**
* Analytics data for send events in analytics engine
*
* @property feeType type of fee (min,max,normal)
* @property tokenSymbol symbol
* @property permissionType optional parameter used for type tx approve
*/
data class AnalyticsData(
val feeType: String,
val tokenSymbol: String,
val permissionType: String? = null,
)

View file

@ -0,0 +1,20 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
/**
* Tx data for create and make approve transaction
*
* @property networkId id of network
* @property feeAmount amount of fee
* @property gasLimit gasLimit for given tx
* @property destinationAddress address to send tx
* @property dataToSign data to sing with signer
*/
data class ApproveTxData(
val networkId: String,
val feeAmount: BigDecimal,
val gasLimit: Int,
val destinationAddress: String,
val dataToSign: String,
)

View file

@ -0,0 +1,24 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
/**
* Tx data for create and make transaction
*
* @property networkId id of network
* @property feeAmount amount of fee
* @property gasLimit gasLimit for given tx
* @property destinationAddress address to send tx
* @property dataToSign data to sing with signer
* @property amountToSend amount of tx
* @property currencyToSend currency for tx
*/
data class SwapTxData(
val networkId: String,
val feeAmount: BigDecimal,
val gasLimit: Int,
val destinationAddress: String,
val dataToSign: String,
val amountToSend: BigDecimal,
val currencyToSend: Currency,
)