Updated on 2026-08-14

This commit is contained in:
Tangem 2022-05-23 20:31:27 +04:00
commit 13d9e4cc82
26 changed files with 610 additions and 600 deletions

View file

@ -5,7 +5,12 @@ import android.content.Context
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.SimpleAlertDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
@ -16,6 +21,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDisc
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.features.wallet.ui.wallet.CurrencySelectionDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -54,7 +60,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
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 AppDialog.CurrencySelectionDialog ->
CurrencySelectionDialog.create(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog ->
CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,

View file

@ -39,10 +39,23 @@ fun BigDecimal.toFormattedCurrencyString(
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: String): String {
fun BigDecimal.toFiatRateString(
fiatCurrencyName: String
): String {
val value = this
.setScale(2, RoundingMode.HALF_UP)
.formatWithSpaces()
return "$value $fiatCurrencyName"
}
fun BigDecimal.toFiatString(
rateValue: BigDecimal,
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.HALF_UP)
return "≈ ${fiatCurrencyName} $fiatValue"
return fiatValue.toFormattedFiatValue(fiatCurrencyName, formatWithSpaces)
}
fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
@ -50,8 +63,12 @@ fun BigDecimal.toFiatValue(rateValue: BigDecimal): BigDecimal {
return fiatValue.setScale(2, RoundingMode.HALF_UP)
}
fun BigDecimal.toFormattedFiatValue(fiatCurrencyName: String): String {
return "≈ ${fiatCurrencyName} $this"
fun BigDecimal.toFormattedFiatValue(
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
val fiatValue = if (formatWithSpaces) this.formatWithSpaces() else this
return "${fiatValue} $fiatCurrencyName"
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
@ -111,4 +128,33 @@ fun BigDecimal.formatAmountAsSpannedString(
append(' ')
append(currencySymbol)
}
}
fun BigDecimal.formatWithSpaces(): String {
val str = this.toString()
var integerStr = str.substringBefore('.')
val reminderStr = str.substringAfter('.')
val packets = arrayListOf<String>()
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(0, integerStr)
break
}
index -= 3
packets.add(integerStr.substring(startIndex = index))
integerStr = integerStr.substring(startIndex = 0, endIndex = index)
}
return buildString {
packets.forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}
if (reminderStr.isNotBlank()) {
append('.')
append(reminderStr)
}
}
}

View file

