Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-27 20:29:04 +03:00
commit 0905bb1ad9
35 changed files with 474 additions and 303 deletions

View file

@ -17,10 +17,11 @@ 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
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.InterruptOnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.TryToInterruptRegistrationDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
@ -72,8 +73,8 @@ 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 TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
@ -109,10 +110,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ClipboardOrScanQr ->
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
PersonalSignDialog.create(state.dialog.data, context)
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context)
is WalletConnectDialog.BnbTransactionDialog ->
BnbTransactionDialog.create(
data = state.dialog.data,
@ -132,7 +131,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.TryToInterrupt -> TryToInterruptRegistrationDialog.create(context, state.dialog)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context)

View file

@ -68,7 +68,7 @@ fun BigDecimal.toFormattedFiatValue(
): String {
val fiatValue = this.setScale(2, RoundingMode.HALF_UP)
.let { if (formatWithSpaces) it.formatWithSpaces() else it }
return "${fiatValue} $fiatCurrencyName"
return "$fiatValue$fiatCurrencyName"
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()

View file

@ -101,6 +101,7 @@ class ConfigManager {
),
infuraProjectId = values.infuraProjectId,
tronGridApiKey = values.tronGridApiKey,
saltPayAuthToken = values.saltPay.credentials.token,
),
appsFlyerDevKey = values.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = values.amplitudeApiKey,
@ -129,6 +130,7 @@ class ConfigManager {
subdomain = values.bscQuiknodeSubdomain,
),
infuraProjectId = values.infuraProjectId,
saltPayAuthToken = values.saltPay.credentials.token,
),
appsFlyerDevKey = values.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = values.amplitudeApiKey,

View file

