Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-18 22:31:09 +03:00
commit 7658f822c1
68 changed files with 595 additions and 378 deletions

View file

@ -69,8 +69,8 @@ repositories {
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation implementation(project(path: ':domain'))
// TODO: refactoring: only for backwards compatibility with non-relocated services to the network module
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'

View file

@ -20,7 +20,7 @@ import com.tangem.domain.DomainDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -55,13 +55,13 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
if (dialogState.value == null) return
val context = LocalContext.current
val errorConverter = remember { DomainErrorConverter(context) }
val errorConverter = remember { ModuleMessageConverter(context) }
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.DialogError -> ErrorDialog(
title = stringResource(id = R.string.common_error),
body = errorConverter.convertError(dialog.error),
body = errorConverter.convert(dialog.error),
onDismissRequest
)
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)

View file

@ -15,16 +15,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.domain.DomainError
import com.tangem.domain.ErrorConverter
import com.tangem.common.module.ModuleError
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.compose.extensions.stringResourceDefault
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
/**
[REDACTED_AUTHOR]
@ -41,8 +42,8 @@ fun OutlinedTextFieldWidget(
isEnabled: Boolean = true,
isVisible: Boolean = true,
isLoading: Boolean = false,
error: DomainError? = null,
errorConverter: ErrorConverter<String>? = null,
error: ModuleError? = null,
errorConverter: ModuleMessageConverter? = null,
debounceTextChanges: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
@ -79,7 +80,7 @@ private fun OutlinedProgressTextField(
placeholder: String = "",
isEnabled: Boolean = true,
isLoading: Boolean = false,
error: DomainError? = null,
error: ModuleError? = null,
debounce: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
@ -132,8 +133,8 @@ private fun OutlinedProgressTextField(
@Composable
private fun AnimatedErrorView(
error: DomainError? = null,
errorConverter: ErrorConverter<String>,
error: ModuleError? = null,
errorConverter: ModuleMessageConverter,
) {
AnimatedVisibility(
visible = error != null,
@ -141,7 +142,7 @@ private fun AnimatedErrorView(
exit = slideOutVertically() + fadeOut(),
) {
error?.let {
ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp))
ErrorView(errorConverter.convert(it), style = TextStyle(fontSize = 14.sp))
}
}
}
@ -149,20 +150,14 @@ private fun AnimatedErrorView(
@Preview
@Composable
fun OutlinedTextFieldWithErrorTest() {
val converter = remember {
object : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
return "Hello, i'am the error: ${error::class.java.simpleName}"
}
}
}
val context = LocalContext.current
val converter = remember { ModuleMessageConverter(context) }
class SimpleError(
override val code: Int = 1,
override val message: String = "Error message",
override val data: Any? = null,
) : DomainError
) : ModuleError
val modifier = Modifier
.fillMaxWidth()

View file

@ -22,6 +22,7 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_round
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_round
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_round
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_round
else -> R.drawable.ic_tangem_logo
}
}
@ -44,6 +45,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Solana, Blockchain.SolanaTestnet -> R.drawable.ic_solana_no_color
Blockchain.Fantom, Blockchain.FantomTestnet -> R.drawable.ic_fantom_no_color
Blockchain.BSC, Blockchain.BSCTestnet, Blockchain.Binance, Blockchain.BinanceTestnet -> R.drawable.ic_bsc_no_color
Blockchain.Dogecoin -> R.drawable.ic_dogecoin_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.common.extensions
import com.tangem.common.extensions.isZero
import com.tangem.network.api.tangemTech.Coins
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@ -10,7 +10,7 @@ import java.text.DecimalFormatSymbols
import java.util.*
fun BigDecimal.toFormattedString(
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat()
@ -26,7 +26,7 @@ fun BigDecimal.toFormattedCurrencyString(
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN,
limitNumberOfDecimals: Boolean = true
): String {
val decimalsForRounding = if (limitNumberOfDecimals){
val decimalsForRounding = if (limitNumberOfDecimals) {
if (decimals > 8) 8 else decimals
} else {
decimals
@ -52,7 +52,7 @@ fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: FiatCurrencyName): String
return "≈ ${fiatCurrencyName} $this"
}
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"
fun Coins.CurrenciesResponse.Currency.toFormattedString(): String = "${this.name} (${this.code}) - ${this.unit}"
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.moduleMessage
import android.content.Context
import com.tangem.common.module.ModuleMessage
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainModuleMessage
import com.tangem.tap.common.moduleMessage.domain.DomainMessageConverter
class ModuleMessageConverter(
private val context: Context
) : ModuleMessageConverter<ModuleMessage, String> {
override fun convert(message: ModuleMessage): String {
val convertedMessage = when (message) {
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
else -> null
}
return convertedMessage ?: convertUnknownMessage(message)
}
private fun convertUnknownMessage(message: ModuleMessage): String {
return "Unknown message: ${message::class.java.simpleName}"
}
}

View file

@ -1,10 +1,9 @@
package com.tangem.tap.features.tokens.addCustomToken
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainError
import com.tangem.domain.ErrorConverter
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.wallet.R
/**
@ -12,27 +11,23 @@ import com.tangem.wallet.R
*/
class DomainErrorConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val errorMessage = when (error) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error)
else -> null
}
return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}"
) : ModuleMessageConverter<DomainError, String?> {
override fun convert(message: DomainError): String? = when (message) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convert(message)
else -> null
}
}
private class AddCustomTokenConverter(
private val context: Context
) : ErrorConverter<String> {
) : ModuleMessageConverter<DomainError, String?> {
override fun convertError(error: DomainError): String {
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
override fun convert(message: DomainError): String? {
val customTokenError = (message as? AddCustomTokenError) ?: throw UnsupportedOperationException()
val rawMessage = when (customTokenError) {
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.Warning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenError.Warning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
@ -42,10 +37,11 @@ private class AddCustomTokenConverter(
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_required_field
else -> null
}
return when (rawMessage) {
is Int -> context.getString(rawMessage)
is String -> rawMessage
else -> "Unknown error: ${customTokenError::class.java.simpleName}"
else -> null
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common.moduleMessage.domain
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainError
import com.tangem.domain.DomainModuleMessage
/**
[REDACTED_AUTHOR]
*/
class DomainMessageConverter(
private val context: Context
) : ModuleMessageConverter<DomainModuleMessage, String?> {
override fun convert(message: DomainModuleMessage): String? {
return when (message) {
is DomainError -> DomainErrorConverter(context).convert(message)
else -> null
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.NetworkServices
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.NavigationState
@ -54,6 +55,9 @@ data class AppState(
val domainState: DomainState
get() = domainStore.state
val domainNetworks: NetworkServices
get() = domainState.globalState.networkServices
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(

View file

@ -4,8 +4,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -102,12 +100,10 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
domainStore.dispatch(DomainGlobalAction.SetScanResponse(result.data))
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> {
domainStore.dispatch(DomainGlobalAction.SetScanResponse(null))
action.onFailure?.invoke(result.error)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
@ -33,8 +35,10 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->
is GlobalAction.SaveScanNoteResponse ->{
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}

View file

@ -10,7 +10,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
@ -20,7 +19,6 @@ data class GlobalState(
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

@ -2,13 +2,17 @@ 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
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.network.api.tangemTech.TangemTechService
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
@ -26,7 +30,6 @@ 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
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -37,7 +40,9 @@ class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
private val coinMarketCapService = CoinMarketCapService()
private val tangemTechService: TangemTechService
get() = store.state.domainNetworks.tangemTechService
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
@ -98,20 +103,58 @@ class TapWalletManager {
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List<Currency>) {
suspend fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>) {
rates.forEach { (currency, priceResult) ->
when (priceResult) {
is Result.Success -> {
dispatchOnMain(
WalletAction.LoadFiatRate.Success(
currency to priceResult.data
))
}
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
null -> {}
}
}
}
// get and submit previous result of equivalents.
val throttledResult = currencies.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
Pair(it, fiatRatesThrottler.geValue(it))
}
if (throttledResult.isNotEmpty()) handleFiatRatesResult(throttledResult)
if (throttledResult.isNotEmpty()) {
handleFiatRatesResult(throttledResult.toMap())
}
val toUpdate = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
toUpdate.forEach {
val result = coinMarketCapService.getRate(it.currencySymbol, fiatCurrency)
if (result is Result.Success) {
fiatRatesThrottler.updateThrottlingTo(it)
fiatRatesThrottler.setValue(it, result)
val toUpdateCurrencies = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
val toUpdateIds = toUpdateCurrencies.mapNotNull { it.id }.distinct()
if (toUpdateIds.isEmpty()) return
//TODO: refactoring: move fiatRatesThrottler to the TangemTechRepository
when (val result = tangemTechService.coins.prices(fiatCurrency, toUpdateIds)) {
is Result.Success -> {
val missedCurrencies = mutableListOf<String>()
val updatedCurrencies = mutableMapOf<Currency, Result<BigDecimal>?>()
result.data.prices.forEach { (name, value) ->
val currency = toUpdateCurrencies.firstOrNull { it.id == name }.guard {
missedCurrencies.add(name)
return@forEach
}
val priceResult = Result.Success(value.toBigDecimal())
updatedCurrencies[currency] = priceResult
fiatRatesThrottler.updateThrottlingTo(currency)
fiatRatesThrottler.setValue(currency, priceResult)
}
if (missedCurrencies.isNotEmpty()) {
val missedNames = missedCurrencies.joinToString(", ")
store.dispatchDebugErrorNotification("Not found currencies to update: [$missedNames]")
}
handleFiatRatesResult(updatedCurrencies)
}
handleFiatRatesResult(listOf(it to result))
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
}
}
@ -231,7 +274,7 @@ class TapWalletManager {
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null &&
primaryBlockchain != null
primaryBlockchain != null && primaryBlockchain != Blockchain.Unknown
) {
val blockchainsWithoutPrimary = savedCurrencies.filterNot { it.blockchain == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(
@ -245,7 +288,9 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
}
}
}
}
@ -318,21 +363,14 @@ class TapWalletManager {
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<Currency, Result<BigDecimal>?>>) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
dispatchOnMain(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
null -> {}
}
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}
}
val Currency.id: String?
get() = when (this) {
is Currency.Blockchain -> blockchain.toNetworkId()
is Currency.Token -> token.id
}

View file

@ -35,7 +35,7 @@ data class Currency(
val name: String,
val symbol: String,
val iconUrl: String,
val contracts: List<Contract>?
val contracts: List<Contract>
) {
companion object {
@ -45,9 +45,19 @@ data class Currency(
name = currency.name,
symbol = currency.symbol,
iconUrl = getIconUrl(currency.id),
contracts = currency.contracts?.toContracts(isTestNet)
contracts = prepareListOfContracts(currency.contracts, currency.id, isTestNet)
)
}
private fun prepareListOfContracts(
contractsFromJson: List<ContractFromJson>?,
currencyId: String,
isTestNet: Boolean
): List<Contract> {
val mainNetwork = Contract.fromCurrencyId(currencyId, isTestNet)
val contracts = contractsFromJson?.toContracts(isTestNet) ?: emptyList()
return (listOfNotNull(mainNetwork) + contracts).distinct()
}
}
}
@ -72,6 +82,18 @@ 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
return Contract(
networkId = networkId,
blockchain = blockchain,
address = blockchain.currency,
decimalCount = blockchain.decimals(),
iconUrl = getIconUrl(networkId)
)
}
const val TESTNET = "-testnet"
}
}

View file

@ -4,11 +4,12 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.network.api.tangemTech.Coins
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
@ -20,6 +21,7 @@ sealed class DetailsAction : Action {
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val tangemTechService: TangemTechService,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -46,7 +48,7 @@ sealed class DetailsAction : Action {
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
data class SetCurrencies(val currencies: List<Coins.CurrenciesResponse.Currency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName) : AppCurrencyAction()

View file

@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.Coins
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
@ -19,7 +20,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletM
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -33,7 +33,7 @@ class DetailsMiddleware {
{ next ->
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> prepareData()
is DetailsAction.PrepareScreen -> prepareData(action)
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
@ -68,23 +68,26 @@ class DetailsMiddleware {
}
}
private fun prepareData() {
private fun prepareData(action: DetailsAction.PrepareScreen) {
val fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(storedFiatCurrencies))
}
scope.launch {
val loadedCurrencies = preferencesStorage.getFiatCurrencies()
if (loadedCurrencies.isNullOrEmpty()) {
val response = CoinMarketCapService().getFiatCurrencies()
withContext(Dispatchers.Main) {
when (response) {
is Result.Success -> {
preferencesStorage.saveFiatCurrencies(response.data)
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(response.data))
}
val tangemTechService = action.tangemTechService
when (val result = tangemTechService.coins.currencies()) {
is Result.Success -> {
val fiatCurrencies = result.data.currencies.filter {
it.type == Coins.CurrenciesResponse.CurrencyType.Fiat.type
}
if (fiatCurrencies.isNotEmpty() && fiatCurrencies.toSet() != storedFiatCurrencies.toSet()) {
fiatCurrenciesPrefStorage.save(fiatCurrencies)
dispatchOnMain(DetailsAction.AppCurrencyAction.SetCurrencies(fiatCurrencies))
}
}
} else {
withContext(Dispatchers.Main) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(loadedCurrencies))
}
is Result.Failure -> {}
}
}
}

View file

@ -43,13 +43,15 @@ private fun handlePrepareScreen(
action: DetailsAction.PrepareScreen,
state: DetailsState,
): DetailsState {
val backupIsActive = action.scanResponse.card.backupStatus?.isActive ?: false
return DetailsState(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName,
showAppCurrencyDialog = false,
),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
)

View file

@ -3,11 +3,11 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.tangemTech.Coins
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import org.rekotlin.StateType
import java.util.*
@ -51,8 +51,9 @@ data class SecurityScreenState(
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
val fiatCurrencies: List<Coins.CurrenciesResponse.Currency>? = null,
)

View file

@ -2,10 +2,10 @@ package com.tangem.tap.features.details.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.network.api.tangemTech.Coins
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import com.tangem.wallet.R
@ -13,11 +13,11 @@ class CurrencySelectionDialog {
var dialog: AlertDialog? = null
fun show(currencies: List<FiatCurrency>, currentAppCurrency: FiatCurrencyName, context: Context) {
fun show(currencies: List<Coins.CurrenciesResponse.Currency>, currentAppCurrency: FiatCurrencyName, context: Context) {
if (dialog == null) {
val currenciesToShow = currencies.map { it.toFormattedString() }.toTypedArray()
var currentSelection = currencies.indexOfFirst { it.symbol == currentAppCurrency }
var currentSelection = currencies.indexOfFirst { it.code == currentAppCurrency }
dialog = AlertDialog.Builder(context)
.setTitle(context.getString(R.string.details_row_title_currency))
@ -26,7 +26,7 @@ class CurrencySelectionDialog {
}
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
val selectedCurrency = currencies[currentSelection]
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.symbol))
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.code))
}
.setOnDismissListener {
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)

View file

@ -13,11 +13,9 @@ 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.ErrorConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
@ -27,7 +25,7 @@ import com.tangem.domain.redux.domainStore
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.features.tokens.addCustomToken.DomainErrorConverter
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
/**
@ -82,7 +80,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
@Composable
private fun FormFields(state: MutableState<AddCustomTokenState>) {
val context = LocalContext.current
val errorConverter = remember { DomainErrorConverter(context) }
val errorConverter = remember { ModuleMessageConverter(context) }
val stateValue = state.value
stateValue.form.fieldList.forEach { field ->
@ -99,11 +97,11 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
}
@Composable
fun Warnings(warnings: List<AddCustomTokenWarning>) {
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
if (warnings.isEmpty()) return
val context = LocalContext.current
val warningConverter = remember { DomainErrorConverter(context) }
val warningConverter = remember { ModuleMessageConverter(context) }
Column {
warnings.forEachIndexed { index, item ->
@ -120,7 +118,7 @@ fun Warnings(warnings: List<AddCustomTokenWarning>) {
) {
Text(
modifier = Modifier.padding(16.dp),
text = warningConverter.convertError(item),
text = warningConverter.convert(item),
color = colorResource(id = R.color.white),
fontSize = 14.sp
)
@ -172,14 +170,14 @@ private fun AddCustomTokenFab(
data class ScreenFieldData(
val field: DataField<*>,
val error: AddCustomTokenError?,
val errorConverter: ErrorConverter<String>,
val errorConverter: ModuleMessageConverter,
val viewState: ViewStates.TokenField
) {
companion object {
fun fromState(
field: DataField<*>,
state: AddCustomTokenState,
errorConverter: DomainErrorConverter
errorConverter: ModuleMessageConverter
): ScreenFieldData {
return ScreenFieldData(
field = field,

View file

@ -14,7 +14,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.common.services.Result
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TangemTechServiceManager
import com.tangem.domain.features.addCustomToken.AddCustomTokenService
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.network.api.tangemTech.TangemTechService
@ -147,7 +147,7 @@ private fun CustomActions() {
CustomActionButton(
name = "Find tokens in several networks",
action = {
val manager = TangemTechServiceManager(TangemTechService())
val manager = AddCustomTokenService(TangemTechService())
val currencies = manager.tokens()
val asdfsd = mutableMapOf<String, MutableList<Any>>()
val contractAddresses = currencies.mapNotNull { currency ->

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
@ -68,19 +68,27 @@ class TokensMiddleware {
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
val scanResponse = store.state.globalState.scanResponse ?: return
//TODO: bad things happens.
val currentTokens = store.state.tokensState.addedWallets.toTokens()
val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains(
store.state.tokensState.derivationStyle
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
scanResponse.card.derivationStyle
)
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
scanResponse.card.derivationStyle
)
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
val blockchainsToRemove = currentBlockchains.filter { !action.addedBlockchains.contains(it) }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it.token) }
val tokensToRemove = currentTokens.filter { token -> !action.addedTokens.any { it.token == token } }
val tokensToAdd = action.addedTokens.filter { !currentTokens.contains(it) }
val tokensToRemove = currentTokens.filter {
token -> !action.addedTokens.any { it.token == token.token }
}
val derivationStyle = scanResponse.card.derivationStyle
removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove)
removeCurrenciesIfNeeded(convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = derivationStyle
))
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
store.dispatchDebugErrorNotification("Nothing to save")
@ -88,12 +96,11 @@ class TokensMiddleware {
return
}
val derivationStyle = scanResponse.card.derivationStyle
val currencyList = blockchainsToAdd.map {
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
} + tokensToAdd.map {
Currency.Token(it.token, it.blockchain, it.blockchain.derivationPath(derivationStyle)?.rawPath)
}
val currencyList = convertToCurrencies(
blockchains = blockchainsToAdd,
tokens = tokensToAdd,
derivationStyle = derivationStyle
)
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList)
@ -105,6 +112,22 @@ class TokensMiddleware {
}
}
private fun convertToCurrencies(
blockchains: List<Blockchain>,
tokens: List<TokenWithBlockchain>,
derivationStyle: DerivationStyle?
): List<Currency> {
return blockchains.map {
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
} + tokens.map {
Currency.Token(
it.token,
it.blockchain,
it.blockchain.derivationPath(derivationStyle)?.rawPath
)
}
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
@ -233,17 +256,10 @@ class TokensMiddleware {
addActions.forEach { store.dispatchOnMain(it) }
}
private fun removeCurrenciesIfNeeded(blockchains: List<Blockchain>, tokens: List<Token>) {
if (tokens.isNotEmpty()) {
tokens.forEach { token ->
store.state.walletState.getWalletData(token)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
}
}
}
if (blockchains.isNotEmpty()) {
blockchains.forEach { blockchain ->
store.state.walletState.getWalletData(blockchain)?.let {
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isNotEmpty()) {
currencies.forEach { currency ->
store.state.walletState.getWalletData(currency)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(it))
}
}

View file

@ -22,15 +22,15 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(
addedBlockchains = action.wallets.toBlockchains(action.derivationStyle),
addedTokens = action.wallets.toTokensWithBlockchains(action.derivationStyle),
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
addedWallets = action.wallets,
derivationStyle = action.derivationStyle
)
}
is TokensAction.SetNonRemovableCurrencies -> {
tokensState.copy(
nonRemovableBlockchains = action.wallets.toBlockchains(tokensState.derivationStyle),
nonRemovableBlockchains = action.wallets.toNonCustomBlockchains(tokensState.derivationStyle),
nonRemovableTokens = action.wallets.toTokensContractAddresses(),
)
}

View file

@ -27,11 +27,13 @@ fun List<WalletData>.toTokensContractAddresses(): List<ContractAddress> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token?.contractAddress }.distinct()
}
fun List<WalletData>.toTokens(): List<Token> {
return mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }.distinct()
fun List<WalletData>.toNonCustomTokens(derivationStyle: DerivationStyle?): List<Token> {
return filter { !it.currency.isCustomCurrency(derivationStyle) }
.mapNotNull { (it.currency as? com.tangem.tap.features.wallet.redux.Currency.Token)?.token }
.distinct()
}
fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
fun List<WalletData>.toNonCustomTokensWithBlockchains(derivationStyle: DerivationStyle?): List<TokenWithBlockchain> {
return mapNotNull {
if (it.currency !is com.tangem.tap.features.wallet.redux.Currency.Token) return@mapNotNull null
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
@ -39,7 +41,7 @@ fun List<WalletData>.toTokensWithBlockchains(derivationStyle: DerivationStyle?):
}.distinct()
}
fun List<WalletData>.toBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
fun List<WalletData>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull {
if (it.currency.isCustomCurrency(derivationStyle)) {
null
@ -58,7 +60,7 @@ fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency
if (supportedBlockchains == null) return this
return map {
it.copy(contracts =
it.contracts?.filter {
it.contracts.filter {
supportedBlockchains.contains(it.blockchain) && it.blockchain.canHandleTokens()
}
)

View file

@ -59,7 +59,7 @@ fun CollapsedCurrencyItem(
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
text = currency.fullName,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),

View file

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@ -65,7 +64,7 @@ fun ExpandedCurrencyItem(
.align(Alignment.CenterVertically)
) {
Text(
text = currency.name,
text = currency.fullName,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF1C1C1E),
@ -87,9 +86,7 @@ fun ExpandedCurrencyItem(
)
}
val blockchains = currency.contracts?.map { it.blockchain } ?: listOfNotNull(
Blockchain.fromNetworkId(currency.id)
)
val blockchains = currency.contracts.map { it.blockchain }
Row {
Box(
@ -134,7 +131,7 @@ fun ExpandedCurrencyItem(
.padding(top = 6.dp),
) {
blockchains.map { blockchain ->
val contract = currency.contracts?.firstOrNull { it.blockchain == blockchain }
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
val added = if (contract != null && contract.address != currency.symbol) {
addedTokens.map { it.token.contractAddress }.contains(contract.address)
} else {

View file

@ -66,4 +66,7 @@ fun ListOfCurrencies(
}
}
}
}
val Currency.fullName: String
get() = "${this.name} (${this.symbol})"

View file

@ -46,7 +46,7 @@ fun NetworkItem(
.combinedClickable(
enabled = allowToAdd,
onLongClick = {
if (contract != null) onNetworkItemClicked(contract.address)
if (!isBlockchain) onNetworkItemClicked(contract!!.address)
},
onClick = {},
indication = null,

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.extensions.toQrCode
@ -75,19 +74,9 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
fun getWalletManager(token: Token?): WalletManager? {
if (token == null) return null
return wallets
.mapNotNull { it.walletManager }
.find { walletManager ->
walletManager.cardTokens.any { it.contractAddress == token.contractAddress }
}
}
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return wallets.map { it.walletManager }
.find { it?.wallet?.blockchain == currency.blockchain }
return getWalletStore(currency)?.walletManager
}
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
@ -131,22 +120,6 @@ data class WalletState(
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
}
fun getWalletData(token: Token?): WalletData? {
if (token == null) return null
return walletsData.find {
(it.currency as? Currency.Token)?.token == token &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getWalletData(blockchain: Blockchain?): WalletData? {
if (blockchain == null) return null
return walletsData.find {
(it.currency as? Currency.Blockchain)?.blockchain == blockchain &&
!it.currency.isCustomCurrency(store.state.globalState.scanResponse!!.card.derivationStyle)
}
}
fun getSelectedWalletData(): WalletData? {
return walletsData.find { it.currency == selectedCurrency }
}

View file

@ -1,5 +1,6 @@
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
@ -91,7 +92,7 @@ class MultiWalletMiddleware {
}
}
is Currency.Token -> {
val walletManager = walletState?.getWalletManager(currency.token)
val walletManager = walletState?.getWalletManager(currency)
if (walletManager != null) {
walletManager.removeToken(currency.token)
cardId?.let {
@ -222,6 +223,7 @@ class MultiWalletMiddleware {
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
walletState: WalletState?, globalState: GlobalState?
) {
if (tokens.isEmpty()) return
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory

View file

@ -50,7 +50,7 @@ class TradeCryptoMiddleware {
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
currency is Currency.Token && currency.blockchain.isTestnet()
) {
val walletManager = store.state.walletState.getWalletManager(currency.token)
val walletManager = store.state.walletState.getWalletManager(currency)
if (walletManager !is EthereumWalletManager) return
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }

View file

@ -99,7 +99,8 @@ class MultiWalletReducer {
addTokens(listOf(action.token), action.blockchain, state)
}
is WalletAction.MultiWallet.TokenLoaded -> {
val pendingTransactions = state.getWalletManager(action.token)
val currency = Currency.fromBlockchainNetwork(action.blockchain, action.token)
val pendingTransactions = state.getWalletManager(currency)
?.wallet?.let { wallet ->
wallet.recentTransactions.toPendingTransactions(wallet.address)
} ?: emptyList()
@ -113,7 +114,7 @@ class MultiWalletReducer {
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenWalletData = state.getWalletData(action.token)
val tokenWalletData = state.getWalletData(currency)
val newTokenWalletData = tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
@ -166,11 +167,10 @@ private fun addTokens(
fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletData? {
if (!state.isMultiwalletAllowed) return null
if (state.currencies.any { it is Currency.Token && it.token == this }) {
return null
}
val currency = Currency.fromBlockchainNetwork(blockchain, this)
if (state.currencies.contains(currency)) return null
val walletManager = state.getWalletManager(this)?.wallet
val walletManager = state.getWalletManager(currency)?.wallet
val walletAddresses = createAddressList(walletManager)
return WalletData(
@ -181,6 +181,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = Currency.fromBlockchainNetwork(blockchain, this)
currency = currency
)
}

View file

@ -74,7 +74,8 @@ class OnWalletLoadedReducer {
)
val tokens = wallet.getTokens().mapNotNull { token ->
val tokenWalletData = walletState.getWalletData(token)
val currency = Currency.fromBlockchainNetwork(blockchainNetwork, token)
val tokenWalletData = walletState.getWalletData(currency)
val tokenPendingTransactions =
pendingTransactions.filter { it.currency == token.symbol }
val tokenBalanceStatus = when {

View file

@ -164,7 +164,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
)
}
val wallets = newState.updateTradeCryptoState(exchangeManager, newState.replaceSomeWallets(newWallets))
val wallets = newState.updateTradeCryptoState(
exchangeManager,
newState.replaceSomeWallets(newWallets)
)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
newState = newState.updateWalletStore(walletStore)
}
@ -217,7 +220,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
),
)
val tokenWallets = action.wallet.getTokens()
.mapNotNull { newState.getWalletData(it) }
.mapNotNull { token ->
walletStore?.blockchainNetwork?.let {
newState.getWalletData(Currency.fromBlockchainNetwork(it, token))
}
}
.map {
it.copy(
currencyData = it.currencyData.copy(
@ -288,12 +295,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.updateWalletData(selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
newState = newState.updateWalletData(
selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list
)
)
))
)
}
is WalletAction.SetWalletRent -> {
var walletData = newState.getWalletData(action.blockchain)

View file

@ -166,7 +166,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
store.state.globalState.appCurrency
store.state.globalState.appCurrency,
tangemTechService = store.state.domainNetworks.tangemTechService
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true

View file

@ -115,6 +115,7 @@ class WalletAdapter
private fun toggleWarning(show: Boolean) {
binding.tvExchangeRate.show(!show)
binding.tvCustomCurrency.show(!show)
binding.tvStatusErrorMessage.show(show)
}
}

View file

@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
//TODO: refactoring: move to the domain module and aggregate it as the alternative service for TangemTech
class CoinMarketCapService() {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create(getApiKey()) }

View file

@ -0,0 +1,36 @@
package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.network.api.tangemTech.Coins
/**
[REDACTED_AUTHOR]
*/
class FiatCurrenciesPrefStorage(
private val preferences: SharedPreferences,
private val converter: MoshiJsonConverter,
) {
private val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
private val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
fun migrate() {
preferences.edit(true) {
remove(FIAT_CURRENCIES_KEY_OLD)
}
}
fun save(currencies: List<Coins.CurrenciesResponse.Currency>) {
val json: String = converter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
fun restore(): List<Coins.CurrenciesResponse.Currency> {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
val type = converter.typedList(Coins.CurrenciesResponse.Currency::class.java)
if (json.isNullOrBlank()) return emptyList()
return converter.fromJson(json, type) ?: emptyList()
}
}

View file

@ -4,13 +4,9 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.util.*
@ -20,39 +16,24 @@ class PreferencesStorage(applicationContext: Application) {
val appRatingLaunchObserver: AppRatingLaunchObserver
val usedCardsPrefStorage: UsedCardsPrefStorage
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
usedCardsPrefStorage = UsedCardsPrefStorage(preferences)
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
usedCardsPrefStorage.migrate()
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val type = Types.newParameterizedType(List::class.java, FiatCurrency::class.java)
moshi.adapter(type)
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE)
fiatCurrenciesPrefStorage.migrate()
}
fun getAppCurrency(): FiatCurrencyName {
return preferences.getString(APP_CURRENCY_KEY, DEFAULT_FIAT_CURRENCY)
?: DEFAULT_FIAT_CURRENCY
?: DEFAULT_FIAT_CURRENCY
}
fun saveAppCurrency(fiatCurrencyName: FiatCurrencyName) {
return preferences.edit().putString(APP_CURRENCY_KEY, fiatCurrencyName).apply()
}
fun getFiatCurrencies(): List<FiatCurrency>? {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
return if (json.isNullOrBlank()) null else fiatCurrenciesAdapter.fromJson(json) as List<FiatCurrency>
}
fun saveFiatCurrencies(currencies: List<FiatCurrency>) {
val json: String = fiatCurrenciesAdapter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
preferences.edit { putString(APP_CURRENCY_KEY, fiatCurrencyName) }
}
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
@ -63,7 +44,7 @@ class PreferencesStorage(applicationContext: Application) {
}
fun saveDisclaimerAccepted() {
preferences.edit().putBoolean(DISCLAIMER_ACCEPTED_KEY, true).apply()
preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) }
}
fun wasDisclaimerAccepted(): Boolean {
@ -71,7 +52,7 @@ class PreferencesStorage(applicationContext: Application) {
}
fun saveTwinsOnboardingShown() {
preferences.edit().putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true).apply()
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
fun wasTwinsOnboardingShown(): Boolean {
@ -86,7 +67,6 @@ class PreferencesStorage(applicationContext: Application) {
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"

View file

@ -11,10 +11,9 @@ import timber.log.Timber
*/
class UsedCardsPrefStorage(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter
) {
private val jsonConverter = MoshiJsonConverter.INSTANCE
internal fun migrate() {
val scannedIds = preferences.getString(SCANNED_CARDS_IDS_KEY, null) ?: return
@ -27,7 +26,7 @@ class UsedCardsPrefStorage(
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: UsedCardInfo(cardId, true)
?: UsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
@ -39,7 +38,7 @@ class UsedCardsPrefStorage(
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: UsedCardInfo(cardId, isActivationStarted = true)
?: UsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
@ -55,7 +54,7 @@ class UsedCardsPrefStorage(
fun activationFinished(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = false)
?: UsedCardInfo(cardId, isActivationStarted = false)
?: UsedCardInfo(cardId, isActivationStarted = false)
save(foundItem, restoredList)
}

View file

@ -122,8 +122,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
android:background="@drawable/shape_rectangle_rounded_4"

1
common/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

9
common/build.gradle Normal file
View file

@ -0,0 +1,9 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View file

@ -0,0 +1,8 @@
package com.tangem.common
/**
[REDACTED_AUTHOR]
*/
interface Validator<Data, Error> {
fun validate(data: Data? = null): Error?
}

View file

@ -0,0 +1,14 @@
package com.tangem.common.module
/**
[REDACTED_AUTHOR]
* A module exception
*/
interface ModuleException {
val message: String
}
/**
* An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue.
*/
interface FbConsumeException

View file

@ -0,0 +1,22 @@
package com.tangem.common.module
/**
[REDACTED_AUTHOR]
* The base object for communication between modules
*/
interface ModuleMessage
interface ModuleMessageConverter<ModuleMessage, R> {
fun convert(message: ModuleMessage): R
}
/**
* @property code describes what feature is the error coming from
* @property message the error description
* @property data any data that can help in the part where this error is being handled
*/
interface ModuleError : ModuleMessage {
val code: Int
val message: String
val data: Any?
}

View file

@ -47,13 +47,14 @@ android {
dependencies {
implementation implementation(project(path: ':network'))
implementation implementation(project(path: ':common'))
// Tangem sdk's
implementation 'com.tangem:blockchain:develop-71'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142'
// Kotlin
// Kotlin coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
// State management

View file

@ -1,29 +1,46 @@
package com.tangem.domain
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
* @property code describes what feature is the error coming from
* @property message the error description
* @property data any data that can help in the part where this error is being handled
* All DomainError descendants must use their own range of codes, but no more than 999 error codes for each.
*/
interface DomainError : DomainMessage {
val code: Int
val message: String
val data: Any?
}
sealed interface DomainModuleMessage : ModuleMessage
open class AnError(
override val code: Int,
sealed class DomainError(
subCode: Int,
override val message: String,
override val data: Any? = null,
) : DomainError
override val data: Any?,
) : DomainModuleMessage, ModuleError {
override val code: Int = ERROR_CODE_DOMAIN + subCode
interface ErrorConverter<T> {
fun convertError(error: DomainError): T
companion object {
// base code used for all error in the module
const val ERROR_CODE_DOMAIN = 10000
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
// const val CODE_ANY_OTHER = 200..299
}
}
interface Validator<Data, Error> {
fun validate(data: Data? = null): Error?
}
sealed class AddCustomTokenError(
subCode: Int = 0
) : DomainError(ERROR_CODE_ADD_CUSTOM_TOKEN + subCode, this::class.java.simpleName, null) {
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
object FieldIsEmpty : AddCustomTokenError()
object FieldIsNotEmpty : AddCustomTokenError()
object InvalidContractAddress : AddCustomTokenError()
object NetworkIsNotSelected : AddCustomTokenError()
object InvalidDecimalsCount : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
sealed class Network : AddCustomTokenError() {
object CheckAddressRequestError : Network()
}
sealed class Warning : AddCustomTokenError() {
object PotentialScamToken : Warning()
object TokenAlreadyAdded : Warning()
}
}

View file

@ -1,17 +1,19 @@
package com.tangem.domain
import com.tangem.common.module.FbConsumeException
import com.tangem.common.module.ModuleException
/**
[REDACTED_AUTHOR]
* Must be handled by the module or sent to Crashlytics
*/
interface DomainInternalException
sealed class AddCustomTokenException(override val message: String) : Throwable(message), ModuleException {
sealed class DomainException(message: String?) : Throwable(message), DomainInternalException {
data class SelectTokeNetworkException(val networkId: String) : DomainException(
data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException(
"Unknown network [$networkId] should not be included in the network selection dialog."
)
), FbConsumeException
data class UnAppropriateInitializationException(val of: String, val info: String? = null) : DomainException(
"The [$of], must be properly initialized. Info []"
)
data class UnAppropriateInitializationException(
val of: String,
val info: String? = null
) : AddCustomTokenException("The [$of], must be properly initialized. Info [$info]")
}

View file

@ -1,15 +0,0 @@
package com.tangem.domain
/**
[REDACTED_AUTHOR]
*/
sealed interface DomainMessage
sealed interface DomainNotification : DomainMessage {
interface Toast : DomainNotification {}
interface Snackbar : DomainNotification {}
interface Dialog : DomainNotification {}
}

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Blockchain
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"avalanche" -> Blockchain.Avalanche
"avalanche-testnet" -> Blockchain.AvalancheTestnet
"avalanche", "avalanche-2" -> Blockchain.Avalanche
"avalanche-testnet", "avalanche-2-testnet" -> Blockchain.AvalancheTestnet
"binancecoin" -> Blockchain.Binance
"binancecoin-testnet" -> Blockchain.BinanceTestnet
"binance-smart-chain" -> Blockchain.BSC

View file

@ -2,8 +2,8 @@ package com.tangem.domain.common.form
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.Validator
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.common.Validator
import com.tangem.domain.AddCustomTokenError
/**
[REDACTED_AUTHOR]

View file

@ -3,12 +3,11 @@ package com.tangem.domain.features.addCustomToken
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.Coins
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.AddHeaderInterceptor
/**
[REDACTED_AUTHOR]
*/
class TangemTechServiceManager(
class AddCustomTokenService(
private val tangemTechService: TangemTechService
) {
@ -44,21 +43,9 @@ class TangemTechServiceManager(
return when (val result = tangemTechService.coins.tokens()) {
is Result.Success -> {
val tokens = result.data.tokens
tokens.filter {
it.contracts.isNullOrEmpty()
}
tokens.filter { it.contracts.isNullOrEmpty() }
}
is Result.Failure -> emptyList()
}
}
fun attachAuthKey(authKey: String) {
tangemTechService.addHeaderInterceptors(listOf(
CardPublicKeyHttpInterceptor(authKey),
))
}
}
private class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf(
"card_public_key" to cardPublicKeyHex,
))
}

View file

@ -1,25 +0,0 @@
package com.tangem.domain.features.addCustomToken
import com.tangem.domain.AnError
import com.tangem.domain.ERROR_CODE_ADD_CUSTOM_TOKEN
/**
[REDACTED_AUTHOR]
*/
sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - error") {
object FieldIsEmpty : AddCustomTokenError()
object FieldIsNotEmpty : AddCustomTokenError()
object InvalidContractAddress : AddCustomTokenError()
object NetworkIsNotSelected : AddCustomTokenError()
object InvalidDecimalsCount : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
sealed class Network : AddCustomTokenWarning() {
object CheckAddressRequestError : Network()
}
}
sealed class AddCustomTokenWarning : AddCustomTokenError() {
object PotentialScamToken : AddCustomTokenWarning()
object TokenAlreadyAdded : AddCustomTokenWarning()
}

View file

@ -2,11 +2,10 @@ package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.form.Field
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
import org.rekotlin.Action
@ -47,9 +46,12 @@ sealed class AddCustomTokenAction : Action {
// warnings
sealed class Warning : AddCustomTokenAction() {
data class Add(val warnings: Set<AddCustomTokenWarning>) : Warning()
data class Remove(val warnings: Set<AddCustomTokenWarning>) : Warning()
data class Replace(val remove: Set<AddCustomTokenWarning>, val add: Set<AddCustomTokenWarning>) : Warning()
data class Add(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
data class Remove(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
data class Replace(
val remove: Set<AddCustomTokenError.Warning>,
val add: Set<AddCustomTokenError.Warning>
) : Warning()
}
// To change the screenState

View file

@ -4,10 +4,12 @@ import android.webkit.ValueCallback
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken
import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded
import com.tangem.domain.AddCustomTokenException
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainException
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
@ -22,7 +24,6 @@ import com.tangem.domain.redux.dispatchOnMain
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.network.api.tangemTech.Coins
import com.tangem.network.api.tangemTech.TangemTechService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -147,9 +148,9 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private suspend fun updateWarningAlreadyAdded(isInAppSavedList: Boolean) {
if (isInAppSavedList) {
AddCustomTokenWarning.TokenAlreadyAdded.add()
TokenAlreadyAdded.add()
} else {
AddCustomTokenWarning.TokenAlreadyAdded.remove()
TokenAlreadyAdded.remove()
}
}
@ -172,7 +173,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val result = when (foundTokensResult) {
is Result.Success -> foundTokensResult.data
is Result.Failure -> {
// val warning = AddCustomTokenWarning.Network.CheckAddressRequestError
// val warning = Warning.Network.CheckAddressRequestError
// dispatchOnMain(Warning.Add(setOf(warning)))
emptyList()
}
@ -184,8 +185,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private suspend fun manageFoundTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
if (foundTokens.isEmpty()) {
// token not found - it's completely custom
AddCustomTokenWarning.TokenAlreadyAdded.remove()
AddCustomTokenWarning.PotentialScamToken.add()
TokenAlreadyAdded.remove()
PotentialScamToken.add()
dispatchOnMain(SetFoundTokenId(null))
clearTokenFields()
@ -210,33 +211,33 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
if (isInAppSavedTokens) {
lockTokenFields()
lockAddButton()
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
PotentialScamToken.replace(TokenAlreadyAdded)
} else {
// not in the saved tokens list
if (singleTokenContract.active) {
lockTokenFields()
unlockAddButton()
if (hubState.derivationPathIsSelected()) {
AddCustomTokenWarning.PotentialScamToken.add()
PotentialScamToken.add()
} else {
AddCustomTokenWarning.TokenAlreadyAdded.remove()
AddCustomTokenWarning.PotentialScamToken.remove()
TokenAlreadyAdded.remove()
PotentialScamToken.remove()
}
} else {
unlockAddButton()
AddCustomTokenWarning.PotentialScamToken.add()
PotentialScamToken.add()
}
}
}
else -> {
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
PotentialScamToken.replace(TokenAlreadyAdded)
val dialog = DomainDialog.SelectTokenDialog(
items = foundToken.contracts,
networkIdConverter = { networkId ->
val blockchain = Blockchain.fromNetworkId(networkId)
if (blockchain == null || blockchain == Blockchain.Unknown) {
throw DomainException.SelectTokeNetworkException(networkId)
throw AddCustomTokenException.SelectTokeNetworkException(networkId)
}
hubState.blockchainToName(blockchain) ?: ""
},
@ -255,8 +256,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
private suspend fun replaceWarnings(
warningsAdd: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
warningsRemove: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
warningsAdd: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
warningsRemove: MutableSet<AddCustomTokenError.Warning> = mutableSetOf(),
) {
if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) {
dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet()))
@ -265,7 +266,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private suspend fun updateAddButton() {
val state = hubState
if (state.warnings.contains(AddCustomTokenWarning.TokenAlreadyAdded)) {
if (state.warnings.contains(TokenAlreadyAdded)) {
lockAddButton()
return
}
@ -485,19 +486,19 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
dispatchOnMain(action)
}
private suspend fun AddCustomTokenWarning.add() {
private suspend fun AddCustomTokenError.Warning.add() {
dispatchOnMain(Warning.Add(setOf(this)))
}
private suspend fun AddCustomTokenWarning.remove() {
private suspend fun AddCustomTokenError.Warning.remove() {
dispatchOnMain(Warning.Remove(setOf(this)))
}
private suspend fun AddCustomTokenWarning.replace(to: AddCustomTokenWarning) {
private suspend fun AddCustomTokenError.Warning.replace(to: AddCustomTokenError.Warning) {
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
}
// private suspend fun AddCustomTokenWarning.replace(replace: Boolean, to: AddCustomTokenWarning) {
// private suspend fun Warning.replace(replace: Boolean, to: Warning) {
// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
// }
@ -512,8 +513,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
is OnCreate -> {
val card = requireNotNull(globalState.scanResponse?.card)
val tangemTechServiceManager = TangemTechServiceManager(TangemTechService())
tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString())
val tangemTechServiceManager = AddCustomTokenService(globalState.networkServices.tangemTechService)
var derivationPathState = state.screenState.derivationPath
derivationPathState = when (card.derivationStyle) {
@ -656,7 +656,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw DomainException.UnAppropriateInitializationException(
throw AddCustomTokenException.UnAppropriateInitializationException(
"AddCustomTokenHub", "$objName must be not NULL"
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
@ -16,9 +17,9 @@ data class AddCustomTokenState(
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
val tokenId: String? = null,
val warnings: Set<AddCustomTokenWarning> = emptySet(),
val warnings: Set<AddCustomTokenError.Warning> = emptySet(),
val screenState: ScreenState = createInitialScreenState(),
val tangemTechServiceManager: TangemTechServiceManager? = null
val tangemTechServiceManager: AddCustomTokenService? = null
) : StateType {
inline fun <reified T> getField(id: FieldId): T = form.getField(id) as T

View file

@ -9,6 +9,6 @@ import org.rekotlin.Action
*/
//TODO: refactoring: is alias for the GlobalAction
sealed class DomainGlobalAction : Action {
data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction()
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction()
data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction()
}

View file

@ -1,8 +1,10 @@
package com.tangem.domain.redux.global
import android.webkit.ValueCallback
import com.tangem.common.extensions.toHexString
import com.tangem.domain.redux.BaseStoreHub
import com.tangem.domain.redux.DomainState
import com.tangem.network.common.CardPublicKeyHttpInterceptor
import org.rekotlin.Action
/**
@ -33,7 +35,11 @@ internal class DomainGlobalHub : BaseStoreHub<DomainGlobalState>("DomainGlobalHu
}
override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState = when (action) {
is DomainGlobalAction.SetScanResponse -> {
is DomainGlobalAction.SaveScanNoteResponse -> {
val cardPublicKeyHex = action.scanResponse.card.cardPublicKey.toHexString()
state.networkServices.tangemTechService.addHeaderInterceptors(
listOf(CardPublicKeyHttpInterceptor(cardPublicKeyHex))
)
state.copy(scanResponse = action.scanResponse)
}
is DomainGlobalAction.ShowDialog -> {

View file

@ -2,12 +2,20 @@ package com.tangem.domain.redux.global
import com.tangem.domain.DomainDialog
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.tangemTech.TangemTechService
/**
[REDACTED_AUTHOR]
*/
//TODO: refactoring: is alias for the GlobalState
data class DomainGlobalState(
// there is a part of mirrors from the GlobalState.
// It updates on GlobalAction.SaveScanNoteResponse -> DomainGlobalAction.SaveScanNoteResponse(scanResponse)
val scanResponse: ScanResponse? = null,
//
val networkServices: NetworkServices = NetworkServices(),
val dialog: DomainDialog? = null,
)
data class NetworkServices(
val tangemTechService: TangemTechService = TangemTechService()
)

View file

@ -9,9 +9,17 @@ java {
}
dependencies {
implementation implementation(project(path: ':common'))
// Tangem sdk's
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142'
// Kotlin coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
// Logs
implementation 'com.jakewharton.timber:timber:4.7.1'
// Network
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3"))
implementation("com.squareup.okhttp3:okhttp")

View file

@ -9,12 +9,7 @@ interface HttpResponse
sealed interface TangemTechResponse : HttpResponse
sealed class Coins : TangemTechResponse {
data class PricesResponse(val prices: List<Price>) : Coins() {
data class Price(
val name: String,
val price: BigDecimal,
)
}
data class PricesResponse(val prices: Map<String, Double>) : Coins()
data class CheckAddressResponse(val imageHost: String?, val tokens: List<Token>, val total: Int) : Coins() {
data class Token(
@ -51,11 +46,15 @@ sealed class Coins : TangemTechResponse {
data class CurrenciesResponse(val currencies: List<Currency>) {
data class Currency(
val id: String,
val code: String,
val code: String, // this is an uppercase id
val name: String,
val rateBTC: String,
val unit: String,
val unit: String, // $, €, ₽
val type: String,
)
enum class CurrencyType(val type: String) {
Fiat("fiat"), Crypto("crypto")
}
}
}

View file

@ -11,7 +11,7 @@ interface TangemTechApi {
@GET("coins/prices")
suspend fun coinsPrices(
@Query("currency") currency: String,
@Query("ids") ids: List<String>,
@Query("ids") ids: String,
): Coins.PricesResponse
@GET("coins/check-address")

View file

@ -5,6 +5,8 @@ import com.tangem.common.services.performRequest
import com.tangem.network.common.AddHeaderInterceptor
import com.tangem.network.common.CacheControlHttpInterceptor
import com.tangem.network.common.createRetrofitInstance
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
@ -32,7 +34,8 @@ class TangemTechService {
private fun createApi(): TangemTechApi {
val retrofit = createRetrofitInstance(
baseUrl = baseUrl,
interceptors = headerInterceptors.toList()
interceptors = headerInterceptors.toList(),
// logEnabled = true,
)
return retrofit.create(TangemTechApi::class.java).apply {
techRoutes.forEach { it.setApi(this) }
@ -59,22 +62,24 @@ class CoinsRoute : TangemTechRoute {
suspend fun prices(
currency: String,
ids: List<String>
): Result<Coins.PricesResponse> {
return performRequest { api.coinsPrices(currency, ids) }
): Result<Coins.PricesResponse> = withContext(Dispatchers.IO) {
performRequest {
api.coinsPrices(currency.lowercase(), ids.joinToString(","))
}
}
suspend fun checkAddress(
contractAddress: String,
networkId: String? = null
): Result<Coins.CheckAddressResponse> {
return performRequest { api.coinsCheckAddress(contractAddress, networkId) }
): Result<Coins.CheckAddressResponse> = withContext(Dispatchers.IO) {
performRequest { api.coinsCheckAddress(contractAddress, networkId) }
}
suspend fun currencies(): Result<Coins.CurrenciesResponse> {
return performRequest { api.coinsCurrencies() }
suspend fun currencies(): Result<Coins.CurrenciesResponse> = withContext(Dispatchers.IO) {
performRequest { api.coinsCurrencies() }
}
suspend fun tokens(): Result<Coins.TokensResponse> {
return performRequest { api.coinsTokens() }
suspend fun tokens(): Result<Coins.TokensResponse> = withContext(Dispatchers.IO) {
performRequest { api.coinsTokens() }
}
}

View file

@ -23,4 +23,8 @@ open class AddHeaderInterceptor(
class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf(
"Cache-Control" to "max-age=$maxAgeSeconds",
))
class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf(
"card_public_key" to cardPublicKeyHex,
))

View file

@ -0,0 +1,23 @@
package com.tangem.network.common
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleMessage
/**
[REDACTED_AUTHOR]
*/
sealed interface NetworkModuleMessage : ModuleMessage
sealed class NetworkError(
subCode: Int,
override val message: String,
override val data: Any?,
) : NetworkModuleMessage, ModuleError {
override val code: Int = ERROR_CODE_NETWORK + subCode
companion object {
// base code used for all error in the module
const val ERROR_CODE_NETWORK = 20000
// const val CODE_ANY_OTHER = 100..199
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.network.common
/**
[REDACTED_AUTHOR]
*/
interface NetworkInternalException
sealed class NetworkException(message: String?) : Throwable(message), NetworkInternalException {
}

View file

@ -1,3 +1,4 @@
include ':app'
include ':domain'
include ':network'
include ':common'