@ -1,7 +1,5 @@
package com.tangem.tap.common.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.content.*
import android.content.res.Resources
@ -192,45 +190,27 @@ fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, formatArgs)
}
fun View.showAnimated(durationMillis: Long = 300) {
this.animateVisibility(
show = true,
durationMillis = durationMillis
)
}
fun View.hideAnimated(
durationMillis: Long = 300,
hiddenVisibility: Int = View.GONE
) {
this.animateVisibility(
show = false,
durationMillis = durationMillis,
hiddenVisibility = hiddenVisibility
)
}
private fun View.animateVisibility(
fun View.animateVisibility(
show: Boolean,
durationMillis: Long = 300,
durationMillis: Long = SHORT_ANIMATION_DURATION,
hiddenVisibility: Int = View.GONE
) {
if (this.isVisible == show) return
if (show) {
this.alpha = 0f
this.isVisible = true
this.animate()
.alpha(1f)
.setDuration(durationMillis)
.setListener(null)
.withStartAction {
this.alpha = 0f
this.isVisible = true
}
} else {
this.animate()
.alpha(0f)
.setDuration(durationMillis)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
this@animateVisibility.visibility = hiddenVisibility
}
})
.withStartAction {
this.visibility = hiddenVisibility
}
}
}
}
private const val SHORT_ANIMATION_DURATION = 80L

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.TestAction
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
@ -12,7 +13,12 @@ interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(val headerId: Int, val messageId: Int, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(
val headerId: Int,
val messageId: Int,
val onOk: VoidCallback? = null
) : AppDialog()
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,
@ -22,4 +28,9 @@ sealed class AppDialog : StateDialog {
data class TestActionsDialog(
val actionsList: List<TestAction>
) : AppDialog()
data class CurrencySelectionDialog(
val currenciesList: List<FiatCurrency>,
val currentAppCurrency: FiatCurrency,
) : AppDialog()
}

View file

@ -34,17 +34,17 @@ class TapWalletManager {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler = ThrottlerWithValues<Blockchain, Result<Wallet>>(10000)
private val walletManagersThrottler = ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
suspend fun loadWalletData(walletManager: WalletManager) {
val blockchain = walletManager.wallet.blockchain
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
walletManagersThrottler.geValue(blockchain)!!
val result = if (walletManagersThrottler.isStillThrottled(blockchainNetwork)) {
walletManagersThrottler.geValue(blockchainNetwork)!!
} else {
val safeUpdateResult = walletManager.safeUpdate()
walletManagersThrottler.updateThrottlingTo(blockchain)
walletManagersThrottler.setValue(blockchain, safeUpdateResult)
walletManagersThrottler.updateThrottlingTo(blockchainNetwork)
walletManagersThrottler.setValue(blockchainNetwork, safeUpdateResult)
safeUpdateResult
}
when (result) {

View file

@ -4,9 +4,7 @@ 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.TangemTechService
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.wallet.R
@ -18,9 +16,6 @@ sealed class DetailsAction : Action {
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrency,
val fiatCurrencies: List<FiatCurrency>? = null,
val tangemTechService: TangemTechService,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -46,13 +41,6 @@ sealed class DetailsAction : Action {
object CreateBackup : DetailsAction()
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()

View file

@ -3,24 +3,23 @@ package com.tangem.tap.features.details.redux
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.CurrenciesResponse
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
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.currenciesRepository
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
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.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -28,15 +27,12 @@ import org.rekotlin.Middleware
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val appCurrencyMiddleware = AppCurrencyMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> prepareData(action)
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
@ -69,48 +65,6 @@ class DetailsMiddleware {
}
}
private fun prepareData(action: DetailsAction.PrepareScreen) {
val fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatch(
DetailsAction.AppCurrencyAction.SetCurrencies(
currencies = storedFiatCurrencies.mapToUiModel()
)
)
}
scope.launch {
val tangemTechService = action.tangemTechService
when (val result = tangemTechService.currencies()) {
is Result.Success -> {
val currenciesList = result.data.currencies
if (currenciesList.isNotEmpty() &&
currenciesList.toSet() != storedFiatCurrencies.toSet()
) {
fiatCurrenciesPrefStorage.save(currenciesList)
dispatchOnMain(
DetailsAction.AppCurrencyAction.SetCurrencies(
currencies = currenciesList.mapToUiModel()
)
)
}
}
is Result.Failure -> {}
}
}
}
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {
return this.map {
FiatCurrency(
code = it.code,
name = it.name,
symbol = it.unit
)
}
}
class EraseWalletMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
@ -158,22 +112,6 @@ class DetailsMiddleware {
}
}
class AppCurrencyMiddleware {
fun handle(action: DetailsAction.AppCurrencyAction) {
when (action) {
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
store.state.globalState.tapWalletManager.rates.clear()
preferencesStorage.fiatCurrenciesPrefStorage
.saveAppCurrency(action.fiatCurrency)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletAction.LoadFiatRate())
}
else -> { /* no-op */
}
}
}
}
class ManageSecurityMiddleware {
fun handle(action: DetailsAction.ManageSecurity) {
when (action) {

View file

@ -29,9 +29,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.AppCurrencyAction -> {
handleAppCurrencyAction(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
@ -48,10 +45,6 @@ private fun handlePrepareScreen(
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = state.appCurrencyState.copy(
currentFiatCurrency = action.fiatCurrencyName,
showAppCurrencyDialog = false,
),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
)
@ -93,31 +86,6 @@ private fun handleEraseWallet(
}
}
private fun handleAppCurrencyAction(
action: DetailsAction.AppCurrencyAction, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppCurrencyAction.SetCurrencies -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(fiatCurrencies = action.currencies))
}
DetailsAction.AppCurrencyAction.ChooseAppCurrency -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = true))
}
DetailsAction.AppCurrencyAction.Cancel -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = false))
}
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
state.copy(
appCurrencyState = state.appCurrencyState.copy(
currentFiatCurrency = action.fiatCurrency,
showAppCurrencyDialog = false
)
)
}
else -> state
}
}
private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState,
): DetailsState {

View file

@ -4,7 +4,6 @@ import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.store
import org.rekotlin.StateType
@ -15,7 +14,6 @@ data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
@ -48,10 +46,4 @@ data class SecurityScreenState(
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val currentFiatCurrency: FiatCurrency = FiatCurrency.Default,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.details.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.store
import com.tangem.wallet.R
class CurrencySelectionDialog {
var dialog: AlertDialog? = null
fun show(
currenciesList: List<FiatCurrency>,
currentAppCurrency: FiatCurrency,
context: Context
) {
if (dialog == null) {
val currenciesToShow = currenciesList
.map { it.displayName }
.toTypedArray()
var currentSelection = currenciesList
.indexOfFirst { it.code == currentAppCurrency.code }
dialog = AlertDialog.Builder(context)
.setTitle(context.getString(R.string.details_row_title_currency))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ ->
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
val selectedCurrency = currenciesList[currentSelection]
store.dispatch(
DetailsAction.AppCurrencyAction.SelectAppCurrency(
fiatCurrency = selectedCurrency
)
)
}
.setOnDismissListener {
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
currentSelection = which
}.show()
}
}
fun clear() {
dialog = null
}
}

View file

@ -24,7 +24,6 @@ import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private var currencySelectionDialog = CurrencySelectionDialog()
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
@ -115,11 +114,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(DetailsAction.CreateBackup)
}
tvAppCurrency.text = state.appCurrencyState.currentFiatCurrency.code
tvAppCurrencyTitle.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
@ -140,17 +134,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
null -> null
}
currentSecurity?.let { tvSecurity.text = getString(it) }
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
currenciesList = state.appCurrencyState.fiatCurrencies,
currentAppCurrency = state.appCurrencyState.currentFiatCurrency,
context = requireContext()
)
} else {
currencySelectionDialog.clear()
}
}
}