@ -14,6 +14,7 @@ import java.math.BigDecimal
* @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
* founds will be destroyed. Null if currency don't have existential deposit
* @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
* @param isCardSingleToken shows that [Currency] is a card token
* */
data class WalletDataModel(
val currency: Currency,
@ -21,6 +22,7 @@ data class WalletDataModel(
val walletAddresses: List<AddressData>,
val existentialDeposit: BigDecimal?,
val fiatRate: BigDecimal?,
val isCardSingleToken: Boolean,
) {
/**

View file

@ -6,10 +6,12 @@ import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import java.math.BigDecimal
/**
* Contains info about the blockchain and its currencies
*
* @param userWalletId ID of the associated [UserWallet]
* @param blockchain [Blockchain] of this WalletStore
* @param derivationPath [DerivationPath] of this store, null if the card does not support the
@ -21,6 +23,9 @@ import java.math.BigDecimal
* TODO: Remove after WalletMiddleware refactoring
* @param walletManager [WalletManager], may be null if it fails to create this manager.
* TODO: Remove after WalletMiddleware refactoring
*
* @property blockchainWalletData Returns the [WalletDataModel] of the blockchain of this wallet store
* or throw [NoSuchElementException] if this wallet store not contains [WalletDataModel] of the blockchain
* */
data class WalletStoreModel(
val userWalletId: UserWalletId,
@ -34,6 +39,9 @@ data class WalletStoreModel(
val walletManager: WalletManager?,
) {
val blockchainWalletData: WalletDataModel
get() = walletsData.first { it.currency is Currency.Blockchain }
/**
* Represents wallet blockchain rent
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than

View file

@ -73,13 +73,13 @@ private class WalletMangerWalletStoreBuilderImpl(
override fun build(): WalletStoreModel {
val wallet = walletManager.wallet
val blockchainWalletData = wallet.blockchain.toBlockchainWalletData(walletManager)
val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager)
val tokenWalletsData = walletManager.cardTokens.map { it.toTokenWalletData(walletManager) }
return WalletStoreModel(
userWalletId = userWalletId,
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath,
walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)),
walletsData = (listOf(blockchainWalletData) + tokenWalletsData),
walletRent = null,
walletManager = walletManager,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager),
@ -97,6 +97,7 @@ private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManag
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
)
}
@ -113,6 +114,7 @@ private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager?.cardTokens?.contains(token) ?: false,
)
}
}
@ -128,6 +130,7 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
)
}
@ -143,6 +146,7 @@ private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataMod
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager.cardTokens.contains(this),
)
}

View file

@ -70,7 +70,6 @@ object ScanCardProcessor {
result
.doOnFailure { error ->
onProgressStateChange(false)
onScanStateChange(false)
onFailure(error)
}
@ -87,7 +86,6 @@ object ScanCardProcessor {
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse1,
onProgressStateChange = onProgressStateChange,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse2 ->
@ -144,7 +142,6 @@ object ScanCardProcessor {
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline nextHandler: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
@ -167,7 +164,6 @@ object ScanCardProcessor {
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onProgressStateChange(false)
onFailure(TangemSdkError.UserCancelled())
}
},
@ -208,14 +204,12 @@ object ScanCardProcessor {
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
is Result.Failure -> {
SaltPayExceptionHandler.handle(result.error)
delay(DELAY_SDK_DIALOG_CLOSE)
onFailure(TangemSdkError.ExceptionError(result.error))
onProgressStateChange(false)
}
}
}
@ -223,10 +217,10 @@ object ScanCardProcessor {
delay(DELAY_SDK_DIALOG_CLOSE)
if (scanResponse.card.backupStatus?.isActive == false) {
showSaltPayTapVisaLogoCardDialog()
onProgressStateChange(false)
} else {
onSuccess(scanResponse)
}
onProgressStateChange(false)
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
@ -242,7 +236,6 @@ object ScanCardProcessor {
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
}

View file

@ -43,7 +43,8 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
private fun Sequence<WalletDataModel>.mapToStatus(): Sequence<TotalFiatBalanceStatus> {
return this.map { walletData ->
when (walletData.status) {
if (walletData.fiatRate == null) TotalFiatBalanceStatus.Error
else when (walletData.status) {
is WalletDataModel.VerifiedOnline,
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,

View file

@ -27,6 +27,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStores
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithDemoAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
@ -34,6 +35,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.filterByCoin
import com.tangem.tap.features.wallet.models.getPendingTransactions
@ -207,6 +209,8 @@ internal class DefaultWalletAmountsRepository(
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// fixme move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}
.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }
@ -296,6 +300,7 @@ internal class DefaultWalletAmountsRepository(
private suspend fun updateWalletStoreWithAmounts(
walletStore: WalletStoreModel,
updatedWallet: Wallet,
isDemo: Boolean,
) = withContext(Dispatchers.Default) {
Timber.d(
"""
@ -309,7 +314,11 @@ internal class DefaultWalletAmountsRepository(
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
it.updateWithAmounts(wallet = updatedWallet)
if (isDemo) {
it.updateWithDemoAmounts(wallet = updatedWallet)
} else {
it.updateWithAmounts(wallet = updatedWallet)
}
},
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.common.core.TangemError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.getPendingTransactions
import java.math.BigDecimal
@ -74,12 +75,25 @@ internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
)
}
internal fun WalletDataModel.updateWithDemoAmount(wallet: Wallet): WalletDataModel {
val amount = DemoHelper.config.getBalance(wallet.blockchain)
return this.copy(
status = WalletDataModel.VerifiedOnline(amount = amount.value ?: BigDecimal.ZERO),
)
}
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithAmount(wallet)
}
}
internal fun List<WalletDataModel>.updateWithDemoAmounts(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithDemoAmount(wallet)
}
}
internal fun WalletDataModel.updateWithError(
wallet: Wallet,
error: TangemError,
@ -164,7 +178,7 @@ internal fun List<WalletDataModel>.updateWithSelf(
val updatedWalletsData = arrayListOf<WalletDataModel>()
newWalletsData.forEach { newWalletData ->
val walletDataToUpdate = oldWalletsData.find(newWalletData::isSameWalletData)
val walletDataToUpdate = oldWalletsData.firstOrNull(newWalletData::isSameWalletData)
if (walletDataToUpdate != null) {
updatedWalletsData.add(walletDataToUpdate.updateWithSelf(newWalletData))
} else {
@ -176,5 +190,5 @@ internal fun List<WalletDataModel>.updateWithSelf(
}
internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean {
return currency == other.currency
return this.currency == other.currency
}

View file

@ -49,6 +49,14 @@ internal fun WalletStoreModel.updateWithAmounts(
)
}
internal fun WalletStoreModel.updateWithDemoAmounts(
wallet: Wallet,
): WalletStoreModel {
return this.copy(
walletsData = walletsData.updateWithDemoAmounts(wallet = wallet),
)
}
internal fun WalletStoreModel.updateWithFiatRates(
rates: Map<String, Double>,
): WalletStoreModel {
@ -96,6 +104,9 @@ private inline fun List<WalletStoreModel>.replaceWalletStores(
walletStoresToUpdate.forEach { walletStoreToUpdate ->
val index = mutableStores.indexOfFirst(walletStoreToUpdate::isSameWalletStore)
// Can be possible if user hides wallet store when it's tokens is loading
if (index == -1) return@forEach
val currentWalletStore = mutableStores[index]
val updatedWalletStore = update(currentWalletStore)
@ -116,6 +127,7 @@ private inline fun List<WalletStoreModel>.replaceWalletStores(
}
internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean {
return this.blockchain == other.blockchain &&
return this.userWalletId == other.userWalletId &&
this.blockchain == other.blockchain &&
this.derivationPath == other.derivationPath
}

View file

@ -95,6 +95,7 @@ fun ProgressButton(
Button(
modifier = modifier,
onClick = onClick,
enabled = !inProgress,
colors = ButtonDefaults.textButtonColors(
backgroundColor = backgroundColor,
contentColor = contentColor,

View file

@ -79,10 +79,9 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
} else {
changeButtonState(ButtonState.ENABLED)
}
// else { //todo hide this because
// changeButtonState(ButtonState.ENABLED)
// }
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
@ -103,19 +102,23 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
navigateTo(AppScreen.Wallet)
}
} else {
store.onCardScanned(scanResponse)
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
navigateTo(AppScreen.Wallet)
}
}
},
)
}
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(200)
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
}

View file

@ -22,7 +22,6 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.wallet.redux.Artwork
@ -188,12 +187,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> {
when {
onboardingWalletState.isSaltPay -> handleOnBackPressedSaltPay(onboardingWalletState)
else -> handleOnBackPressed(onboardingWalletState)
}
}
OnboardingWalletAction.OnBackPressed -> handleOnBackPressed(onboardingWalletState)
}
}
@ -408,30 +402,33 @@ private fun initSaltPayOnBackupFinishedIfNeeded(
}
}
private fun handleOnBackPressedSaltPay(state: OnboardingWalletState) {
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
BackupStep.Finished -> {
store.dispatchDialogShow(
SaltPayDialog.Activation.TryToInterrupt(
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
onCancel = { /* do nothing */ },
),
)
BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
-> {
showInterruptOnboardingDialog()
}
is BackupStep.WriteBackupCard -> {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
BackupStep.Finished -> {
if (state.isSaltPay) {
showInterruptOnboardingDialog()
} else {
store.dispatch(NavigationAction.PopBackTo())
}
}
else -> handleOnBackPressed(state)
}
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
-> {
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(NavigationAction.PopBackTo())
}
is BackupStep.WriteBackupCard ->
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
private fun showInterruptOnboardingDialog() {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(NavigationAction.PopBackTo())
},
),
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import com.tangem.common.CardFilter
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
@ -104,9 +105,13 @@ sealed class BackupStep {
object Finished : BackupStep()
}
sealed class OnboardingDialog : StateDialog {
data class InterruptOnboarding(val onOk: VoidCallback) : BackupDialog()
}
sealed class BackupDialog : StateDialog {
object AddMoreBackupCards : StateDialog
object BackupInProgress : StateDialog
object UnfinishedBackupFound : StateDialog
object ConfirmDiscardingBackup : StateDialog
object AddMoreBackupCards : BackupDialog()
object BackupInProgress : BackupDialog()
object UnfinishedBackupFound : BackupDialog()
object ConfirmDiscardingBackup : BackupDialog()
}

