Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-20 14:57:12 +03:00
commit 422f184c8f
39 changed files with 715 additions and 205 deletions

View file

@ -4,6 +4,11 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
ext.configVar = [
configEnvironment: "CONFIG_ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
]
android {
compileSdkVersion 31
defaultConfig {
@ -13,19 +18,14 @@ android {
minSdkVersion 21
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
buildFeatures {
compose true
viewBinding true
}
buildTypes {
release {
debuggable false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
}
debug {
debuggable true
minifyEnabled false
@ -34,14 +34,24 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"dev\"'
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
release {
debuggable false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
}
}
kotlinOptions {

View file

@ -1,12 +1,12 @@
{
"tokens" : [
{
"id" : "polygon/test",
"id" : "polygon-pos/test",
"symbol" : "MATIC",
"name" : "Polygon",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
}
@ -18,17 +18,17 @@
"name" : "Dai Stablecoin",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
}
@ -40,7 +40,7 @@
"name" : "Dummy ERC20",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
}
@ -52,12 +52,12 @@
"name" : "Ether",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
}
@ -69,7 +69,7 @@
"name" : "Test Token",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
}
@ -81,17 +81,17 @@
"name" : "Tether USD",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
},
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
},
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
}
@ -103,7 +103,7 @@
"name" : "Wrapped Ether",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
}
@ -115,7 +115,7 @@
"name" : "Wrapped Matic",
"contracts" : [
{
"networkId" : "polygon-pos",
"networkId" : "polygon-pos/test",
"address" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
}
@ -135,7 +135,7 @@
"name" : "Tangem Coin A",
"contracts" : [
{
"networkId" : "solana",
"networkId" : "solana/test",
"address" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
}
@ -147,14 +147,14 @@
"name" : "Tangem Coin B",
"contracts" : [
{
"networkId" : "solana",
"networkId" : "solana/test",
"address" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
}
]
},
{
"id" : "binance/test",
"id" : "binancecoin/test",
"symbol" : "BNB",
"name" : "Binance",
"contracts" : [
@ -167,14 +167,14 @@
"name" : "Hemster - 452",
"contracts" : [
{
"networkId" : "binancecoin",
"networkId" : "binancecoin/test",
"address" : "HEM-452",
"decimalCount" : 8
}
]
},
{
"id" : "bsc/test",
"id" : "binance-smart-chain/test",
"symbol" : "BNB",
"name" : "Bsc",
"contracts" : [
@ -187,7 +187,7 @@
"name" : "Binance-Peg BTCB Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
}
@ -199,7 +199,7 @@
"name" : "Binance-Peg BUSD Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
}
@ -211,7 +211,7 @@
"name" : "Binance-Peg USDC Token",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
}
@ -223,7 +223,7 @@
"name" : "Binance-Peg XRP",
"contracts" : [
{
"networkId" : "binance-smart-chain",
"networkId" : "binance-smart-chain/test",
"address" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
}
@ -255,7 +255,7 @@
"name" : "Xeenus",
"contracts" : [
{
"networkId" : "ethereum",
"networkId" : "ethereum/test",
"address" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
}
@ -267,7 +267,7 @@
"name" : "Yeenus",
"contracts" : [
{
"networkId" : "ethereum",
"networkId" : "ethereum/test",
"address" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
}
@ -279,7 +279,7 @@
"name" : "Zeenus",
"contracts" : [
{
"networkId" : "ethereum",
"networkId" : "ethereum/test",
"address" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
}
@ -299,7 +299,7 @@
"name" : "The Fuji stablecoin",
"contracts" : [
{
"networkId" : "avalanche",
"networkId" : "avalanche/test",
"address" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
}
@ -311,7 +311,7 @@
"name" : "To the Moon",
"contracts" : [
{
"networkId" : "avalanche",
"networkId" : "avalanche/test",
"address" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
}
@ -331,7 +331,7 @@
"name" : "Fantom USD",
"contracts" : [
{
"networkId" : "fantom",
"networkId" : "fantom/test",
"address" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
}
@ -343,7 +343,7 @@
"name" : "Wrapped Fantom",
"contracts" : [
{
"networkId" : "fantom",
"networkId" : "fantom/test",
"address" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
}

View file

@ -53,6 +53,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(

View file

@ -0,0 +1,75 @@
package com.tangem.tap.common
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.widget.LinearLayoutCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
/**
[REDACTED_AUTHOR]
*/
object TestActions {
// It used only for the test actions in debug or debug_beta builds
var testAmountInjectionForWalletManagerEnabled = false
/**
* @param isTestView - true must be used if you want to show or hide your view depends on BuildConfig
*/
fun initFor(view: View, actions: List<TestAction>, isTestView: Boolean = false) {
if (isTestView) view.show(BuildConfig.TEST_ACTION_ENABLED)
if (!BuildConfig.TEST_ACTION_ENABLED) return
view.setOnClickListener {
store.dispatchDialogShow(AppDialog.TestActionsDialog(actions))
}
}
}
typealias TestAction = Pair<String, () -> Unit>
class TestActionsBottomSheetDialog(
private val appDialog: AppDialog.TestActionsDialog,
context: Context
) : BottomSheetDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(createContentView())
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.peekHeight = 400
setOnCancelListener {
store.dispatchDialogHide()
}
}
private fun createContentView(): View {
val actionButtons = mutableListOf<View>()
appDialog.actionsList.forEach { (actionName, action) ->
val view = Button(context).apply {
text = actionName
setOnClickListener {
action()
cancel()
}
}
actionButtons.add(view)
}
return LinearLayoutCompat(context).apply {
orientation = LinearLayoutCompat.VERTICAL
actionButtons.forEach { addView(it) }
}
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.common.module.ModuleMessage
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun AddCustomTokenWarning(
modifier: Modifier = Modifier,
warning: ModuleMessage,
converter: ModuleMessageConverter
) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Text(
text = stringResource(id = R.string.common_warning),
color = colorResource(id = R.color.white),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
SpacerH8()
Text(
text = converter.convert(warning),
color = colorResource(id = R.color.white),
fontSize = 13.sp,
lineHeight = 18.sp
)
}
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
@ -20,8 +21,9 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
if (isDemoWallet()) {
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)
} else {
update()

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.TestAction
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
@ -17,4 +18,8 @@ sealed class AppDialog : StateDialog {
val currency: Currency,
val addressData: AddressData,
) : AppDialog()
data class TestActionsDialog(
val actionsList: List<TestAction>
) : AppDialog()
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
@ -15,7 +14,6 @@ import com.tangem.tap.common.ThrottlerWithValues
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
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.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
@ -25,8 +23,6 @@ import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -60,7 +56,6 @@ class TapWalletManager {
}
when (result) {
is Result.Success -> {
checkForRentWarning(walletManager)
dispatchOnMain(WalletAction.LoadWallet.Success(result.data, blockchainNetwork))
}
is Result.Failure -> {
@ -329,40 +324,6 @@ class TapWalletManager {
else -> null
}
}
private suspend fun checkForRentWarning(walletManager: WalletManager) {
val rentProvider = walletManager as? RentProvider ?: return
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean =
balance < rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs =
walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return
val currency = walletManager.wallet.blockchain.currency
dispatchOnMain(
WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
)
)
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
fun Wallet.getFirstToken(): Token? {

View file

@ -21,7 +21,7 @@ data class WarningMessage(
val messageFormatArg: String? = null
) {
val blockchainList: List<Blockchain>? by lazy {
blockchains?.map { Blockchain.fromId(it.toUpperCase()) }
blockchains?.map { Blockchain.fromId(it.uppercase()) }
}

View file

@ -5,6 +5,7 @@ import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.R
import java.util.*
/**
[REDACTED_AUTHOR]
@ -181,6 +182,24 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun restoreFundsWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Warning,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_funds_restoration_message,
messageResId = R.string.alert_funds_restoration_message,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more
)
const val REMAINING_SIGNATURES_WARNING = 10
fun getRestoreFundsGuideUrl(locale: String): String {
val code = if (locale == Locale("ru").language) "ru" else "en"
return "https://tangem.com/$code/notion"
}
}
}

View file

@ -152,7 +152,7 @@ class CurrenciesRepository(val context: Application) {
.map {
val token = it.toToken()
val id = currencies
.find { it.contracts?.find { it.address == token.contractAddress } != null }
.find { it.contracts.find { it.address == token.contractAddress } != null }
?.id
token.copy(id = id)
}
@ -180,7 +180,7 @@ class CurrenciesRepository(val context: Application) {
val fileName = if (isTestNet) "testnet_tokens" else "tokens"
val json = context.assets.readJsonFileToString(fileName)
return currenciesAdapter.fromJson(json)!!.tokens
.map { Currency.fromJsonObject(it, isTestNet) }
.map { Currency.fromJsonObject(it) }
}
private fun loadTokensJson(blockchain: Blockchain): String? {

View file

@ -26,8 +26,8 @@ data class CurrenciesFromJson(
)
fun List<ContractFromJson>.toContracts(isTestNet: Boolean): List<Contract> {
return mapNotNull { Contract.fromJsonObject(it, isTestNet) }
fun List<ContractFromJson>.toContracts(): List<Contract> {
return mapNotNull { Contract.fromJsonObject(it) }
}
data class Currency(
@ -39,23 +39,22 @@ data class Currency(
) {
companion object {
fun fromJsonObject(currency: CurrencyFromJson, isTestNet: Boolean): Currency {
fun fromJsonObject(currency: CurrencyFromJson): Currency {
return Currency(
id = currency.id,
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id),
contracts = prepareListOfContracts(currency.contracts, currency.id, isTestNet)
contracts = prepareListOfContracts(currency.contracts, currency.id)
)
}
private fun prepareListOfContracts(
contractsFromJson: List<ContractFromJson>?,
currencyId: String,
isTestNet: Boolean
): List<Contract> {
val mainNetwork = Contract.fromCurrencyId(currencyId, isTestNet)
val contracts = contractsFromJson?.toContracts(isTestNet) ?: emptyList()
val mainNetwork = Contract.fromCurrencyId(currencyId)
val contracts = contractsFromJson?.toContracts() ?: emptyList()
return (listOfNotNull(mainNetwork) + contracts).distinct()
}
}
@ -70,11 +69,10 @@ data class Contract(
) {
companion object {
fun fromJsonObject(contract: ContractFromJson, isTestNet: Boolean): Contract? {
val networkId = if (isTestNet) contract.networkId + TESTNET else contract.networkId
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
fun fromJsonObject(contract: ContractFromJson): Contract? {
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: return null
return Contract(
networkId = networkId,
networkId = contract.networkId,
blockchain = blockchain,
address = contract.address,
decimalCount = contract.decimalCount,
@ -82,19 +80,17 @@ data class Contract(
)
}
fun fromCurrencyId(currencyId: String, isTestNet: Boolean): Contract? {
val networkId = if (isTestNet) currencyId + TESTNET else currencyId
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
fun fromCurrencyId(currencyId: String): Contract? {
val blockchain = Blockchain.fromNetworkId(currencyId) ?: return null
return Contract(
networkId = networkId,
networkId = currencyId,
blockchain = blockchain,
address = blockchain.currency,
decimalCount = blockchain.decimals(),
iconUrl = getIconUrl(networkId)
iconUrl = getIconUrl(currencyId)
)
}
const val TESTNET = "-testnet"
}
}

View file

@ -85,7 +85,7 @@ class DemoConfig {
)
val demoCardIds: List<String> by lazy {
val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList()
val demoIds = getReleaseIds().toMutableList()
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
return@lazy demoIds.distinct()
@ -103,6 +103,11 @@ class DemoConfig {
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
private fun getReleaseIds(): List<String> {
return (releaseDemoCardIds +
releaseDemoCardIds_19042022).distinct()
}
private val releaseDemoCardIds = mutableListOf<String>(
// Tangem Wallet:
"AC01000000041100",
@ -145,14 +150,113 @@ class DemoConfig {
"AB02000000051059",
)
// https://tangem.slack.com/archives/GMXC6PP71/p1650360610759269
private val releaseDemoCardIds_19042022 = listOf(
// Wallet
"AC01000000045754",
"AC01000000041662",
"AC01000000041647",
"AC01000000041209",
"AC01000000042462",
"AC01000000041100",
"AC01000000041621",
"AC01000000045960",
"AC01000000041092",
"AC01000000041217",
"AC01000000013489",
"AC01000000028610",
"AC01000000028701",
"AC01000000028578",
"AC01000000027281",
"AC01000000027216",
"AC01000000028594",
"AC01000000028602",
"AC01000000028636",
"AC01000000013968",
"AC01000000027208",
"AC01000000013471",
"AC01000000028586",
"AC01000000013703",
"AC01000000028628",
"AC01000000028693",
"AC01000000028685",
"AC01000000013950",
"AC01000000013828",
"AC01000000013497",
"AC01000000013836",
"AC01000000013505",
// Note BTC
"AB01000000059608",
"AB01000000046647",
"AB01000000046571",
"AB01000000046746",
"AB01000000059574",
"AB01000000046753",
"AB01000000046605",
"AB01000000046761",
"AB01000000046720",
"AB01000000046530",
"AB01000000016475",
"AB01000000016483",
"AB01000000016491",
"AB01000000020709",
"AB01000000020717",
"AB01000000015550",
"AB01000000015394",
"AB01000000016079",
"AB01000000016087",
"AB01000000016095",
"AB01000000020915",
"AB01000000017184",
"AB01000000020907",
"AB01000000017192",
"AB01000000016210",
"AB01000000016111",
"AB01000000016103",
"AB01000000015766",
"AB01000000015774",
"AB01000000015782",
"AB01000000022598",
"AB01000000022580",
// Note ETH
"AB02000000051083",
"AB02000000051059",
"AB02000000051158",
"AB02000000050986",
"AB02000000051026",
"AB02000000050960",
"AB02000000051042",
"AB02000000051091",
"AB02000000051034",
"AB02000000051133",
"AB02000000019924",
"AB02000000019932",
"AB02000000022092",
"AB02000000022282",
"AB02000000023983",
"AB02000000023439",
"AB02000000020328",
"AB02000000020310",
"AB02000000021565",
"AB02000000022357",
"AB02000000023355",
"AB02000000022324",
"AB02000000022100",
"AB02000000019999",
"AB02000000020013",
"AB02000000020005",
"AB02000000020021",
"AB02000000020039",
"AB02000000020278",
"AB02000000020252",
"AB02000000018652",
"AB02000000018561",
)
private val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
//TODO: delete bellow ids before 3.28 release
"AB01000000045060", // Note BTC
"AB02000000045028", // Note ETH
"AC79000000000004", // Wallet 4.46
)
private val debugTestDemoCardIds = listOf<String>(

View file

@ -21,6 +21,7 @@ sealed class OnboardingWalletAction : Action {
sealed class BackupAction : Action {
object DetermineBackupStep : BackupAction()
object IntroduceBackup : BackupAction()
object StartBackup : BackupAction()
object DismissBackup : BackupAction()

View file

@ -111,7 +111,15 @@ private fun handleWalletAction(action: Action) {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> null
else -> {
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
walletState.backupState.backupStep == BackupStep.Finished
) {
BackupAction.IntroduceBackup
} else {
null
}
}
}
newAction?.let { store.dispatch(it) }
}

View file

@ -39,6 +39,10 @@ class BackupReducer {
): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup,
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {
state.copy(backupStep = BackupStep.AddBackupCards)
@ -104,6 +108,7 @@ class BackupReducer {
BackupAction.OnAccessCodeDialogClosed -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
BackupAction.DiscardBackup -> BackupState()
BackupAction.WritePrimaryCard -> state
is BackupAction.WriteBackupCard -> state
@ -117,7 +122,6 @@ class BackupReducer {
is BackupAction.CheckAccessCode -> state
BackupAction.DetermineBackupStep -> state
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.DiscardBackup -> state
BackupAction.ResumeBackup -> state
BackupAction.DiscardSavedBackup -> state
BackupAction.StartBackup -> state

View file

@ -12,7 +12,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
@ -22,10 +21,12 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.ToggledRippleTheme
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestAddCustomTokenActions
import com.tangem.wallet.R
/**
@ -51,7 +52,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
LazyColumn(
contentPadding = PaddingValues(bottom = 90.dp)
) {
// item { AddCustomTokenDebugActions() }
item { TestAddCustomTokenActions() }
item {
Surface(
modifier = Modifier.padding(16.dp),
@ -110,19 +111,11 @@ fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
}
Surface(
AddCustomTokenWarning(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Text(
modifier = Modifier.padding(16.dp),
text = warningConverter.convert(item),
color = colorResource(id = R.color.white),
fontSize = 14.sp
)
}
warning = item,
converter = warningConverter
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
package com.tangem.tap.features.tokens.addCustomToken.compose.test
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
@ -25,8 +25,8 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
@Composable
fun AddCustomTokenDebugActions() {
if (!BuildConfig.DEBUG) return
fun TestAddCustomTokenActions() {
if (!BuildConfig.TEST_ACTION_ENABLED) return
Column() {
// deep test
@ -47,10 +47,10 @@ fun AddCustomTokenDebugActions() {
@Composable
private fun AllInOne() {
// validation error
ContractAddressButton(
name = "invalid",
address = "unk"
)
// ContractAddressButton(
// name = "invalid",
// address = "unk"
// )
// active = true
ContractAddressButton(
name = "true",
@ -71,6 +71,10 @@ private fun AllInOne() {
name = "unknown",
address = "0x1111111111111111112111111111111111111113"
)
ContractAddressButton(
name = "solana",
address = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
)
}
@Composable

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@ -71,6 +72,7 @@ fun CollapsedCurrencyItem(
if (contract.address == currency.symbol) {
BlockchainNetworkItem(
blockchain = Blockchain.fromNetworkId(contract.networkId),
isMainNetwork = true,
addedBlockchains = addedBlockchains
)
} else {
@ -95,6 +97,7 @@ fun CollapsedCurrencyItem(
} else {
BlockchainNetworkItem(
blockchain = blockchain,
isMainNetwork = false,
addedBlockchains = addedBlockchains
)
}
@ -114,18 +117,39 @@ fun CollapsedCurrencyItem(
@Composable
fun BlockchainNetworkItem(
blockchain: Blockchain?,
isMainNetwork: Boolean,
addedBlockchains: List<Blockchain>
) {
val added = addedBlockchains.contains(blockchain)
val icon =
if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
if (icon != null) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
Box(Modifier
.size(20.dp)) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
Modifier
.size(20.dp)
)
if (isMainNetwork) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80))
)
}
}
}
Spacer(modifier = Modifier.size(5.dp))
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.tokens.ui.compose
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@ -53,17 +54,36 @@ fun NetworkItem(
interactionSource = remember { MutableInteractionSource() }
)
) {
SubcomposeAsyncImage(
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
modifier = Modifier
.clip(CircleShape)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
.size(20.dp)
.align(Alignment.CenterVertically)
)
Box(modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp, top = 16.dp, bottom = 16.dp, end = 6.dp)
) {
SubcomposeAsyncImage(
model = if (added) blockchain.getRoundIconRes() else blockchain.getGreyedOutIconRes(),
contentDescription = blockchain.fullName,
loading = { CurrencyPlaceholderIcon(blockchain.id) },
error = { CurrencyPlaceholderIcon(blockchain.id) },
modifier = Modifier
.size(20.dp)
)
if (isBlockchain) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(7.dp)
.clip(CircleShape)
.background(Color.White)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(5.dp)
.clip(CircleShape)
.background(Color(0xFF1ACE80))
)
}
}
}
Row(
modifier = Modifier
.fillMaxHeight()

View file

@ -91,6 +91,8 @@ sealed class WalletAction : Action {
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
object RestoreFundsWarningClosed : Warnings()
}
data class LoadFiatRate(
@ -159,4 +161,6 @@ sealed class WalletAction : Action {
val minRent: String,
val rentExempt: String
) : WalletAction()
data class RemoveWalletRent(val blockchain: BlockchainNetwork) : WalletAction()
}

View file

@ -395,6 +395,9 @@ sealed interface Currency {
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
}
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
companion object {
fun fromBlockchainNetwork(
@ -402,13 +405,13 @@ sealed interface Currency {
token: com.tangem.blockchain.common.Token? = null
): Currency {
return if (token != null) {
Currency.Token(
Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
} else {
Currency.Blockchain(
Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
)
@ -430,7 +433,7 @@ sealed interface Currency {
}
fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token {
return Currency.Token(
return Token(
token = tokenWithBlockchain.token,
blockchain = tokenWithBlockchain.blockchain,
derivationPath = null

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.solana.SolanaWalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
@ -130,17 +129,13 @@ class MultiWalletMiddleware {
scope.launch(Dispatchers.Main) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
if (walletState?.getWalletData(blockchainNetwork) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
blockchainNetwork, walletManager
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
wallet = wallet,
blockchain = blockchainNetwork
)
)
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
blockchainNetwork, walletManager
))
store.dispatch(WalletAction.LoadWallet.Success(
wallet = wallet,
blockchain = blockchainNetwork
))
}
}
}

View file

@ -3,8 +3,10 @@ package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
@ -20,9 +22,12 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
@ -34,6 +39,7 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import java.math.BigDecimal
class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
@ -77,21 +83,18 @@ class WalletMiddleware {
}
}
is WalletAction.LoadWallet.Success -> {
checkForRentWarning(walletState.getWalletManager(action.blockchain))
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
)
)
store.dispatch(
WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain
)
)
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
))
store.dispatch(WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain
))
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
@ -293,4 +296,43 @@ class WalletMiddleware {
tokenRate = tokenRate
)
}
private fun checkForRentWarning(walletManager: WalletManager?) {
val rentProvider = walletManager as? RentProvider ?: return
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean {
return balance < rentExempt
}
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
val currency = walletManager.wallet.blockchain.currency
if (show) {
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(
blockchain = BlockchainNetwork.fromWalletManager(walletManager),
))
}
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
}

View file

@ -8,7 +8,9 @@ import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -64,6 +66,9 @@ class WarningsMiddleware {
)
}
}
is WalletAction.Warnings.RestoreFundsWarningClosed -> {
preferencesStorage.saveRestoreFundsWarningClosed()
}
}
}
@ -88,6 +93,9 @@ class WarningsMiddleware {
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
return@let
}
if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) {
addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning())
}
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
@ -193,7 +201,7 @@ class WarningsMiddleware {
}
private fun setWarningMessages() {
store.dispatch(WalletAction.Warnings.Set(getWarnings()))
store.dispatchOnMain(WalletAction.Warnings.Set(getWarnings()))
}
private fun getWarnings(): List<WarningMessage> {

View file

@ -314,6 +314,15 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.updateWalletsData(listOf(walletData))
}
}
is WalletAction.RemoveWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)
if (walletData == null) {
newState
} else {
walletData = walletData.copy(warningRent = null)
newState = newState.updateWalletsData(listOf(walletData))
}
}
}
return newState
}

View file

@ -15,6 +15,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.common.extensions.guard
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -24,6 +25,7 @@ import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
@ -54,9 +56,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletState == newState.walletState
}.select { it.walletState }
state.select { it.walletState }
}
}
@ -70,12 +70,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupTransactionsRecyclerView()
setupButtons()
setupTestActionButton()
}
private fun setupTransactionsRecyclerView() = with(binding) {
pendingTransactionAdapter = PendingTransactionsAdapter()
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
@ -93,6 +92,15 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
btnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
}
private fun setupTestActionButton() {
view?.findViewById<View>(R.id.l_balance)?.let { view ->
TestActions.initFor(
view = view,
actions = TestWalletDetails.solanaRentExemptWarning()
)
}
}
override fun newState(state: WalletState) {
if (activity == null || view == null) return
if (state.selectedCurrency == null) return
@ -113,11 +121,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(
WalletAction.LoadWallet(
blockchain = BlockchainNetwork(selectedWallet.currency.blockchain, selectedWallet.currency.derivationPath, emptyList())
store.dispatch(WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList()
)
)
))
}
}
@ -165,6 +175,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) {
if (selectedWalletData.currency.isBlockchain()) return@with
if (selectedWalletData.shouldShowCoinAmountWarning()) {
val blockchainName = selectedWalletData.currency.blockchain.fullName
val warningMessage = requireContext().getString(

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.content.res.Resources
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.ConfigurationCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@ -13,6 +15,7 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
@ -56,7 +59,9 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
tvTitle.text = getString(warning.titleResId, warning.title)
tvMessage.text = getString(
resId = warning.messageResId, default = warning.message, formatArgs = warning.messageFormatArg
resId = warning.messageResId,
default = warning.message,
formatArgs = warning.messageFormatArg
)
}
@ -73,10 +78,13 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.hide()
binding.btnClose.hide()
}
WarningMessage.Type.Temporary -> {
binding.groupControlsRating.hide()
binding.groupControlsTemporary.show()
binding.btnClose.hide()
val buttonAction =
when (warning.titleResId) {
R.string.warning_important_security_info -> {
@ -84,6 +92,20 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
store.dispatch(WalletAction.ShowDialog.SignedHashesMultiWalletDialog)
}
}
R.string.alert_funds_restoration_message -> {
binding.btnClose.show()
binding.btnClose.setOnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed)
}
val locale = ConfigurationCompat
.getLocales(Resources.getSystem().configuration)
.get(0)
val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language)
View.OnClickListener {
store.dispatchOpenUrl(url)
}
}
else -> {
View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
@ -99,6 +121,7 @@ class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHol
WarningMessage.Type.AppRating -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.show()
binding.btnClose.show()
val analyticsHandler = store.state.globalState.analyticsHandlers
binding.btnClose.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)

View file

@ -0,0 +1,94 @@
package com.tangem.tap.features.wallet.ui.test
import com.tangem.blockchain.blockchains.solana.SolanaWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.TestAction
import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import java.math.BigDecimal
import kotlin.random.Random
/**
[REDACTED_AUTHOR]
*/
class TestWalletDetails {
companion object {
fun solanaRentExemptWarning(): List<TestAction> {
val checker = SolanaRentWarningActionEmitter()
return listOf(
"BALANCE = 0.0" to { checker.setZeroBalance() },
"BALANCE < 0.00089088" to { checker.setLessThanRentExempt() },
"BALANCE > 0.00089088" to { checker.setMoreThanRentExempt() },
"BALANCE = 0.00089087" to { checker.setLessThanRentExemptByOne() },
"BALANCE = 0.00089088 (rent exempt)" to { checker.setForRentExemptBarrier() },
"BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() },
)
}
}
}
private class SolanaRentWarningActionEmitter {
private val zero = BigDecimal.ZERO
private val one = BigDecimal(0.00000001)
private val rentExemptBarrier = BigDecimal(0.00089088)
private val lessThanRentExemptByOne = rentExemptBarrier.minus(one)
private val moreThanRentExemptByOne = rentExemptBarrier.plus(one)
private val moreThanRentExempt = rentExemptBarrier.plus(Random.nextDouble().toBigDecimal())
private val lessThanRentExempt = rentExemptBarrier
.minus(Random.nextDouble(0.0, rentExemptBarrier.minus(one).toDouble()).toBigDecimal())
fun setZeroBalance() {
setBalance(zero)
}
fun setForRentExemptBarrier() {
setBalance(rentExemptBarrier)
}
fun setLessThanRentExemptByOne() {
setBalance(lessThanRentExemptByOne)
}
fun setMoreThanRentExemptByOne() {
setBalance(moreThanRentExemptByOne)
}
fun setMoreThanRentExempt() {
setBalance(moreThanRentExempt)
}
fun setLessThanRentExempt() {
setBalance(lessThanRentExempt)
}
private fun setBalance(value: BigDecimal) {
val amount = Amount(getBlockchain()).copy(value = value)
getWalletManager().apply {
TestActions.testAmountInjectionForWalletManagerEnabled = true
wallet.setAmount(amount)
}
store.dispatch(WalletAction.LoadData)
}
private fun getWalletManager(): SolanaWalletManager {
return store.state.walletState.getWalletManager(BlockchainNetwork(
blockchain = getBlockchain(),
card = scanResponse().card
)) as SolanaWalletManager
}
private fun getBlockchain(): Blockchain = when {
scanResponse().card.isTestCard -> Blockchain.SolanaTestnet
else -> Blockchain.Solana
}
private fun scanResponse(): ScanResponse {
return store.state.globalState.scanResponse ?: throw UnsupportedOperationException()
}
}

View file

@ -64,12 +64,21 @@ class PreferencesStorage(applicationContext: Application) {
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
}
fun wasRestoreFundsWarningClosed(): Boolean {
return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false)
}
fun saveRestoreFundsWarningClosed() {
preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) }
}
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed"
}
}

View file

@ -103,7 +103,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_close, btn_can_be_better, btn_really_cool" />
app:constraint_referenced_ids="btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
<string name="common_custom">Custom</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
<string name="common_custom">Custom</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
<string name="common_custom">Custom</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Доступные сети</string>
<string name="alert_manage_tokens_addresses_message">Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства.</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="alert_funds_restoration_message">Инструкция по восстановлению средств</string>
<string name="common_custom">Пользовательский</string>

View file

@ -40,6 +40,7 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="alert_manage_tokens_addresses_message">Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="alert_funds_restoration_message">Funds restoration instuction</string>
<string name="common_custom">Custom</string>

View file

@ -5,30 +5,30 @@ import com.tangem.blockchain.common.Blockchain
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"avalanche", "avalanche-2" -> Blockchain.Avalanche
"avalanche-testnet", "avalanche-2-testnet" -> Blockchain.AvalancheTestnet
"avalanche/test", "avalanche-2/test" -> Blockchain.AvalancheTestnet
"binancecoin" -> Blockchain.Binance
"binancecoin-testnet" -> Blockchain.BinanceTestnet
"binancecoin/test" -> Blockchain.BinanceTestnet
"binance-smart-chain" -> Blockchain.BSC
"binance-smart-chain-testnet" -> Blockchain.BSCTestnet
"binance-smart-chain/test" -> Blockchain.BSCTestnet
"ethereum" -> Blockchain.Ethereum
"ethereum-testnet" -> Blockchain.EthereumTestnet
"ethereum/test" -> Blockchain.EthereumTestnet
"polygon-pos" -> Blockchain.Polygon
"polygon-pos-testnet" -> Blockchain.PolygonTestnet
"polygon-pos/test" -> Blockchain.PolygonTestnet
"solana" -> Blockchain.Solana
"solana-testnet" -> Blockchain.SolanaTestnet
"solana/test" -> Blockchain.SolanaTestnet
"fantom" -> Blockchain.Fantom
"fantom-testnet" -> Blockchain.FantomTestnet
"fantom/test" -> Blockchain.FantomTestnet
"bitcoin" -> Blockchain.Bitcoin
"bitcoin-testnet" -> Blockchain.BitcoinTestnet
"bitcoin/test" -> Blockchain.BitcoinTestnet
"bitcoin-cash" -> Blockchain.BitcoinCash
"bitcoin-cash-testnet" -> Blockchain.BitcoinCashTestnet
"bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet
"cardano" -> Blockchain.CardanoShelley
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rsk" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
"stellar-testnet" -> Blockchain.StellarTestnet
"stellar/test" -> Blockchain.StellarTestnet
"tezos" -> Blockchain.Tezos
"ripple" -> Blockchain.XRP
else -> null
@ -39,31 +39,31 @@ fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche-testnet"
Blockchain.AvalancheTestnet -> "avalanche/test"
Blockchain.Binance -> "binancecoin"
Blockchain.BinanceTestnet -> "binancecoin-testnet"
Blockchain.BinanceTestnet -> "binancecoin/test"
Blockchain.BSC -> "binance-smart-chain"
Blockchain.BSCTestnet -> "binance-smart-chain-testnet"
Blockchain.BSCTestnet -> "binance-smart-chain/test"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin-testnet"
Blockchain.BitcoinTestnet -> "bitcoin/test"
Blockchain.BitcoinCash -> "bitcoin-cash"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash-testnet"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash/test"
Blockchain.Cardano -> "cardano"
Blockchain.CardanoShelley -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum-testnet"
Blockchain.EthereumTestnet -> "ethereum/test"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom-testnet"
Blockchain.FantomTestnet -> "fantom/test"
Blockchain.Litecoin -> "litecoin"
Blockchain.Polygon -> "matic-network"
Blockchain.PolygonTestnet -> "matic-networks-testnet"
Blockchain.PolygonTestnet -> "matic-networks/test"
Blockchain.RSK -> "rootstock"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar-testnet"
Blockchain.StellarTestnet -> "stellar/test"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana-testnet"
Blockchain.SolanaTestnet -> "solana/test"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
}

View file

@ -1,7 +1,9 @@
package com.tangem.domain.common.form
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
import com.tangem.blockchain.blockchains.solana.SolanaAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressService
import com.tangem.common.Validator
import com.tangem.domain.AddCustomTokenError
@ -25,15 +27,36 @@ class StringIsNotEmptyValidator : CustomTokenValidator<String>() {
}
class TokenContractAddressValidator : CustomTokenValidator<String>() {
private var blockchain: Blockchain = Blockchain.Unknown
fun nextValidationFor(blockchain: Blockchain) {
this.blockchain = blockchain
}
override fun validate(data: String?): AddCustomTokenError? {
if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty
return if (EthereumAddressService().validate(data)) {
return if (getAddressService().validate(data)) {
null
} else {
AddCustomTokenError.InvalidContractAddress
}
}
private fun getAddressService(): AddressService {
return when (blockchain) {
Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService()
Blockchain.Unknown -> EthereumAddressService()
else -> {
if (blockchain.isEvm()) {
EthereumAddressService()
} else {
throw UnsupportedOperationException()
}
}
}
}
}
class TokenNetworkValidator : CustomTokenValidator<Blockchain>() {

View file

@ -413,7 +413,10 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val decimalsValidator: TokenDecimalsValidator = state.getValidator(Decimals)
val networkValidator: TokenNetworkValidator = state.getValidator(Network)
return when (this) {
ContractAddress -> contractAddressValidator.validate(value as String)
ContractAddress -> {
contractAddressValidator.nextValidationFor(Network.getFieldValue())
contractAddressValidator.validate(value as String)
}
Network, DerivationPath -> networkValidator.validate(value as Blockchain)
Name -> nameValidator.validate(value as String)
Symbol -> symbolValidator.validate(value as String)