View file

@ -1,17 +1,22 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.Card
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
import org.rekotlin.Action
sealed class WalletAction : Action {
@ -98,7 +103,10 @@ sealed class WalletAction : Action {
data class LoadFiatRate(
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(val fiatRate: Pair<Currency, BigDecimal?>) : WalletAction()
data class Success(
val fiatRates: Map<Currency, BigDecimal?>
) : WalletAction()
object Failure : WalletAction()
}
@ -163,4 +171,9 @@ sealed class WalletAction : Action {
) : WalletAction()
data class RemoveWalletRent(val wallet: Wallet) : WalletAction()
sealed class AppCurrencyAction : WalletAction() {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
}

View file

@ -1,7 +1,13 @@
package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.common.extensions.isZero
@ -25,8 +31,8 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
data class WalletState(

View file

@ -0,0 +1,78 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.persistence.FiatCurrenciesPrefStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
class AppCurrencyMiddleware(
private val tangemTechService: TangemTechService,
private val tapWalletManager: TapWalletManager,
private val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage,
private val appCurrencyProvider: () -> FiatCurrency,
) {
fun handle(action: WalletAction.AppCurrencyAction) {
when (action) {
is WalletAction.AppCurrencyAction.ChooseAppCurrency -> showSelector()
is WalletAction.AppCurrencyAction.SelectAppCurrency -> selectCurrency(action)
}
}
private fun showSelector() {
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatchDialogShow(
AppDialog.CurrencySelectionDialog(
currenciesList = storedFiatCurrencies.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke()
)
)
}
scope.launch {
when (val result = tangemTechService.currencies()) {
is Result.Success -> {
val currenciesList = result.data.currencies
if (currenciesList.isNotEmpty() &&
currenciesList.toSet() != storedFiatCurrencies.toSet()
) {
fiatCurrenciesPrefStorage.save(currenciesList)
store.dispatchDialogShow(
AppDialog.CurrencySelectionDialog(
currenciesList = storedFiatCurrencies.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke()
)
)
}
}
is Result.Failure -> {}
}
}
}
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
tapWalletManager.rates.clear()
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletAction.LoadFiatRate())
}
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {
return this.map {
FiatCurrency(
code = it.code,
name = it.name,
symbol = it.unit
)
}
}
}