View file

@ -38,11 +38,11 @@ class GnosisRegistrator(
}
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2"
Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
private val addressTreasureSafe = "0x8e9260a049d3Aa9ac60D0d4F27017320E0e2396B"
private val addressTreasureSafe = "0x24A3c2382497075b6D93258f5938f7B661c06318"
private val atomicNonce = AtomicLong()

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.tap.common.zendesk.ZendeskConfig
import org.spongycastle.util.encoders.Base64.toBase64String
/**
[REDACTED_AUTHOR]
@ -8,12 +9,14 @@ import com.tangem.tap.common.zendesk.ZendeskConfig
data class SaltPayConfig(
val zendesk: ZendeskConfig,
val kycProvider: KYCProvider,
val credentials: Credentials,
) {
companion object {
fun stub(): SaltPayConfig {
return SaltPayConfig(
zendesk = ZendeskConfig("", "", "", "", ""),
kycProvider = KYCProvider("", "", "", ""),
credentials = Credentials("", ""),
)
}
}
@ -24,4 +27,11 @@ data class KYCProvider(
val externalIdParameterKey: String,
val sidParameterKey: String,
val sidValue: String,
)
)
data class Credentials(
val user: String,
val password: String,
) {
val token: String by lazy { "Basic ${toBase64String("$user:$password".toByteArray())}" }
}

View file

@ -4,20 +4,21 @@ import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class TryToInterruptRegistrationDialog {
class InterruptOnboardingDialog {
companion object {
fun create(context: Context, dialog: SaltPayDialog.Activation.TryToInterrupt): Dialog {
fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.onboarding_exit_alert_title))
setMessage(context.getString(R.string.onboarding_exit_alert_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
setNegativeButton(R.string.common_cancel) { _, _ -> dialog.onCancel() }
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
@ -11,6 +10,5 @@ sealed class SaltPayDialog : StateDialog {
sealed class Activation : SaltPayDialog() {
object NoGas : SaltPayDialog()
data class OnError(val error: SaltPayActivationError) : SaltPayDialog()
data class TryToInterrupt(val onOk: VoidCallback, val onCancel: VoidCallback) : SaltPayDialog()
}
}

View file

@ -6,11 +6,11 @@ import com.tangem.blockchain.extensions.successOr
import com.tangem.common.Filter
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.api.paymentology.KYCStatus
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -286,7 +286,7 @@ suspend fun SaltPayActivationManager.update(
return Result.Failure(ex)
}
// checkGasIfNeeded(this, newStep).successOr { return it }
checkGasIfNeeded(this, newStep).successOr { return it }
Timber.d("update: success: %s", newStep)
return Result.Success(newStep)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.toFiatRateString
@ -10,7 +10,6 @@ import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.ProgressState
@ -24,11 +23,9 @@ import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.BigDecimal
internal fun List<WalletStoreModel>.mapToReduxModels(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
internal fun List<WalletStoreModel>.mapToReduxModels(): List<WalletStore> {
return this.map { walletStoreModel ->
walletStoreModel.mapToReduxModel(isMultiWalletAllowed)
walletStoreModel.mapToReduxModel()
}
}
@ -44,100 +41,150 @@ internal fun TotalFiatBalance.mapToReduxModel(): TotalBalance {
)
}
internal fun WalletStoreModel.mapToReduxModel(
isMultiWalletAllowed: Boolean,
): WalletStore {
internal fun WalletStoreModel.mapToReduxModel(): WalletStore {
val appCurrencySymbol = store.state.globalState.appCurrency.symbol
return WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletRent),
walletsData = walletsData.mapToReduxModels(walletRent, appCurrencySymbol),
)
.updateTokenModels(blockchainWalletData.status.amount)
.setupIfHadCardSingleToken(
blockchain = blockchain,
walletsDataModel = walletsData,
appCurrencySymbol = appCurrencySymbol,
blockchainWalletData = blockchainWalletData.mapToReduxModel(
walletRent = walletRent,
appCurrencySymbol = appCurrencySymbol,
),
)
}
private fun List<WalletDataModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
private fun List<WalletDataModel>.mapToReduxModels(
walletRent: WalletStoreModel.WalletRent?,
appCurrencySymbol: String,
): List<WalletData> {
return this.map { walletDataModel ->
with(walletDataModel) {
val amount = status.amount
val amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
val appCurrency = store.state.globalState.appCurrency
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrency.symbol)
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol)
val blockchainAmountValue = getBlockchainAmount()
WalletData(
currency = currency,
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
WalletAddresses(
selectedAddress = selectedAddress,
list = walletAddresses,
)
},
existentialDepositString = existentialDeposit?.toPlainString(),
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
pendingTransactions = status.pendingTransactions,
mainButton = WalletMainButton.SendButton(
enabled = !blockchainAmountValue.isZero()
&& !status.amount.isZero()
&& status.pendingTransactions.isEmpty(),
),
walletRent = walletRent?.let {
WalletRent(
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}",
)
},
currencyData = BalanceWidgetData(
status = when (status) {
is WalletDataModel.Loading -> BalanceStatus.Loading
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable -> BalanceStatus.Unreachable
is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
},
currency = currency.currencyName,
currencySymbol = currency.currencySymbol,
blockchainAmount = blockchainAmountValue,
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
token = when {
!isMultiWalletAllowed && currency is Currency.Token -> {
TokenData(
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
tokenSymbol = currency.currencySymbol,
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
)
}
else -> null
},
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
?.amountToCreateAccount
?.toString(),
errorMessage = status.errorMessage,
),
)
}
walletDataModel.mapToReduxModel(walletRent, appCurrencySymbol)
}
}
private fun WalletDataModel.getBlockchainAmount(): BigDecimal {
val walletStore = store.state.walletState.getWalletStore(currency) ?: return BigDecimal.ZERO
return walletStore.walletManager?.wallet?.amounts?.get(AmountType.Coin)?.value ?: BigDecimal.ZERO
private fun WalletDataModel.mapToReduxModel(
walletRent: WalletStoreModel.WalletRent?,
appCurrencySymbol: String,
): WalletData {
val amount = status.amount
val amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrencySymbol)
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrencySymbol)
return WalletData(
currency = currency,
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
WalletAddresses(
selectedAddress = selectedAddress,
list = walletAddresses,
)
},
existentialDepositString = existentialDeposit?.toPlainString(),
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
pendingTransactions = status.pendingTransactions,
mainButton = WalletMainButton.SendButton(
enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(),
),
walletRent = walletRent?.let {
WalletRent(
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}",
)
},
currencyData = BalanceWidgetData(
status = when (status) {
is WalletDataModel.Loading -> BalanceStatus.Loading
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable -> BalanceStatus.Unreachable
is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
},
currency = currency.currencyName,
currencySymbol = currency.currencySymbol,
blockchainAmount = BigDecimal.ZERO,
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
token = null,
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
?.amountToCreateAccount
?.toString(),
errorMessage = status.errorMessage,
),
)
}
private fun WalletStore.updateTokenModels(blockchainAmount: BigDecimal): WalletStore {
val updatedTokensWalletData = walletsData.filter { it.currency.isToken() }.map {
it.copy(
mainButton = when (it.mainButton) {
is WalletMainButton.SendButton -> {
WalletMainButton.SendButton(it.mainButton.enabled && !blockchainAmount.isZero())
}
is WalletMainButton.CreateWalletButton -> it.mainButton
},
currencyData = it.currencyData.copy(
blockchainAmount = blockchainAmount,
),
)
}
return updateWallets(updatedTokensWalletData)
}
private fun WalletStore.setupIfHadCardSingleToken(
blockchain: Blockchain,
walletsDataModel: List<WalletDataModel>,
appCurrencySymbol: String,
blockchainWalletData: WalletData,
): WalletStore {
// Card with single token contains only 2 model - blockchain and token
if (walletsData.size != 2) return this
val cardSingleTokenWalletData = walletsDataModel.firstOrNull {
it.currency.isToken() && it.currency.blockchain == blockchain && it.isCardSingleToken
} ?: return this
val blockchainWalletDataWithSingleToken = blockchainWalletData.copy(
currencyData = blockchainWalletData.currencyData.copy(
token = cardSingleTokenWalletData.toTokenData(appCurrencySymbol),
),
)
return updateWallets(listOf(blockchainWalletDataWithSingleToken))
}
private fun WalletDataModel.toTokenData(appCurrencySymbol: String): TokenData {
val amount = status.amount
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
return TokenData(
amount = amount,
amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrencySymbol),
tokenSymbol = currency.currencySymbol,
fiatRate = fiatRate,
fiatRateString = fiatRate?.toFiatRateString(appCurrencySymbol),
)
}

View file

@ -318,7 +318,7 @@ class WalletMiddleware {
}
}
val reduxWalletStores = wallStores.mapToReduxModels(state.isMultiwalletAllowed)
val reduxWalletStores = wallStores.mapToReduxModels()
store.dispatchOnMain(
WalletAction.WalletStoresChanged.UpdateWalletStores(
reduxWalletStores = reduxWalletStores,

View file

@ -16,6 +16,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.ModelWatcher
import com.badoo.mvicore.modelWatcher
import com.tangem.common.doOnResult
import com.tangem.core.analytics.Analytics
@ -39,6 +41,8 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
@ -67,7 +71,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
private val walletDataWatcher = modelWatcher<WalletData> {
private val walletDataWatcher: ModelWatcher<WalletData> = modelWatcher {
val addressCardStrategy: DiffStrategy<WalletData> = { old, new ->
old.currency != new.currency ||
old.walletAddresses?.selectedAddress != new.walletAddresses?.selectedAddress ||
old.shouldShowMultipleAddress() != new.shouldShowMultipleAddress()
}
WalletData::pendingTransactions {
showPendingTransactionsIfPresent(it)
}
@ -77,25 +87,35 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
WalletData::currencyData {
setupBalanceData(it)
}
WalletData::walletAddresses { walletAddresses ->
setupCopyAndShareButtons(walletAddresses?.selectedAddress?.address)
}
WalletData::assembleWarnings { warnings ->
handleWarnings(warnings)
}
(WalletData::currencyData or WalletData::currency) { walletData ->
setupCurrency(walletData.currencyData, walletData.currency)
setupSwipeRefresh(walletData.currencyData, walletData.currency)
}
watch({ it }, addressCardStrategy) { walletData ->
setupAddressCard(
shouldShowMultipleAddress = walletData.shouldShowMultipleAddress(),
selectedAddress = walletData.walletAddresses?.selectedAddress,
currency = walletData.currency,
)
}
}
private val walletStateWatcher = modelWatcher<WalletState> {
(WalletState::selectedCurrency or WalletState::selectedWalletData) { state ->
val selectedWalletData = state.selectedWalletData
if (selectedWalletData != null) {
walletDataWatcher.invoke(selectedWalletData)
setupButtons(selectedWalletData, state.isExchangeServiceFeatureOn)
setupAddressCard(selectedWalletData)
handleWarnings(selectedWalletData)
private val walletStateWatcher: ModelWatcher<WalletState> = modelWatcher {
WalletState::selectedWalletData { selectedWallet ->
if (selectedWallet != null) {
walletDataWatcher.invoke(selectedWallet)
}
}
(WalletState::selectedCurrency or WalletState::isExchangeServiceFeatureOn) { state ->
if (state.selectedWalletData != null) {
setupButtons(state.selectedWalletData!!, state.isExchangeServiceFeatureOn)
(WalletState::selectedWalletData or WalletState::isExchangeServiceFeatureOn) { state ->
val selectedWallet = state.selectedWalletData
if (selectedWallet != null) {
setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn)
}
}
(WalletState::state or WalletState::error) { state ->
@ -132,8 +152,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onStop() {
super.onStop()
store.unsubscribe(this)
walletDataWatcher.clear()
walletStateWatcher.clear()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -147,6 +165,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupTestActionButton()
}
override fun onDestroyView() {
super.onDestroyView()
clearWatchers()
}
private fun setupTransactionsRecyclerView() = with(binding) {
pendingTransactionAdapter = PendingTransactionsAdapter()
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
@ -251,19 +274,21 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
currencyData.status == BalanceStatus.Refreshing
}
private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) {
lWalletDetails.btnCopy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, requireContext()))
private fun setupCopyAndShareButtons(walletAddress: String?) {
binding.lWalletDetails.btnCopy.setOnClickListener {
if (walletAddress != null) {
store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext()))
}
}
lWalletDetails.btnShare.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.ShareAddress(addressString, requireContext()))
binding.lWalletDetails.btnShare.setOnClickListener {
if (walletAddress != null) {
store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext()))
}
}
}
rowButtons.updateButtonsVisibility(
private fun setupButtonsRow(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) {
binding.rowButtons.updateButtonsVisibility(
exchangeServiceFeatureOn = isExchangeServiceFeatureOn,
buyAllowed = selectedWallet.isAvailableToBuy,
sellAllowed = selectedWallet.isAvailableToSell,
@ -271,9 +296,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
)
}
private fun handleWarnings(selectedWallet: WalletData) = with(binding) {
private fun handleWarnings(warnings: List<WalletWarning>) = with(binding) {
val converter = WalletWarningConverter(requireContext())
val warningDetails = selectedWallet.assembleWarnings().map { converter.convert(it) }
val warningDetails = warnings.map { converter.convert(it) }
warningMessagesAdapter.submitList(warningDetails)
rvWarningMessages.show(warningDetails.isNotEmpty())
@ -294,51 +319,59 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty())
}
private fun setupAddressCard(state: WalletData) = with(binding.lWalletDetails) {
if (state.walletAddresses != null) {
if (state.shouldShowMultipleAddress() && state.currency is Currency.Blockchain) {
(cardBalance as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
private fun setupAddressCard(
shouldShowMultipleAddress: Boolean,
selectedAddress: AddressData?,
currency: Currency,
) = with(binding.lWalletDetails) {
if (selectedAddress == null) return@with
val checkedId =
MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
setupAddressTypeChips(shouldShowMultipleAddress, selectedAddress, currency)
chipGroupAddressType.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type =
MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chipGroupAddressType.hide()
}
tvAddress.text = state.walletAddresses.selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(
WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
requireContext(),
),
)
}
ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
tvAddress.text = selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(selectedAddress.exploreUrl, requireContext()))
}
ivQrCode.setImageBitmap(selectedAddress.shareUrl.toQrCode())
tvReceiveMessage.text = when (val currency = state.currency) {
is Currency.Blockchain -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol,
currency.blockchain.fullName,
)
is Currency.Token -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName,
)
tvReceiveMessage.text = when (currency) {
is Currency.Blockchain -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol,
currency.blockchain.fullName,
)
is Currency.Token -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName,
)
}
}
private fun setupAddressTypeChips(
shouldShowMultipleAddress: Boolean,
selectedAddress: AddressData,
currency: Currency,
) = with(binding.lWalletDetails) {
if (shouldShowMultipleAddress && currency is Currency.Blockchain) {
(cardBalance as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
val checkedId =
MultipleAddressUiHelper.typeToId(selectedAddress.type)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
chipGroupAddressType.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type =
MultipleAddressUiHelper.idToType(checkedId, currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chipGroupAddressType.hide()
}
}
@ -425,6 +458,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
inflater.inflate(R.menu.menu_wallet_details, menu)
}
private fun clearWatchers() {
walletDataWatcher.clear()
walletStateWatcher.clear()
}
private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) {
val text = getString(mainMessage).appendIfNotNull(error, "\nError: ")
setStatus(text, R.color.warning, R.drawable.ic_warning_small)

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
@ -72,8 +73,11 @@ class SingleWalletView : WalletView() {
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
binding?.rvPendingTransaction?.show(pendingTransactions.isNotEmpty())
val knownTransactions = pendingTransactions.filterNot {
it.type == PendingTransactionType.Unknown
}
pendingTransactionAdapter.submitList(knownTransactions)
binding?.rvPendingTransaction?.show(knownTransactions.isNotEmpty())
}
private fun setupBalance(state: WalletState, primaryWallet: WalletData) {

View file

@ -24,8 +24,8 @@ internal sealed interface WalletSelectorAction : Action {
val walletsStores: Map<UserWalletId, List<WalletStoreModel>>,
) : WalletSelectorAction
data class BalanceLoaded(
val userWalletModel: UserWalletModel,
data class BalancesLoaded(
val userWalletModels: List<UserWalletModel>,
) : WalletSelectorAction
object UnlockWithBiometry : WalletSelectorAction {

View file

@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
internal class WalletSelectorMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
@ -81,7 +80,7 @@ internal class WalletSelectorMiddleware {
is WalletSelectorAction.SelectedWalletChanged,
is WalletSelectorAction.UnlockWithBiometry.Error,
is WalletSelectorAction.UnlockWithBiometry.Success,
is WalletSelectorAction.BalanceLoaded,
is WalletSelectorAction.BalancesLoaded,
is WalletSelectorAction.IsLockedChanged,
is WalletSelectorAction.HandleError,
is WalletSelectorAction.CloseError,
@ -104,20 +103,10 @@ internal class WalletSelectorMiddleware {
state: WalletSelectorState,
) {
if (updatedWalletStores.isNotEmpty()) scope.launch(Dispatchers.Default) {
state.wallets
.associateWith { updatedWalletStores[it.id] }
.forEach { (wallet, walletStores) ->
val isWalletTokensEmpty = (wallet.type as? UserWalletModel.Type.MultiCurrency)?.tokensCount == 0
val updatedWallet = if (walletStores == null && isWalletTokensEmpty) {
wallet.copy(fiatBalance = TotalFiatBalance.Loaded(BigDecimal.ZERO))
} else {
wallet.updateWalletStoresAndCalculateFiatBalance(walletStores.orEmpty())
}
if (wallet != updatedWallet) {
store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet))
}
}
val updatedWallets = state.wallets.updateWalletStoresAndCalculateFiatBalance(updatedWalletStores)
if (updatedWallets != state.wallets) {
store.dispatchOnMain(WalletSelectorAction.BalancesLoaded(updatedWallets))
}
}
}
@ -314,18 +303,28 @@ internal class WalletSelectorMiddleware {
}
}
private suspend fun List<UserWalletModel>.updateWalletStoresAndCalculateFiatBalance(
walletStores: Map<UserWalletId, List<WalletStoreModel>>,
): List<UserWalletModel> {
return this
.associateWith { walletStores[it.id] }
.map { (wallet, walletStores) ->
wallet.updateWalletStoresAndCalculateFiatBalance(walletStores)
}
}
private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance(
walletStores: List<WalletStoreModel>,
walletStores: List<WalletStoreModel>?,
): UserWalletModel {
return this.copy(
type = when (type) {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores.flatMap { it.walletsData }.size,
tokensCount = walletStores?.flatMap { it.walletsData }?.size ?: 0,
)
is UserWalletModel.Type.SingleCurrency -> type
},
fiatBalance = totalFiatBalanceCalculator.calculate(
walletStores = walletStores,
walletStores = walletStores.orEmpty(),
initial = TotalFiatBalance.Loading,
),
)

View file

@ -24,8 +24,8 @@ internal object WalletSelectorReducer {
is WalletSelectorAction.IsLockedChanged -> state.copy(
isLocked = action.isLocked,
)
is WalletSelectorAction.BalanceLoaded -> state.copy(
wallets = state.wallets.updateWithBalance(action.userWalletModel),
is WalletSelectorAction.BalancesLoaded -> state.copy(
wallets = action.userWalletModels,
)
is WalletSelectorAction.HandleError -> state.copy(error = action.error)
is WalletSelectorAction.CloseError -> state.copy(error = null)
@ -85,19 +85,6 @@ internal object WalletSelectorReducer {
}
}
private fun List<UserWalletModel>.updateWithBalance(
userWalletModel: UserWalletModel,
): List<UserWalletModel> {
return ArrayList(this).apply {
val index = indexOfFirst { it.id == userWalletModel.id }
if (index == -1) {
add(userWalletModel)
} else {
this[index] = userWalletModel
}
}
}
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
return if (isMultiCurrency) {
UserWalletModel.Type.MultiCurrency(

View file

@ -9,6 +9,7 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
import java.math.BigDecimal
internal fun WalletSelectorScreenState.updateWithNewState(
newState: WalletSelectorState,
@ -50,10 +51,14 @@ private fun List<UserWalletModel>.toUiModels(
): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->
with(userWalletModel) {
val balance = UserWalletItem.Balance(
amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol),
isLoading = fiatBalance is TotalFiatBalance.Loading,
)
val formatAmount = { amount: BigDecimal ->
amount.toFormattedFiatValue(appCurrency.symbol)
}
val balance = when (fiatBalance) {
is TotalFiatBalance.Error -> UserWalletItem.Balance.Error(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Loaded -> UserWalletItem.Balance.Loaded(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Loading -> UserWalletItem.Balance.Loading
}
when (type) {
is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem(
id = id,

View file

@ -9,9 +9,8 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
internal object MockData {
private val multiCurrencyUserWallet = MultiCurrencyUserWalletItem(
id = UserWalletId("wallet_1"),
balance = UserWalletItem.Balance(
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
isLoading = false,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
@ -22,9 +21,8 @@ internal object MockData {
private val singleCurrencyUserWallet = SingleCurrencyUserWalletItem(
id = UserWalletId("wallet_4"),
balance = UserWalletItem.Balance(
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
isLoading = false,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",

View file

@ -180,15 +180,24 @@ private fun TokensInfo(
if (isLocked) {
LockedPlaceholder()
} else {
if (balance.isLoading) {
LoadingTokensInfo(
isMultiCurrencyWallet = tokensCount != null,
)
} else {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
)
when (balance) {
is UserWalletItem.Balance.Error -> {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
showWarning = true,
)
}
is UserWalletItem.Balance.Loaded -> {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
showWarning = false,
)
}
is UserWalletItem.Balance.Loading -> {
LoadingTokensInfo(isMultiCurrencyWallet = tokensCount != null)
}
}
}
}
@ -234,17 +243,31 @@ private fun LoadedTokensInfo(
modifier: Modifier = Modifier,
balanceAmount: String,
tokensCount: Int?,
showWarning: Boolean,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
) {
Text(
text = balanceAmount,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
text = balanceAmount,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
if (showWarning) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_alert_24),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
}
}
if (tokensCount != null) {
SpacerH2()
Text(

View file

@ -9,10 +9,13 @@ internal sealed interface UserWalletItem {
val balance: Balance
val isLocked: Boolean
data class Balance(
val amount: String,
val isLoading: Boolean,
)
sealed interface Balance {
object Loading : Balance
data class Error(val amount: String) : Balance
data class Loaded(val amount: String) : Balance
}
}
internal data class MultiCurrencyUserWalletItem(

View file

@ -151,13 +151,12 @@
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance"
app:layout_goneMarginTop="12dp" />
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<include
android:id="@+id/l_card_balance"
@ -194,7 +193,7 @@
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance" />
<ProgressBar
android:id="@+id/pb_loading_user_tokens"
@ -206,7 +205,7 @@
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_wallets"

View file

@ -59,7 +59,8 @@ object Versions {
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-151"
const val tangemBlockchainSdk = "develop-152"
// const val tangemBlockchainSdk = "0.0.1"
const val tangemCardSgk = "develop-179"
// endregion Tangem

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M11,15H13V17H11V15ZM11,7H13V13H11V7ZM12,2C6.47,2 2,6.5 2,12C2,14.652 3.054,17.196 4.929,19.071C5.858,20 6.96,20.736 8.173,21.239C9.386,21.741 10.687,22 12,22C14.652,22 17.196,20.946 19.071,19.071C20.946,17.196 22,14.652 22,12C22,10.687 21.741,9.386 21.239,8.173C20.736,6.96 20,5.858 19.071,4.929C18.142,4 17.04,3.264 15.827,2.761C14.614,2.259 13.313,2 12,2ZM12,20C9.878,20 7.843,19.157 6.343,17.657C4.843,16.157 4,14.122 4,12C4,9.878 4.843,7.843 6.343,6.343C7.843,4.843 9.878,4 12,4C14.122,4 16.157,4.843 17.657,6.343C19.157,7.843 20,9.878 20,12C20,14.122 19.157,16.157 17.657,17.657C16.157,19.157 14.122,20 12,20Z" />
</vector>

View file

@ -11,7 +11,7 @@ object SaltPayWorkaround {
Blockchain.SaltPay -> Token(
name = "WXDAI",
symbol = "wxDAI",
contractAddress = "0x4346186e7461cB4DF06bCFCB4cD591423022e417",
contractAddress = "0x4200000000000000000000000000000000000006",
decimals = 18,
id = "wrapped-xdai",
)