View file

@ -15,7 +15,13 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -29,11 +35,17 @@ 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.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import java.math.BigDecimal
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
@ -41,12 +53,19 @@ import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
private val appCurrencyMiddleware by lazy(mode = LazyThreadSafetyMode.NONE) {
AppCurrencyMiddleware(
tangemTechService = store.state.domainNetworks.tangemTechService,
tapWalletManager = store.state.globalState.tapWalletManager,
fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
@ -71,6 +90,7 @@ class WalletMiddleware {
walletState,
globalState
)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
@ -121,11 +141,15 @@ class WalletMiddleware {
)
when (ratesResult) {
is Result.Success -> {
ratesResult.data.loadedRates.forEach {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it.toPair()))
ratesResult.data.loadedRates.let {
dispatchOnMain(WalletAction.LoadFiatRate.Success(it))
}
ratesResult.data.failedRates.forEach { (currency, throwable) ->
Timber.e(throwable, "Loading rates failed for [%s]", currency.currencySymbol)
Timber.e(
throwable,
"Loading rates failed for [%s]",
currency.currencySymbol
)
}
}
is Result.Failure -> {

View file

@ -0,0 +1,16 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
class AppCurrencyReducer {
fun reduce(
action: WalletAction.AppCurrencyAction,
state: WalletState,
): WalletState {
return when (action) {
is WalletAction.AppCurrencyAction.SelectAppCurrency,
is WalletAction.AppCurrencyAction.ChooseAppCurrency -> state
}
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.*
@ -17,7 +16,6 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.BigDecimal
import java.math.RoundingMode
class OnWalletLoadedReducer {
@ -65,7 +63,7 @@ class OnWalletLoadedReducer {
amount = coinAmountValue,
amountFormatted = formattedAmount,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.code)
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(coinSendButton),
@ -109,12 +107,6 @@ class OnWalletLoadedReducer {
val newWallets = tokens + newWalletData
val wallets = walletState.replaceSomeWallets((newWallets))
val totalBalance = TotalBalance(
state = wallets.findTotalBalanceState(),
fiatAmount = wallets.calculateTotalFiatAmount(),
fiatCurrency = fiatCurrency,
)
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
@ -122,7 +114,9 @@ class OnWalletLoadedReducer {
}
return walletState
.updateWalletsData(wallets)
.updateTotalBalance(totalBalance)
.updateTotalBalance(
totalBalance = obtainTotalBalance(wallets, fiatCurrency)
)
.copy(
state = state,
error = null
@ -192,48 +186,4 @@ class OnWalletLoadedReducer {
state = ProgressState.Done, error = null
)
}
private fun List<WalletData>.findTotalBalanceState(): TotalBalance.State {
return this.mapToTotalBalanceState()
.fold(initial = TotalBalance.State.Loading) { accState, newState ->
accState or newState
}
}
private fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
private fun List<WalletData>.mapToTotalBalanceState(): List<TotalBalance.State> {
return this.map {
when (it.currencyData.status) {
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress -> TotalBalance.State.Success
BalanceStatus.Unreachable,
BalanceStatus.NoAccount,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain -> TotalBalance.State.SomeTokensFailed
BalanceStatus.Loading,
null -> TotalBalance.State.Loading
}
}
}
infix fun TotalBalance.State.or(newState: TotalBalance.State): TotalBalance.State {
return when (this) {
TotalBalance.State.Loading -> when (newState) {
TotalBalance.State.Loading -> this
TotalBalance.State.SomeTokensFailed,
TotalBalance.State.Success -> newState
}
TotalBalance.State.Success,
TotalBalance.State.SomeTokensFailed -> when (newState) {
TotalBalance.State.Loading,
TotalBalance.State.SomeTokensFailed -> newState
TotalBalance.State.Success -> this
}
}
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import java.math.BigDecimal
fun obtainTotalBalance(
wallets: List<WalletData>,
appCurrency: FiatCurrency
): TotalBalance {
return TotalBalance(
state = wallets.findTotalBalanceState(),
fiatAmount = wallets.calculateTotalFiatAmount(),
fiatCurrency = appCurrency
)
}
private fun List<WalletData>.findTotalBalanceState(): TotalBalance.State {
return this.mapToTotalBalanceState()
.fold(initial = TotalBalance.State.Loading) { accState, newState ->
accState or newState
}
}
private fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
return this.map { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
private fun List<WalletData>.mapToTotalBalanceState(): List<TotalBalance.State> {
return this.map {
when (it.currencyData.status) {
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress -> TotalBalance.State.Success
BalanceStatus.Unreachable,
BalanceStatus.NoAccount,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain -> TotalBalance.State.SomeTokensFailed
BalanceStatus.Loading,
null -> TotalBalance.State.Loading
}
}
}
infix fun TotalBalance.State.or(newState: TotalBalance.State): TotalBalance.State {
return when (this) {
TotalBalance.State.Loading -> when (newState) {
TotalBalance.State.Loading -> this
TotalBalance.State.SomeTokensFailed,
TotalBalance.State.Success -> newState
}
TotalBalance.State.Success,
TotalBalance.State.SomeTokensFailed -> when (newState) {
TotalBalance.State.Loading,
TotalBalance.State.SomeTokensFailed -> newState
TotalBalance.State.Success -> this
}
}
}

View file

@ -3,25 +3,38 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.BlockchainNetwork
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.TradeCryptoState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletDialog
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletRent
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
import java.math.RoundingMode
import org.rekotlin.Action
class WalletReducer {
companion object {
@ -33,6 +46,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val multiWalletReducer = MultiWalletReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
val appCurrencyReducer = AppCurrencyReducer()
if (action !is WalletAction) return state.walletState
@ -261,7 +275,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
is WalletAction.LoadFiatRate.Success ->
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
newState = setNewFiatRate(action.fiatRates, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (state.twinCardsState.cardNumber) {
@ -315,6 +329,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
val walletsData = walletStore.walletsData.map { it.copy(walletRent = null) }
newState = newState.updateWalletsData(walletsData)
}
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
else -> { /* no-op */
}
}
@ -370,52 +387,71 @@ private fun handleCheckSignedHashesActions(
private fun setNewFiatRate(
fiatRate: Pair<Currency, BigDecimal?>,
fiatRates: Map<Currency, BigDecimal?>,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val rate = fiatRate.second ?: return state
val rateFormatted = rate.toFormattedCurrencyString(
decimals = 2,
currency = appCurrency.code,
roundingMode = RoundingMode.HALF_UP
)
val currency = fiatRate.first
val rateFormatter: (BigDecimal) -> String = { rate: BigDecimal ->
rate.toFiatRateString(
fiatCurrencyName = appCurrency.symbol
)
}
return if (!state.isMultiwalletAllowed) {
setSingleWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
return if (state.isMultiwalletAllowed) {
setMultiWalletFiatRate(
fiatRates = fiatRates.mapNotNullValues { it.value },
rateFormatter = rateFormatter,
appCurrency = appCurrency,
state = state
)
} else {
setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state)
val fiatRate = fiatRates.entries.firstOrNull()
val currency = fiatRate?.key ?: return state
val rate = fiatRate.value ?: return state
setSingleWalletFiatRate(
rate = rate,
rateFormatted = rateFormatter(rate),
currency = currency,
appCurrency = appCurrency,
state = state
)
}
}
private fun setMultiWalletFiatRate(
rate: BigDecimal,
rateFormatted: String,
currency: Currency,
fiatRates: Map<Currency, BigDecimal>,
rateFormatter: (BigDecimal) -> String,
appCurrency: FiatCurrency,
state: WalletState
): WalletState {
val newWalletsData = fiatRates.mapNotNull { (currency, rate) ->
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
val walletStore = state.getWalletStore(currency) ?: return state
val wallet = walletStore.walletManager?.wallet
val walletData = state.getWalletData(currency) ?: return state
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
val fiatAmount = when (currency) {
is Currency.Blockchain ->
wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatValue(rate)
is Currency.Token ->
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol)
state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate,
fiatRateString = rateFormatter(rate)
)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.code)
val newWalletData = state.getWalletData(currency)?.copy(
currencyData = walletData.currencyData.copy(
fiatAmountFormatted = fiatAmountFormatted,
fiatAmount = fiatAmount
),
fiatRate = rate, fiatRateString = rateFormatted
)
return state.updateWalletData(newWalletData)
return state
.updateWalletsData(newWalletsData)
.updateTotalBalance(
totalBalance = obtainTotalBalance(newWalletsData, appCurrency)
)
}
private fun setSingleWalletFiatRate(

View file

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

View file

@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.Currency
@ -63,9 +62,7 @@ class WalletAdapter
fun bind(wallet: WalletData) = with(binding) {
tvCurrency.text = wallet.currencyData.currency
tvAmount.text = wallet.currencyData.amountFormatted?.takeWhile { !it.isWhitespace() }
tvCurrencySymbol.text =
wallet.currencyData.amountFormatted?.takeLastWhile { !it.isWhitespace() }
tvAmount.text = wallet.currencyData.amountFormatted.orEmpty()
tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted
tvExchangeRate.text = wallet.fiatRateString
cardWallet.setOnClickListener {
@ -74,8 +71,8 @@ class WalletAdapter
val blockchain = wallet.currency.blockchain
val token = (wallet.currency as? Currency.Token)?.token
val isCustom =
wallet.currency.isCustomCurrency(store.state.globalState.scanResponse?.card?.derivationStyle)
val isCustom = wallet.currency
.isCustomCurrency(store.state.globalState.scanResponse?.card?.derivationStyle)
tvExchangeRate.show(!isCustom)
tvCustomCurrency.show(isCustom)
@ -86,43 +83,35 @@ class WalletAdapter
)
when (wallet.currencyData.status) {
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> hideWarning(isCustom)
BalanceStatus.Loading -> {
hideWarning(isCustom)
if (wallet.currencyData.amountFormatted == null) {
tvExchangeRate.text = root.getString(R.string.wallet_balance_loading)
}
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress -> hideMessage()
BalanceStatus.Loading -> if (wallet.currencyData.amountFormatted == null) {
showMessage(root.getString(R.string.wallet_balance_loading))
}
BalanceStatus.TransactionInProgress ->
showWarning(root.getString(R.string.wallet_balance_tx_in_progress), isCustom)
showMessage(root.getString(R.string.wallet_balance_tx_in_progress))
BalanceStatus.Unreachable ->
showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable), isCustom)
showMessage(root.getString(R.string.wallet_balance_blockchain_unreachable))
BalanceStatus.NoAccount ->
showWarning(root.getString(R.string.wallet_error_no_account), isCustom)
showMessage(root.getString(R.string.wallet_error_no_account))
else -> {
}
}
}
private fun showWarning(message: String, isCustom: Boolean = false) {
toggleWarning(true, isCustom)
binding.tvStatusErrorMessage.text = message
private fun showMessage(message: String) {
toggleMessage(true)
binding.tvStatus.text = message
}
private fun hideWarning(isCustom: Boolean = false) {
toggleWarning(false, isCustom)
private fun hideMessage() {
toggleMessage(false)
}
private fun toggleWarning(show: Boolean, isCustom: Boolean = false) {
if (!show) {
binding.tvExchangeRate.show(!isCustom)
binding.tvCustomCurrency.show(isCustom)
} else {
binding.tvExchangeRate.hide()
binding.tvCustomCurrency.hide()
}
binding.tvStatusErrorMessage.show(show)
private fun toggleMessage(show: Boolean) {
binding.tvAmount.show(!show)
binding.tvStatus.show(show)
}
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
object CurrencySelectionDialog {
fun create(
dialog: AppDialog.CurrencySelectionDialog,
context: Context
): AlertDialog {
val currenciesToShow = dialog.currenciesList
.map { it.displayName }
.toTypedArray()
var currentSelection = dialog.currenciesList
.indexOfFirst { it.code == dialog.currentAppCurrency.code }
return AlertDialog.Builder(context)
.setTitle(context.getString(R.string.details_row_title_currency))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> /* no-op */ }
.setPositiveButton(context.getString(R.string.common_done)) { _, _ ->
val selectedCurrency = dialog.currenciesList[currentSelection]
store.dispatch(
WalletAction.AppCurrencyAction.SelectAppCurrency(
fiatCurrency = selectedCurrency
)
)
}
.setOnDismissListener {
store.dispatchDialogHide()
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
currentSelection = which
}
.create()
}
}

View file

@ -6,7 +6,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.common.card.Card
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -134,23 +137,16 @@ class MultiWalletView : WalletView {
binding: FragmentWalletBinding,
totalBalance: TotalBalance,
) = with(binding.lCardTotalBalance) {
when (totalBalance.state) {
TotalBalance.State.Loading -> {
pbLoading.showAnimated()
tvBalance.hideAnimated(hiddenVisibility = View.INVISIBLE)
tvProcessing.hideAnimated()
}
TotalBalance.State.SomeTokensFailed -> {
tvBalance.showAnimated()
pbLoading.hideAnimated()
tvProcessing.showAnimated()
}
TotalBalance.State.Success -> {
tvBalance.showAnimated()
pbLoading.hideAnimated()
tvProcessing.hideAnimated()
}
}
tvBalance.animateVisibility(
show = totalBalance.state != TotalBalance.State.Loading,
hiddenVisibility = View.INVISIBLE
)
pbLoading.animateVisibility(
show = totalBalance.state == TotalBalance.State.Loading
)
tvProcessing.animateVisibility(
show = totalBalance.state == TotalBalance.State.SomeTokensFailed
)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol
@ -158,7 +154,7 @@ class MultiWalletView : WalletView {
tvCurrencyName.text = totalBalance.fiatCurrency.code
tvCurrencyName.setOnClickListener {
// TODO: Open app currency selector
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
}

View file

@ -216,47 +216,20 @@
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_app_currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title"
tools:text="USD" />
<TextView
android:id="@+id/tv_app_currency_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_currency"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_disclaimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/disclaimer_title"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/disclaimer_title"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_card_tou"
@ -306,4 +279,4 @@
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,205 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@android:color/white"
android:elevation="3dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="76dp">
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@android:color/white"
android:elevation="3dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_currency"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/shape_circle" />
<TextView
android:id="@+id/tv_token_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:textColor="@android:color/white"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency"
app:layout_constraintStart_toStartOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/iv_currency"
tools:text="J" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_currency"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/shape_circle" />
<TextView
android:id="@+id/tv_currency"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="2dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@+id/tv_amount"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="parent"
tools:text="Binance Smart Chain Optimal" />
android:id="@+id/tv_token_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:textColor="@android:color/white"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/iv_currency"
app:layout_constraintEnd_toEndOf="@id/iv_currency"
app:layout_constraintStart_toStartOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/iv_currency"
tools:text="J" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.45"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/guideline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tv_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:gravity="end"
android:maxEms="12"
android:maxLength="12"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@+id/tv_currency_symbol"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
tools:text="1234567890.1234567890" />
android:id="@+id/tv_currency"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:textStyle="bold"
app:lineHeight="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintEnd_toStartOf="@+id/tv_amount"
tools:text="Binance Smart Chain Optimal" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.5"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_exchange_rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"
tools:text="USD 3 588" />
android:id="@+id/tv_amount_fiat"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxEms="12"
android:maxLength="12"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:textStyle="bold"
android:textAlignment="viewEnd"
app:lineHeight="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintStart_toEndOf="@+id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
tools:text="12300.43 $" />
<TextView
android:id="@+id/tv_custom_currency"
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"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"
android:text="@string/common_custom" />
android:id="@+id/tv_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxEms="12"
android:maxLength="12"
android:maxLines="1"
android:textColor="@color/darkGray2"
android:textSize="12sp"
android:textAlignment="viewEnd"
app:lineHeight="20dp"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
tools:text="2.002134 BTC" />
<TextView
android:id="@+id/tv_status_error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/warning"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_amount_fiat"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"
tools:text="@string/lorem_ipsum" />
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/darkGray2"
android:textSize="12sp"
android:textAlignment="viewEnd"
app:lineHeight="20dp"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline2"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Unreachable..." />
<TextView
android:id="@+id/tv_status_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:drawablePadding="5dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/wallet_balance_loading"
android:textColor="@color/darkGray6"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
android:id="@+id/tv_exchange_rate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:maxLines="1"
android:textColor="@color/darkGray2"
android:textSize="12sp"
app:lineHeight="20dp"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintEnd_toStartOf="@id/guideline2"
tools:text="46 908 $" />
<TextView
android:id="@+id/tv_currency_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="end"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintTop_toTopOf="parent"
tools:text="BTC" />
<TextView
android:id="@+id/tv_amount_fiat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingEnd="16dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline"
tools:text="0.43 USD" />
android:id="@+id/tv_custom_currency"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/darkGray2"
android:textSize="14sp"
android:text="@string/common_custom"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintEnd_toStartOf="@id/guideline2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>

View file

@ -11,6 +11,7 @@
<item name="android:windowContentOverlay">@null</item>
<item name="actionMenuTextColor">@color/menu_accent_color</item>
<item name="materialCardViewStyle">@style/Widget.AppTheme.CardView</item>
</style>
<style name="AppTheme" parent="BaseAppTheme" />
@ -156,6 +157,10 @@
<item name="android:textColor">@color/selector_chip_shop_text</item>
</style>
<style name="Widget.AppTheme.CardView" parent="Widget.MaterialComponents.CardView">
<item name="cardCornerRadius">8dp</item>
</style>
</resources>
<!-- android:fontFamily="sans-serif" // roboto regular -->
@ -163,4 +168,4 @@
<!-- android:fontFamily="sans-serif-condensed" // roboto condensed -->
<!-- android:fontFamily="sans-serif-black" // roboto black -->
<!-- android:fontFamily="sans-serif-thin" // roboto thin (android 4.2) -->
<!-- android:fontFamily="sans-serif-medium" // roboto medium (android 5.0) -->
<!-- android:fontFamily="sans-serif-medium" // roboto medium (android 5.0) -->