Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-21 00:32:09 +04:00
parent e561cac009
commit c28d80cb18
91 changed files with 178 additions and 5792 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.entities
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
open class Button(val enabled: Boolean)

View file

@ -0,0 +1,5 @@
package com.tangem.tap.common.entities
import com.tangem.tap.common.toggleWidget.WidgetState
enum class ProgressState : WidgetState { Loading, Done, Error }

View file

@ -27,8 +27,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -132,16 +130,9 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
} else {
WalletFragment()
}
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
}
AppScreen.Send -> {
val featureToggles = store.state.daggerGraphState.get(
@ -176,16 +167,9 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
}
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::tokenDetailsRouter)
.getEntryFragment()
} else {
WalletDetailsFragment()
}
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::tokenDetailsRouter)
.getEntryFragment()
}
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()

View file

@ -42,12 +42,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
suspend fun Store<AppState>.onUserWalletSelected(
userWallet: UserWallet,
refresh: Boolean = false,
sendAnalyticsEvent: Boolean = false,
) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh, sendAnalyticsEvent)
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean = false) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, sendAnalyticsEvent)
}
fun Store<*>.dispatchToastNotification(resId: Int) {

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.tap.common.TestActions
@ -10,8 +11,6 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -78,4 +77,23 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null else addressDataList[0]
}
fun Wallet.createAddressesData(): List<WalletDataModel.AddressData> {
val listOfAddressData = mutableListOf<WalletDataModel.AddressData>()
// put a defaultAddress at the first place
addresses.forEach {
val addressData = WalletDataModel.AddressData(
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value),
)
if (it.type == AddressType.Default) {
listOfAddressData.add(0, addressData)
} else {
listOfAddressData.add(addressData)
}
}
return listOfAddressData
}

View file

@ -15,7 +15,6 @@ import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
@ -33,7 +32,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state, appStateHolder),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),

View file

@ -35,8 +35,6 @@ import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -54,7 +52,6 @@ data class AppState(
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val walletState: WalletState = WalletState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
@ -92,7 +89,6 @@ data class AppState(
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,

View file

@ -3,13 +3,9 @@ package com.tangem.tap.common.redux
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.home.data.HomeRepositoryImpl
import com.tangem.tap.features.home.domain.HomeRepository
import com.tangem.tap.features.wallet.data.WalletRepositoryImpl
import com.tangem.tap.features.wallet.domain.WalletRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
class FeatureRepositoryProvider(tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider) {
val walletRepository: WalletRepository = WalletRepositoryImpl(tangemTechApi, dispatchers)
val homeRepository: HomeRepository = HomeRepositoryImpl(tangemTechApi, dispatchers)
}

View file

@ -18,7 +18,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.BuyExchangeService
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -87,7 +86,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
// store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
// }
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
}
}
@ -115,9 +113,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
feedbackManager.openChat(chatConfig, action.feedbackData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)

View file

@ -3,9 +3,9 @@ package com.tangem.tap.common.toggleWidget
import android.graphics.drawable.Drawable
import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
/**
[REDACTED_AUTHOR]

View file

@ -2,25 +2,16 @@ package com.tangem.tap.common.toggleWidget
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.AnticipateOvershootInterpolator
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.view.animation.ScaleAnimation
import android.view.animation.*
import android.widget.ViewSwitcher
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.common.entities.ProgressState
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
class RefreshBalanceWidget(
private val root: ViewGroup,
) : ViewStateWidget {
class RefreshBalanceWidget(root: ViewGroup) : ViewStateWidget {
var isShowing: Boolean? = null
private set

View file

@ -4,39 +4,27 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.rekotlin.Store
import timber.log.Timber
class TapWalletManager(
private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(),
@ -55,15 +43,15 @@ class TapWalletManager(
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,
// ensuring that only one job is active at any given time.
loadUserWalletDataJob = CoroutineScope(dispatchers.io)
.launch { loadUserWalletData(userWallet, refresh, sendAnalyticsEvent) }
.launch { loadUserWalletData(userWallet, sendAnalyticsEvent) }
.also { it.join() }
}
private suspend fun loadUserWalletData(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
private suspend fun loadUserWalletData(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
@ -78,114 +66,11 @@ class TapWalletManager(
withMainContext {
// Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet))
store.dispatchWalletAction(
action = WalletAction.UpdateCanSaveUserWallets(
canSaveUserWallets = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.shouldSaveUserWalletsSync(),
),
)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded)
}
val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
setupWalletConnectV2(userWallet)
loadData(userWallet = userWallet, refresh = refresh)
}
}
private fun Store<AppState>.dispatchWalletAction(action: WalletAction) {
val walletFeatureToggles = state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
dispatch(action = action)
}
}
private fun setupWalletConnectV2(userWallet: UserWallet) {
val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
scope.launch {
val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch
wcInteractor.startListening(
userWalletId = userWallet.walletId.stringValue,
cardId = cardId,
)
}
}
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
walletStoresManager.fetch(userWallet, refresh)
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
store.state.globalState.topUpController?.loadDataSuccess()
store.dispatchWithMain(WalletAction.Warnings.CheckHashesCount.VerifyOnlineIfNeeded)
val wcInteractor = store.state.daggerGraphState.walletConnectInteractor
wcInteractor?.setUserChains(getAccountsForWc(wcInteractor))
}
.doOnFailure { error ->
val errorAction = when (error) {
is WalletStoresError -> when (error) {
is WalletStoresError.FetchFiatRatesError -> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.UpdateWalletManagerTokensError -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.InternalError(
message = error.cause.localizedMessage ?: error.customMessage,
),
)
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.CreationError,
)
is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
error = TapError.UnknownBlockchain,
)
is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
error = TapError.NoInternetConnection,
)
}
else -> WalletAction.LoadData.Failure(error = null)
}
Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
store.dispatchOnMain(errorAction)
}
}
private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List<Account> {
val walletManagerToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
val walletManagers = if (walletManagerToggles.isRedesignedScreenEnabled) {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
} else {
store.state.walletState.walletManagers
}
return walletManagers.mapNotNull {
val wallet = it.wallet
val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull(
wallet.blockchain.toNetworkId(),
)
chainId?.let {
Account(
chainId,
wallet.address,
wallet.publicKey.derivationPath?.rawPath,
)
}
}
}

View file

@ -11,10 +11,10 @@ import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.createAddressesData
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.redux.reducers.createAddressesData
import java.math.BigDecimal
interface WalletStoreBuilder {

View file

@ -1,68 +0,0 @@
package com.tangem.tap.domain.statePrinter
import com.tangem.blockchain.common.Wallet
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.redux.state.StringStateConverter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class WalletStateConverter : StringStateConverter<AppState> {
private val converter = MoshiJsonConverter.INSTANCE
override fun convert(stateHolder: AppState): String {
val walletState = stateHolder.walletState
val walletManagers = mutableListOf<Map<String, Any?>>()
walletState.walletManagers.forEach {
val wallet = it.wallet
val walletManagerMap = mutableMapOf<String, Any?>()
walletManagerMap["walletManager"] = mapOf<String, Any>(
"wallet" to convertWallet(it.wallet),
)
walletManagers.add(walletManagerMap)
}
val json = converter.prettyPrint(walletManagers)
return json
}
private fun convertWallet(wallet: Wallet): MutableMap<String, Any> {
val walletMap = mutableMapOf<String, Any>()
val amounts = mutableMapOf<String, Any>()
wallet.amounts.forEach { (type, amount) ->
val amountMap = mapOf<String, Any?>(
"value" to amount.value?.toPlainString(),
"currencySymbol" to amount.currencySymbol,
"decimals" to amount.decimals,
"type" to amount.type::class.java.simpleName,
)
amounts[type::class.java.simpleName] = amountMap
}
val publicKeyMap = mapOf(
"seedKey" to wallet.publicKey.seedKey,
"derivedKey" to wallet.publicKey.derivedKey,
"derivationPath" to wallet.publicKey.derivationPath?.rawPath,
"blockchainKey" to wallet.publicKey.blockchainKey,
)
walletMap["address"] = wallet.address
walletMap["blockchain"] = wallet.blockchain.name
walletMap["curve"] = wallet.blockchain.getSupportedCurves()[0].curve
walletMap["publicKey"] = publicKeyMap
walletMap["amounts"] = amounts
walletMap["addresses"] = wallet.addresses.toString()
return walletMap
}
}
fun printWalletState() {
val stringState = WalletStateConverter().convert(store.state)
Timber.d(stringState)
}

View file

@ -26,7 +26,6 @@ import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.customtoken.impl.presentation.models.*
@ -38,8 +37,6 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import com.tangem.wallet.BuildConfig
@ -591,14 +588,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun isTokenAlreadyAdded(): Boolean {
return if (walletFeatureToggles.isRedesignedScreenEnabled) {
isTokenAlreadyAddedNew()
} else {
isTokenAlreadyAddedOld()
}
}
private fun isTokenAlreadyAddedNew(): Boolean {
return currentCryptoCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.any { token ->
@ -621,32 +610,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
}
private fun isTokenAlreadyAddedOld(): Boolean {
return store.state.walletState.walletsStores
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
.flatten()
.filterIsInstance<Currency.Token>()
.any { wrappedCurrency ->
val contractAddress = uiState.form.contractAddressInputField.value
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val sameId = foundToken?.id == wrappedCurrency.token.id
val sameAddress = contractAddress == wrappedCurrency.token.contractAddress
val sameBlockchain =
Blockchain.fromNetworkId(networkSelectorValue.toNetworkId()) == wrappedCurrency.blockchain
val isSameDerivationPath = getDerivationPath().isSameDerivationPath(wrappedCurrency.derivationPath)
sameId && sameAddress && sameBlockchain && isSameDerivationPath
}
}
private fun isBlockchainAlreadyAdded(): Boolean {
return if (walletFeatureToggles.isRedesignedScreenEnabled) {
isBlockchainAlreadyAddedNew()
} else {
isBlockchainAlreadyAddedOld()
}
}
private fun isBlockchainAlreadyAddedNew(): Boolean {
return currentCryptoCurrencies
.filterIsInstance<CryptoCurrency.Coin>()
.any { coin ->
@ -655,18 +619,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
}
private fun isBlockchainAlreadyAddedOld(): Boolean {
return store.state.walletState.walletsStores
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
.flatten()
.filterIsInstance<Currency.Blockchain>()
.any {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
networkSelectorValue == it.blockchain &&
getDerivationPath().isSameDerivationPath(it.derivationPath)
}
}
private fun DerivationPath?.isSameDerivationPath(rawDerivationPath: String?): Boolean {
return this == rawDerivationPath?.let { DerivationPath(it) }
}

View file

@ -8,7 +8,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
@ -25,7 +24,6 @@ object DemoHelper {
TradeCryptoAction.Buy::class.java,
TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)

View file

@ -5,9 +5,9 @@ import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch

View file

@ -36,7 +36,6 @@ import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.features.demo.DemoHelper
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.redux.WalletAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder
@ -381,8 +380,6 @@ class DetailsMiddleware {
preferencesStorage.shouldShowSaveUserWalletScreen = false
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.saveShouldSaveUserWallets(item = true)
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
@ -399,7 +396,6 @@ class DetailsMiddleware {
store.state.daggerGraphState.get(DaggerGraphState::walletsRepository)
.saveShouldSaveUserWallets(item = false)
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
}
.doOnFailure { error ->

View file

@ -6,15 +6,16 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.extensions.guard
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletconnect.WalletConnectActions
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -29,11 +30,11 @@ import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -115,7 +116,6 @@ class WalletConnectMiddleware {
scope.launch {
prepareWalletManager(
scanResponse = data.scanResponse,
walletState = store.state.walletState,
blockchain = action.blockchain,
session = data.session,
walletConnectManager = walletConnectManager,
@ -176,8 +176,14 @@ class WalletConnectMiddleware {
// )
}
is WalletConnectAction.ScanCard -> {
val scanResponse = store.state.globalState.scanResponse ?: return
scanCard(scanResponse, action.session, action.chainId)
val scanResponse = userWalletsListManager.selectedUserWalletSync.guard {
Timber.w("Unable to get selected user wallet for WC session")
return
}
scope.launch(Dispatchers.Main) {
scanCard(scanResponse, action.session, action.chainId)
}
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
@ -274,7 +280,6 @@ class WalletConnectMiddleware {
val walletManager = getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = store.state.walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -372,19 +377,14 @@ class WalletConnectMiddleware {
}
private suspend fun getWalletManagers(): List<WalletManager> {
val walletManagerToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
return if (walletManagerToggles.isRedesignedScreenEnabled) {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
} else {
store.state.walletState.walletManagers
}
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
}
private fun scanCard(scanResponse: ScanResponse, session: WalletConnectSession, chainId: Int?) {
private suspend fun scanCard(userWallet: UserWallet, session: WalletConnectSession, chainId: Int?) {
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
chainId = chainId,
peer = session.peerMeta,
@ -393,27 +393,28 @@ class WalletConnectMiddleware {
return
}
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
handleScanResponse(userWallet, session, blockchain)
}
private fun getAvailableBlockchains(
derivationStyleProvider: DerivationStyleProvider,
walletState: WalletState,
): List<Blockchain> {
return walletState.currencies.filter {
it.isBlockchain() &&
!it.isCustomCurrency(derivationStyleProvider.getDerivationStyle()) && it.blockchain.isEvm()
}.map { it.blockchain }
private suspend fun getAvailableEvmBlockchains(userWalletId: UserWalletId): List<Blockchain> {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
.asSequence()
.filterIsInstance<CryptoCurrency.Coin>()
.filterNot { it.isCustom }
.mapNotNull { Blockchain.fromNetworkId(it.network.id.value) }
.filter { it.isEvm() }
.toList()
}
private suspend fun prepareWalletManager(
scanResponse: ScanResponse,
walletState: WalletState,
blockchain: Blockchain,
session: WalletConnectSession,
walletConnectManager: WalletConnectManager,
) {
val walletManager = getWalletManager(session.wallet, blockchain, walletState).guard {
val walletManager = getWalletManager(session.wallet, blockchain).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -445,20 +446,25 @@ class WalletConnectMiddleware {
}
}
private fun handleScanResponse(scanResponse: ScanResponse, session: WalletConnectSession, blockchain: Blockchain) {
private suspend fun handleScanResponse(
userWallet: UserWallet,
session: WalletConnectSession,
blockchain: Blockchain,
) {
val scanResponse = userWallet.scanResponse
if (!scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return
}
val walletState = store.state.walletState
val updatedSession = session.copy(wallet = session.wallet.copy(blockchain = blockchain))
store.dispatch(
WalletConnectAction.SetNewSessionData(
NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain),
NewWcSessionData(updatedSession, scanResponse, blockchain),
),
)
val blockchains = if (blockchain.isEvm()) {
getAvailableBlockchains(scanResponse.derivationStyleProvider, walletState)
getAvailableEvmBlockchains(userWallet.walletId)
} else {
emptyList()
}
@ -469,11 +475,7 @@ class WalletConnectMiddleware {
)
}
private suspend fun getWalletManager(
wallet: WalletForSession,
blockchain: Blockchain,
walletState: WalletState,
): WalletManager? {
private suspend fun getWalletManager(wallet: WalletForSession, blockchain: Blockchain): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
@ -483,25 +485,15 @@ class WalletConnectMiddleware {
val derivation = blockchainToMake.derivationPath(
style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
)?.rawPath
val walletFeatureToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
return if (walletFeatureToggles.isRedesignedScreenEnabled) {
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchainToMake,
derivationPath = derivation,
)
} else {
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = derivation,
tokens = emptyList(),
)
walletState.getWalletManager(blockchainNetwork)
}
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchainToMake,
derivationPath = derivation,
)
}
private fun isWalletConnectUri(uri: String): Boolean {

View file

@ -20,7 +20,6 @@ import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -109,13 +108,7 @@ internal class AppSettingsViewModel(
}
private fun showAppCurrencySelector() {
val action = if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
NavigationAction.NavigateTo(AppScreen.AppCurrencySelector)
} else {
WalletAction.AppCurrencyAction.ChooseAppCurrency
}
store.dispatchOnMain(action)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppCurrencySelector))
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {

View file

@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.domain.userwallets.Artwork
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
@ -17,7 +16,6 @@ internal data class CardSettingsScreenState(
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
val cardImage: Artwork? = null,
)
internal sealed class CardInfo(

View file

@ -12,11 +12,9 @@ import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import org.rekotlin.StoreSubscriber
@ -37,15 +35,12 @@ internal class CardSettingsViewModel @Inject constructor(
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
}
is Either.Right -> {
store.dispatchOnMain(WalletAction.UpdateUserWalletArtwork(selectedWalletEither.value.walletId))
}
is Either.Right -> Unit
}
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState &&
oldState.walletState == newState.walletState
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
@ -67,7 +62,6 @@ internal class CardSettingsViewModel @Inject constructor(
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
cardImage = store.state.walletState.cardImage,
)
} else {
val cardId = if (state.card.isTangemTwins) {
@ -105,7 +99,6 @@ internal class CardSettingsViewModel @Inject constructor(
onElementClick = {
handleClickingItem(it)
},
cardImage = store.state.walletState.cardImage,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -12,6 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.feedback.FeedbackEmail
@ -24,7 +26,6 @@ import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import timber.log.Timber
// TODO: change to Android ViewModel [REDACTED_JIRA]
internal class DetailsViewModel(
@ -158,7 +160,15 @@ internal class DetailsViewModel(
private fun linkMoreCards() {
Analytics.send(Settings.ButtonCreateBackup())
store.dispatchOnMain(WalletAction.MultiWallet.BackupWallet)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to backup wallet, no user wallet selected")
return
}
val scanResponse = selectedUserWallet.scanResponse
Analytics.addContext(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
private fun scanAndSaveUserWallet() {

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.Action
sealed class DisclaimerAction : Action {

View file

@ -2,9 +2,9 @@ package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.AppScreen
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.DummyDisclaimer
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
data class DisclaimerState(

View file

@ -8,6 +8,7 @@ import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -17,7 +18,6 @@ import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentDisclaimerBinding

View file

@ -1,13 +1,9 @@
package com.tangem.tap.features.disclaimer.ui
import android.graphics.Bitmap
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.*
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.common.entities.ProgressState
class DisclaimerWebViewClient : WebViewClient() {

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.home.redux
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
import java.util.Locale

View file

@ -1,13 +1,7 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.net.Uri
import com.tangem.core.analytics.Analytics
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
@ -15,16 +9,19 @@ import com.tangem.tap.store
class BuyCurrencyIntentHandler : IntentHandler {
override fun handleIntent(intent: Intent?): Boolean {
val data = intent?.data ?: return false
val currency = store.state.walletState.selectedCurrency ?: return false
// FIXME: It hasn't worked since the redesign
// val data = intent?.data ?: return false
// val currency = store.state.walletState.selectedCurrency ?: return false
//
// val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
// return if (data.host == successUri.host && data.authority == successUri.authority) {
// val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
// Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
// true
// } else {
// false
// }
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
return if (data.host == successUri.host && data.authority == successUri.authority) {
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
true
} else {
false
}
return false
}
}

View file

@ -1,74 +0,0 @@
package com.tangem.tap.features.onboarding
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchShare
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
/**
[REDACTED_AUTHOR]
*/
class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context,
) : BottomSheetDialog(context) {
var binding: DialogOnboardingAddressInfoBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogOnboardingAddressInfoBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
setOnCancelListener {
store.dispatchDialogHide()
binding = null
}
}
override fun show() {
super.show()
Analytics.send(Token.Receive.ScreenOpened())
showData(data = stateDialog.addressData)
}
private fun showData(data: WalletDataModel.AddressData) = with(binding!!) {
pseudoToolbar.imvClose.setOnClickListener {
dismissWithAnimation = true
cancel()
}
imvQrCode.setImageBitmap(data.shareUrl.toQrCode())
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Receive.ButtonCopyAddress())
context.copyToClipboard(data.address)
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Receive.ButtonShareAddress())
store.dispatchShare(data.shareUrl)
}
val blockchain = stateDialog.currency.blockchain
tvReceiveMessage.text = tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
blockchain.fullName,
blockchain.currency,
blockchain.fullName,
)
}
}

View file

@ -5,18 +5,18 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.tap.features.demo.isDemoCard
import timber.log.Timber
import java.math.BigDecimal

View file

@ -8,6 +8,7 @@ import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
@ -19,7 +20,6 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.scope

View file

@ -14,6 +14,7 @@ import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.isLockedSync
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
@ -25,7 +26,6 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.preferencesStorage

View file

@ -21,7 +21,6 @@ import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -119,16 +118,8 @@ internal class SaveWalletMiddleware {
)
}
val savedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("User wallet is not saved")
return@launch
}
store.dispatchWithMain(SaveWalletAction.Save.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
store.dispatchWithMain(
action = WalletAction.MultiWallet.CheckForBackupWarning(savedUserWallet.scanResponse.card),
)
}
}
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.wallet.redux.ProgressState
/**
[REDACTED_AUTHOR]

View file

@ -2,23 +2,12 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.tap.common.extensions.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.ReceiptCrypto
import com.tangem.tap.features.send.redux.states.ReceiptFiat
import com.tangem.tap.features.send.redux.states.ReceiptLayoutType
import com.tangem.tap.features.send.redux.states.ReceiptState
import com.tangem.tap.features.send.redux.states.ReceiptSymbols
import com.tangem.tap.features.send.redux.states.ReceiptTokenCrypto
import com.tangem.tap.features.send.redux.states.ReceiptTokenFiat
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.wallet.redux.utils.CAN_BE_LOWER_SIGN
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.store
import java.math.BigDecimal
@ -143,9 +132,9 @@ class ReceiptReducer : SendInternalReducer {
)
} else {
ReceiptTokenFiat(
amountFiat = UNKNOWN_AMOUNT_SIGN,
feeFiat = UNKNOWN_AMOUNT_SIGN,
totalFiat = UNKNOWN_AMOUNT_SIGN,
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
@ -175,7 +164,7 @@ class ReceiptReducer : SendInternalReducer {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = UNKNOWN_AMOUNT_SIGN,
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
symbols = symbols,
)
}
@ -216,7 +205,7 @@ class ReceiptReducer : SendInternalReducer {
sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
}
else -> {
UNKNOWN_AMOUNT_SIGN
BigDecimalFormatter.EMPTY_BALANCE_SIGN
}
}
}
@ -225,4 +214,8 @@ class ReceiptReducer : SendInternalReducer {
val result = if (feeState.feeIsApproximate) "$CAN_BE_LOWER_SIGN $this" else this
return result.trim()
}
private companion object {
const val CAN_BE_LOWER_SIGN = "<"
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.common.entities.ProgressState
import java.math.BigDecimal
/**

View file

@ -40,8 +40,8 @@ import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.ui.adapters
package com.tangem.tap.features.send.ui.adapters
import android.view.LayoutInflater
import android.view.View
@ -13,14 +13,13 @@ import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutWarningCardActionBinding
import timber.log.Timber
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
// TODO: Delete with SendFeatureToggles
@Deprecated(message = "Used only in old send screen")
class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH {
@ -81,11 +80,9 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
private fun setupControlButtons(warning: WarningMessage) = when (warning.type) {
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.hide()
binding.btnClose.hide()
}
WarningMessage.Type.Temporary -> {
binding.groupControlsRating.hide()
binding.groupControlsTemporary.show()
binding.btnClose.hide()
@ -115,7 +112,6 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnClose.setOnClickListener {
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed))
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
// binding.btnCanBeBetter.setOnClickListener {
// Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked))
@ -127,7 +123,6 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked))
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()
task.addOnCompleteListener {

View file

@ -7,6 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
@ -19,11 +21,8 @@ import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.SendViewModel
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.send.ui.dialogs.*
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.utils.ROUGH_SIGN
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.wallet.R
/**
@ -316,7 +315,7 @@ internal class SendStateSubscriber(
fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings)
fun roughOrEmpty(value: String): String {
return if (value == UNKNOWN_AMOUNT_SIGN) value else "$ROUGH_SIGN $value"
return if (value == BigDecimalFormatter.EMPTY_BALANCE_SIGN) value else "$ROUGH_SIGN $value"
}
when (feeProgressState) {
@ -362,7 +361,7 @@ internal class SendStateSubscriber(
llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}")
}
if (receipt.willSentFiat == UNKNOWN_AMOUNT_SIGN) {
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
llTotalContainer.tvWillBeSentValue.hide()
} else {
llTotalContainer.tvWillBeSentValue.show()
@ -414,4 +413,8 @@ internal class SendStateSubscriber(
else -> {}
}
}
private companion object {
const val ROUGH_SIGN = ""
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
@ -13,8 +12,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import timber.log.Timber
import kotlin.properties.Delegates
@ -39,14 +36,6 @@ internal class TokensListMigration(
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies {
return if (walletFeatureToggles.isRedesignedScreenEnabled) {
getNewCryptoCurrencies()
} else {
getLegacyCryptoCurrencies()
}
}
private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies {
return when (val selectedWalletEither = getSelectedWalletSyncUseCase()) {
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
@ -90,41 +79,6 @@ internal class TokensListMigration(
}
}
private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies {
val wallets = store.state.walletState.walletsDataFromStores
val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle()
return TokensListCryptoCurrencies(
coins = wallets.toNonCustomBlockchains(derivationStyle),
tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle),
)
}
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return this
.mapNotNull { walletDataModel ->
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(walletDataModel.currency as? Currency.Blockchain)?.blockchain
}
}
.distinct()
}
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return this
.mapNotNull { walletDataModel ->
if (walletDataModel.currency !is Currency.Token) return@mapNotNull null
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
}
.distinct()
}
fun onSaveButtonClick(
currentTokensList: List<TokenWithBlockchain>,
currentBlockchainList: List<Blockchain>,

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.wallet.data
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.tap.features.wallet.domain.WalletRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
/**
* Implementation of repository for Wallet feature
*
* @property tangemTechApi API for server requests
* @property dispatchers coroutine dispatcher provider
*/
class WalletRepositoryImpl(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletRepository {
override suspend fun getCurrencyList(): CurrenciesResponse = withContext(dispatchers.io) {
tangemTechApi.getCurrencyList().getOrThrow()
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.wallet.domain
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
/** Repository for Wallet feature */
interface WalletRepository {
/** Get list of currency */
suspend fun getCurrencyList(): CurrenciesResponse
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.wallet.models
import com.tangem.tap.domain.model.WalletStoreModel
sealed class WalletWarning(val showingPosition: Int) {
data class ExistentialDeposit(
val currencyName: String,
val edStringValueWithSymbol: String,
) : WalletWarning(1)
data class TransactionInProgress(val currencyName: String) : WalletWarning(showingPosition = 10)
data class BalanceNotEnoughForFee(
val currencyName: String,
val blockchainFullName: String,
val blockchainSymbol: String,
) : WalletWarning(showingPosition = 30)
data class Rent(val walletRent: WalletStoreModel.WalletRent) : WalletWarning(showingPosition = 40)
}
data class WalletWarningDescription(val title: String, val message: String)

View file

@ -1,143 +0,0 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
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.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.redux.models.WalletDialog
import com.tangem.wallet.R
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class WalletAction : Action {
object PopBackToInitialScreen : WalletAction()
data class UpdateCanSaveUserWallets(val canSaveUserWallets: Boolean) : WalletAction()
object LoadData : WalletAction() {
object Refresh : WalletAction()
object Success : WalletAction()
data class Failure(val error: TapError?) : WalletAction()
}
sealed class MultiWallet : WalletAction() {
data class SelectWallet(val currency: Currency?) : MultiWallet()
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(val currency: Currency) : MultiWallet()
object BackupWallet : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
object ScanToGetDerivations : MultiWallet()
/**
* Display warning if card has no backup
*
* @param card card to check status
* */
data class CheckForBackupWarning(val card: CardDTO) : MultiWallet()
}
sealed class Warnings : WalletAction() {
object CheckHashesCount : Warnings() {
/**
* Start online verification of signed hashes for single currency wallets if the warning not displayed
* */
object VerifyOnlineIfNeeded : Warnings()
object SaveCardId : Warnings()
}
object CheckIfNeeded : Warnings()
object Update : Warnings()
data class Set(val warningList: List<WarningMessage>) : Warnings()
object AppRating : Warnings() {
object SetNeverToShow : Warnings()
object RemindLater : Warnings()
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
}
data class Scan(
val onScanSuccessEvent: AnalyticsEvent?,
val scope: CoroutineScope,
) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction()
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
sealed class DialogAction : WalletAction() {
data class QrCode(
val currency: Currency,
val selectedAddress: WalletDataModel.AddressData,
) : DialogAction()
object SignedHashesMultiWalletDialog : DialogAction()
data class ChooseTradeActionDialog(
val buyAllowed: Boolean,
val sellAllowed: Boolean,
val swapAllowed: Boolean,
) : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>) : DialogAction()
data class RussianCardholdersWarningDialog(
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null,
) : DialogAction()
object Hide : DialogAction()
}
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
data class ChangeWallet(val scope: LifecycleCoroutineScope) : WalletAction()
object ShowSaveWalletIfNeeded : WalletAction()
sealed class TradeCryptoAction : WalletAction() {
object Sell : TradeCryptoAction()
data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction()
object Swap : TradeCryptoAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class AppCurrencyAction : WalletAction() {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
data class UserWalletChanged(val userWallet: UserWallet) : WalletAction()
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction()
data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction()
data class UpdateUserWalletArtwork(val walletId: UserWalletId) : WalletAction()
data class SetArtworkUrl(val userWalletId: UserWalletId, val url: String) : WalletAction()
}

View file

@ -1,118 +0,0 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.userwallets.Artwork
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
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.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
import kotlin.properties.ReadOnlyProperty
data class WalletState(
val state: ProgressState = ProgressState.Done,
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val walletsStores: List<WalletStoreModel> = listOf(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedCurrency: Currency? = null,
val isTestnet: Boolean = false,
val totalBalance: TotalFiatBalance? = null,
val showBackupWarning: Boolean = false,
val missingDerivations: List<BlockchainNetwork> = emptyList(),
val loadingUserTokens: Boolean = false,
val walletCardsCount: Int? = null,
val canSaveUserWallets: Boolean = false,
) : StateType {
val walletsDataFromStores: List<WalletDataModel>
get() = walletsStores.flatMap { it.walletsData }
val selectedWalletData: WalletDataModel?
get() = walletsDataFromStores.firstOrNull { it.currency == selectedCurrency }
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { _, _ ->
store.state.twinCardsState
}
val isTangemTwins: Boolean
get() = store.state.globalState.scanResponse?.cardTypesResolver?.isTangemTwins() == true
val isExchangeServiceFeatureOn: Boolean
get() = store.state.globalState.exchangeManager.featureIsSwitchedOn()
val blockchains: List<Blockchain>
get() = walletsStores.mapNotNull { it.walletManager?.wallet?.blockchain }
val currencies: List<Currency>
get() = walletsStores.flatMap { it.walletsData }.map { it.currency }
val walletManagers: List<WalletManager>
get() = walletsStores.mapNotNull { it.walletManager }
private val primaryWalletStore: WalletStoreModel?
get() = if (isMultiwalletAllowed || walletsStores.isEmpty() || walletsStores.size > 1) {
null
} else {
walletsStores[0]
}
val primaryWalletManager: WalletManager?
get() = primaryWalletStore?.walletManager
val primaryWalletData: WalletDataModel?
get() = primaryWalletStore?.blockchainWalletData
val primaryTokenData: WalletDataModel?
get() = primaryWalletStore?.walletsData
?.firstOrNull { it.currency !is Currency.Blockchain }
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager
}
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
return walletsStores.firstOrNull {
it.blockchain == blockchain.blockchain &&
it.derivationPath?.rawPath == blockchain.derivationPath
}?.walletManager
}
fun getWalletStore(currency: Currency?): WalletStoreModel? {
if (currency == null) return null
return walletsStores.firstOrNull {
it.blockchain == currency.blockchain &&
it.derivationPath?.rawPath == currency.derivationPath
}
}
fun getBlockchainAmount(currency: Currency): BigDecimal =
getWalletManager(currency)?.wallet?.amounts?.get(AmountType.Coin)?.value ?: BigDecimal.ZERO
}
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
enum class ErrorType {
NoInternetConnection,
UnknownBlockchain,
}
sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class SendButton(enabled: Boolean) : WalletMainButton(enabled)
}

View file

@ -1,157 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.data.source.preferences.model.DataSourceCurrency
import com.tangem.data.source.preferences.model.DataSourceFiatCurrency
import com.tangem.data.source.preferences.storage.FiatCurrenciesPrefStorage
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.domain.WalletRepository
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.launch
import timber.log.Timber
class AppCurrencyMiddleware(
private val walletRepository: WalletRepository,
private val tapWalletManager: TapWalletManager,
private val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage,
private val featureToggles: WalletFeatureToggles,
private val appCurrencyRepository: AppCurrencyRepository,
private val appCurrencyProvider: () -> FiatCurrency,
) {
private val showSelectorJobHolder = JobHolder()
fun handle(action: WalletAction.AppCurrencyAction) {
when (action) {
is WalletAction.AppCurrencyAction.ChooseAppCurrency -> showSelector()
is WalletAction.AppCurrencyAction.SelectAppCurrency -> selectCurrency(action)
}
}
private fun showSelector() {
if (featureToggles.isRedesignedScreenEnabled) {
showSelectorNew()
} else {
showSelectorLegacy()
}
}
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
if (featureToggles.isRedesignedScreenEnabled) {
selectCurrencyNew(action.fiatCurrency)
} else {
selectCurrencyLegacy(action.fiatCurrency)
}
}
private fun showSelectorNew() {
scope.launch {
val currencies = appCurrencyRepository.getAvailableAppCurrencies()
store.dispatchDialogShow(
WalletDialog.CurrencySelectionDialog(
currenciesList = currencies.map { appCurrency ->
FiatCurrency(
code = appCurrency.code,
name = appCurrency.name,
symbol = appCurrency.symbol,
)
},
currentAppCurrency = appCurrencyProvider.invoke(),
),
)
}.saveIn(showSelectorJobHolder)
}
private fun showSelectorLegacy() {
val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore()
if (storedFiatCurrencies.isNotEmpty()) {
store.dispatchDialogShow(
WalletDialog.CurrencySelectionDialog(
currenciesList = storedFiatCurrencies.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke(),
),
)
}
scope.launch {
runCatching { walletRepository.getCurrencyList() }
.onSuccess { response ->
val currenciesList = response.currencies
.map { with(it) { DataSourceCurrency(id, code, name, rateBTC, unit, type) } }
if (currenciesList.isNotEmpty() && currenciesList.toSet() != storedFiatCurrencies.toSet()) {
fiatCurrenciesPrefStorage.save(currenciesList)
store.dispatchDialogShow(
WalletDialog.CurrencySelectionDialog(
currenciesList = currenciesList.mapToUiModel(),
currentAppCurrency = appCurrencyProvider.invoke(),
),
)
}
}
}
}
private fun selectCurrencyNew(fiatCurrency: FiatCurrency) {
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(fiatCurrency)))
scope.launch {
appCurrencyRepository.changeAppCurrency(fiatCurrency.code)
store.dispatchWithMain(GlobalAction.ChangeAppCurrency(fiatCurrency))
store.dispatchWithMain(DetailsAction.ChangeAppCurrency(fiatCurrency))
store.dispatchWithMain(WalletSelectorAction.ChangeAppCurrency(fiatCurrency))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to select currency, no user wallet selected")
return@launch
}
tapWalletManager.loadData(selectedUserWallet, refresh = true)
}
}
private fun selectCurrencyLegacy(fiatCurrency: FiatCurrency) {
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(fiatCurrency)))
fiatCurrenciesPrefStorage.saveAppCurrency(
with(fiatCurrency) { DataSourceFiatCurrency(code, name, symbol) },
)
store.dispatch(GlobalAction.ChangeAppCurrency(fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(fiatCurrency))
store.dispatch(WalletSelectorAction.ChangeAppCurrency(fiatCurrency))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to select currency, no user wallet selected")
return
}
scope.launch {
tapWalletManager.loadData(selectedUserWallet, refresh = true)
}
}
private fun List<DataSourceCurrency>.mapToUiModel(): List<FiatCurrency> {
return this.map {
FiatCurrency(
code = it.code,
name = it.name,
symbol = it.unit,
)
}
}
}

View file

@ -1,124 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
class MultiWalletMiddleware {
@Suppress("LongMethod", "ComplexMethod")
fun handle(action: WalletAction.MultiWallet, walletState: WalletState?) {
when (action) {
is WalletAction.MultiWallet.SelectWallet -> {
if (action.currency != null) {
store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails))
}
}
is WalletAction.MultiWallet.TryToRemoveWallet -> {
val currency = action.currency
val walletManager = walletState?.getWalletManager(currency).guard {
store.dispatchErrorNotification(TapError.UnsupportedState("walletManager is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
if (currency.isBlockchain() && walletManager.cardTokens.isNotEmpty()) {
store.dispatchDialogShow(
WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol,
),
)
} else {
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
Analytics.send(ButtonRemoveToken(AnalyticsParam.CurrencyType.Currency(currency)))
store.dispatch(WalletAction.MultiWallet.RemoveWallet(currency))
store.dispatch(NavigationAction.PopBackTo())
},
),
)
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove wallet, no user wallet selected")
return
}
scope.launch {
walletCurrenciesManager.removeCurrency(
userWallet = selectedUserWallet,
currencyToRemove = action.currency,
)
}
}
is WalletAction.MultiWallet.BackupWallet -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to backup wallet, no user wallet selected")
return
}
val scanResponse = selectedUserWallet.scanResponse
Analytics.addContext(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is WalletAction.MultiWallet.AddMissingDerivations -> {
store.state.globalState.topUpController?.addMissingDerivations(action.blockchains)
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to scan to get derivations, no user wallet selected")
return
}
store.state.globalState.topUpController?.scanToGetDerivations()
scanAndUpdateCard(selectedUserWallet)
}
else -> {}
}
}
private fun scanAndUpdateCard(selectedUserWallet: UserWallet) = scope.launch(Dispatchers.Default) {
store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor)
.scan(cardId = selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true)
.flatMap { scanResponse ->
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchWithMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedUserWallet.scanResponse))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
}

View file

@ -1,368 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import androidx.core.os.bundleOf
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
@Suppress("LargeClass")
class TradeCryptoMiddleware {
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction()
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Swap -> {
// todo remove old flow
}
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
is TradeCryptoAction.New.Swap -> openSwap(
currency = action.cryptoCurrency,
)
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
}
}
@Deprecated("Use proceedNewBuyAction instead")
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
return
}
val card = store.state.globalState.scanResponse?.card ?: return
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val exchangeManager = store.state.globalState.exchangeManager
val appCurrency = store.state.globalState.appCurrency
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
val walletManager = store.state.walletState.getWalletManager(currency)
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
return
}
scope.launch {
buyErc20TestnetTokens(
card = card,
walletManager = walletManager,
destinationAddress = currency.token.contractAddress,
)
}
return
}
exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = addresses[0].address,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val status = action.cryptoCurrencyStatus
val currency = status.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
val exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = blockchain,
cryptoCurrencyName = currency.symbol,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = topUrl?.let {
WalletDialog.RussianCardholdersWarningDialog.Data(
topUpUrl = it,
)
}
store.dispatchOnMain(
WalletAction.DialogAction.RussianCardholdersWarningDialog(
dialogData = dialogData,
),
)
return
}
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
return@launch
}
buyErc20TestnetTokens(
card = action.userWallet.scanResponse.card,
walletManager = walletManager,
destinationAddress = currency.contractAddress,
)
}
return
}
topUrl?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency)))
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = addresses[0].address,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val currency = action.cryptoCurrencyStatus.currency
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = Blockchain.fromId(currency.network.id.value),
cryptoCurrencyName = currency.symbol,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
return if (walletState.primaryTokenData == null) {
walletState.selectedWalletData?.currency
} else {
walletState.primaryTokenData?.currency as? Currency.Token
}.guard {
store.dispatchDebugErrorNotification("Can't select an appropriate currency for a Trade action")
return null
}
}
private fun preconfigureAndOpenSendScreen(action: TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard {
FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null"))
return
}
store.dispatchOnMain(
PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = selectedWalletData.fiatRate,
),
)
store.dispatchOnMain(
SendAction.SendSpecificTransaction(
sendAmount = action.amount,
destinationAddress = action.destinationAddress,
transactionId = action.transactionId,
),
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
}
private fun openReceiptUrl(transactionId: String) {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
action = CurrencyExchangeManager.Action.Sell,
transactionId = transactionId,
)?.let { store.dispatchOpenUrl(it) }
}
private fun openSwap(currency: CryptoCurrency) {
val bundle = bundleOf(
SwapFragment.CURRENCY_BUNDLE_KEY to currency,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
}
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
val currency = action.tokenCurrency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmount = walletManager.wallet.amounts.values.firstOrNull {
val amountType = it.type
amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmount,
tokenRate = action.tokenFiatRate,
),
)
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) {
val cryptoStatus = action.coinStatus
val currency = cryptoStatus.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin }
when (currency) {
is CryptoCurrency.Coin -> {
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol }
if (amountToSend == null) {
val error = TapError.UnsupportedState(stateError = "Amount to send is null")
FirebaseCrashlytics.getInstance()
.recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = amountToSend,
coinRate = cryptoStatus.value.fiatRate,
),
)
}
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
}
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
class WalletDialogsMiddleware {
fun handle(action: WalletAction.DialogAction) {
when (action) {
is WalletAction.DialogAction.SignedHashesMultiWalletDialog -> {
store.dispatchDialogShow(WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.DialogAction.ChooseTradeActionDialog -> {
store.state.walletState.selectedWalletData?.let {
Analytics.send(Token.ButtonExchange(AnalyticsParam.CurrencyType.Currency(it.currency)))
}
store.dispatchDialogShow(
WalletDialog.ChooseTradeActionDialog(
buyAllowed = action.buyAllowed,
sellAllowed = action.sellAllowed,
swapAllowed = action.swapAllowed,
),
)
}
is WalletAction.DialogAction.QrCode -> {
store.dispatchDialogShow(
AppDialog.AddressInfoDialog(
currency = action.currency,
addressData = action.selectedAddress,
),
)
}
is WalletAction.DialogAction.ChooseCurrency -> {
if (action.amounts.isEmpty()) return
store.dispatchDialogShow(
WalletDialog.SelectAmountToSendDialog(
amounts = action.amounts,
),
)
}
is WalletAction.DialogAction.RussianCardholdersWarningDialog -> {
store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog(action.dialogData))
}
is WalletAction.DialogAction.Hide -> {
store.dispatchDialogHide()
}
}
}
}

View file

@ -1,412 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import androidx.lifecycle.LifecycleCoroutineScope
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.userwallets.GetCardImageUseCase
import com.tangem.domain.wallets.legacy.lockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.getSendableAmounts
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.ifActive
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LargeClass")
class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
private val walletDialogMiddleware = WalletDialogsMiddleware()
private val appCurrencyMiddleware by lazy(mode = LazyThreadSafetyMode.NONE) {
AppCurrencyMiddleware(
// TODO("After adding DI") get dependencies by DI
walletRepository = store.state.featureRepositoryProvider.walletRepository,
tapWalletManager = store.state.globalState.tapWalletManager,
fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage,
appCurrencyRepository = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository),
featureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles),
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
private val networkConnectionManager: NetworkConnectionManager
get() = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
private var updateWalletStoresJob: Job? = null
set(value) {
field?.cancel()
field = value
}
val walletMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
handleAction(state, action)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleAction(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
val globalState = store.state.globalState
val walletState = store.state.walletState
when (action) {
is TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.DialogAction -> walletDialogMiddleware.handle(action)
is WalletAction.CreateWallet -> {
scope.launch {
when (val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId)) {
is CompletionResult.Success -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to create wallet, no user wallet selected")
return@launch
}
val updatedScanResponse = selectedUserWallet.scanResponse.copy(
card = result.data,
)
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedScanResponse))
userWalletsListManager.update(selectedUserWallet.walletId) { userWallet ->
userWallet.copy(scanResponse = updatedScanResponse)
}
}
is CompletionResult.Failure -> Unit
}
}
}
is WalletAction.Scan -> {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
action.scope.launch {
delay(timeMillis = 700)
store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent, action.scope))
}
}
is WalletAction.LoadData,
is WalletAction.LoadData.Refresh,
-> {
val selectedWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to load/refresh wallets data, no user wallet selected")
return
}
scope.launch {
globalState.tapWalletManager.loadData(
userWallet = selectedWallet,
refresh = action is WalletAction.LoadData.Refresh,
)
}
store.dispatchOnMain(WalletAction.UpdateUserWalletArtwork(selectedWallet.walletId))
}
is WalletAction.CopyAddress -> {
Analytics.send(Token.Receive.ButtonCopyAddress())
action.context.copyToClipboard(action.address)
store.dispatch(WalletAction.CopyAddress.Success)
}
is WalletAction.ShareAddress -> {
Analytics.send(Token.Receive.ButtonShareAddress())
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
Analytics.send(Token.ButtonExplore())
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
val walletStore = walletState.getWalletStore(walletState.selectedCurrency)
val selectedWalletData = walletState.selectedWalletData
val walletManager = walletStore?.walletManager
if (walletStore == null || walletManager == null || selectedWalletData == null) {
val error = TapError.UnsupportedState(
"WalletAction.Send: walletStore or selectedWalletData or walletManager is null",
)
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
if (!networkConnectionManager.isOnline) {
store.dispatchErrorNotification(TapError.NoInternetConnection)
return
}
val currency = selectedWalletData.currency
val initSendStateAction = if (action.amount == null) {
val sendableAmounts = walletManager.wallet.getSendableAmounts()
if (sendableAmounts.isEmpty()) {
val error = TapError.UnsupportedState("WalletAction.Send: Nothing to send")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
if (walletState.isMultiwalletAllowed) {
val amountToSend = findAmountToSend(currency = currency, amounts = sendableAmounts)
if (amountToSend == null) {
val error = TapError.UnsupportedState("WalletAction.Send: Amount to send is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
makeInitSendStateActionByCurrency(
currency = currency,
amount = amountToSend,
walletStore = walletStore,
walletManager = walletManager,
selectedWalletData = selectedWalletData,
)
} else {
val isSingleAmount = sendableAmounts.size == 1
if (isSingleAmount) {
makeInitSendStateActionByAmount(
amount = sendableAmounts.first(),
walletStore = walletStore,
walletManager = walletManager,
selectedWalletData = selectedWalletData,
)
} else {
store.dispatch(WalletAction.DialogAction.ChooseCurrency(sendableAmounts))
return
}
}
} else {
// action.amount received from the ChooseCurrency dialog
makeInitSendStateActionByAmount(
amount = action.amount,
walletManager = walletManager,
walletStore = walletStore,
selectedWalletData = selectedWalletData,
)
}
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(currency)))
store.dispatch(initSendStateAction)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
is WalletAction.ShowSaveWalletIfNeeded -> {
showSaveWalletIfNeeded()
}
is WalletAction.ChangeWallet -> {
changeWallet(walletState, action.scope)
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
// Cancel update job when new wallet stores received
updateWalletStoresJob = scope.launch(Dispatchers.Default) {
ifActive { fetchTotalFiatBalance(action.walletStores) }
ifActive { findMissedDerivations(action.walletStores) }
ifActive { tryToShowAppRatingWarning(action.walletStores) }
ifActive { store.state.globalState.topUpController?.walletStoresChanged(action.walletStores) }
}
}
is WalletAction.TotalFiatBalanceChanged -> Unit
is WalletAction.PopBackToInitialScreen -> {
userWalletsListManager.lockIfLockable()
val screen = if (walletState.canSaveUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
}
is WalletAction.ChangeSelectedAddress -> {
changeSelectedWalletAddress(action.type, walletState)
}
is WalletAction.UpdateUserWalletArtwork -> {
scope.launch {
userWalletsListManager
.update(
userWalletId = action.walletId,
update = { userWallet ->
userWallet.copy(
artworkUrl = GetCardImageUseCase().invoke(
cardId = userWallet.cardId,
cardPublicKey = userWallet.scanResponse.card.cardPublicKey,
),
)
},
)
.doOnSuccess {
store.dispatch(
WalletAction.SetArtworkUrl(userWalletId = action.walletId, url = it.artworkUrl),
)
}
}
}
}
}
private fun findAmountToSend(currency: Currency, amounts: List<Amount>): Amount? {
return amounts.find { amount ->
val amountType = amount.type
if (amountType is AmountType.Token && currency is Currency.Token) {
val token = amountType.token
token.symbol == currency.currencySymbol && token.contractAddress == currency.token.contractAddress
} else {
amount.currencySymbol == currency.currencySymbol
}
}
}
private fun makeInitSendStateActionByAmount(
amount: Amount,
walletStore: WalletStoreModel,
walletManager: WalletManager,
selectedWalletData: WalletDataModel,
): PrepareSendScreen = when (amount.type) {
AmountType.Coin ->
PrepareSendScreen(
walletManager = walletManager,
coinAmount = amount,
coinRate = selectedWalletData.fiatRate,
)
is AmountType.Token -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = walletStore.blockchainWalletData.fiatRate,
tokenAmount = amount,
tokenRate = selectedWalletData.fiatRate,
)
}
AmountType.Reserve -> {
val exception = IllegalStateException("WalletAction.Send: Reserve can't be sent")
FirebaseCrashlytics.getInstance().recordException(exception)
throw exception
}
}
private fun makeInitSendStateActionByCurrency(
currency: Currency,
amount: Amount,
walletStore: WalletStoreModel,
walletManager: WalletManager,
selectedWalletData: WalletDataModel,
): PrepareSendScreen = when (currency) {
is Currency.Blockchain -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = amount,
coinRate = selectedWalletData.fiatRate,
)
}
is Currency.Token -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = walletStore.blockchainWalletData.fiatRate,
tokenAmount = amount,
tokenRate = selectedWalletData.fiatRate,
)
}
}
private fun changeSelectedWalletAddress(type: AddressType, state: WalletState) {
val selectedUserWalletId = userWalletsListManager.selectedUserWalletSync?.walletId.guard {
Timber.e("Unable to change selected wallet address, no user wallet selected")
return
}
val selectedCurrency = state.selectedCurrency.guard {
Timber.e("Unable to change selected wallet address, no currency selected")
return
}
scope.launch(Dispatchers.Default) {
walletStoresManager.updateSelectedAddress(selectedUserWalletId, selectedCurrency, type)
}
}
private suspend fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>) {
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(walletStores)
if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
}
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
val missedDerivations = wallStores
.filter { store ->
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
}
.map(WalletStoreModel::blockchainNetwork)
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
}
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
}
private fun showSaveWalletIfNeeded() {
if (preferencesStorage.shouldShowSaveUserWalletScreen &&
tangemSdkManager.canUseBiometry &&
store.state.navigationState.backStack.lastOrNull() == AppScreen.Wallet
) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
}
}
private fun changeWallet(state: WalletState, lifecycleScope: LifecycleCoroutineScope) {
when {
state.canSaveUserWallets -> {
Analytics.send(MainScreen.ButtonMyWallets())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
else -> {
Analytics.send(MainScreen.ButtonScanCard())
store.dispatch(
WalletAction.Scan(
onScanSuccessEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main),
scope = lifecycleScope,
),
)
}
}
}
}

View file

@ -1,184 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
class WarningsMiddleware {
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
when (action) {
is WalletAction.Warnings.Update -> setWarningMessages()
is WalletAction.Warnings.CheckIfNeeded -> {
showCardWarningsIfNeeded(globalState)
val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow()
if (readyToShow) addWarningMessage(warning = WarningMessagesManager.appRatingWarning, autoUpdate = true)
}
is WalletAction.Warnings.CheckHashesCount.VerifyOnlineIfNeeded -> checkHashesCountOnlineIfNeeded()
is WalletAction.Warnings.CheckHashesCount.SaveCardId -> {
val cardId = globalState?.scanResponse?.card?.cardId
cardId?.let { preferencesStorage.usedCardsPrefStorage.scanned(it) }
}
is WalletAction.Warnings.AppRating.RemindLater -> {
preferencesStorage.appRatingLaunchObserver.applyDelayedShowing()
}
is WalletAction.Warnings.AppRating.SetNeverToShow -> {
preferencesStorage.appRatingLaunchObserver.setNeverToShow()
}
is WalletAction.Warnings.CheckRemainingSignatures -> {
if (action.remainingSignatures != null &&
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
// store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format)
addWarningMessage(
warning = WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
autoUpdate = true,
)
}
}
is WalletAction.Warnings.AppRating,
is WalletAction.Warnings.CheckHashesCount,
is WalletAction.Warnings.Set,
-> Unit
}
}
fun tryToShowAppRatingWarning(hasNonZeroWallets: Boolean) {
if (hasNonZeroWallets) {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
addWarningMessage(WarningMessagesManager.appRatingWarning, true)
}
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
globalState?.scanResponse?.let { scanResponse ->
val card = scanResponse.card
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.isTestCard) {
addWarningMessage(WarningMessagesManager.testCardWarning, autoUpdate = true)
return@let
}
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
addWarningMessage(WarningMessagesManager.devCardWarning)
} else if (!preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) {
checkIfWarningNeeded(scanResponse)?.let { warning -> addWarningMessage(warning) }
}
if (card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release && !globalState.cardVerifiedOnline) {
addWarningMessage(WarningMessagesManager.onlineVerificationFailed)
}
if (scanResponse.isDemoCard()) {
addWarningMessage(WarningMessagesManager.demoCardWarning)
}
setWarningMessages()
}
}
private fun showWarningLowRemainingSignaturesIfNeeded(card: CardDTO) {
val remainingSignatures = card.wallets.firstOrNull()?.remainingSignatures
if (remainingSignatures != null &&
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
addWarningMessage(WarningMessagesManager.remainingSignaturesNotEnough(remainingSignatures))
}
}
private fun checkIfWarningNeeded(scanResponse: ScanResponse): WarningMessage? {
if (scanResponse.cardTypesResolver.isTangemTwins() || scanResponse.isDemoCard()) return null
if (scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
val isBackupForbidden = with(scanResponse.card.settings) { !(isBackupAllowed || isHDWalletAllowed) }
return if (scanResponse.card.hasSignedHashes() && isBackupForbidden) {
WarningMessagesManager.signedHashesMultiWalletWarning
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
null
}
}
return if (scanResponse.card.hasSignedHashes()) {
WarningMessagesManager.alreadySignedHashesWarning
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
null
}
}
private fun checkHashesCountOnlineIfNeeded() {
val alreadySignedHashesWarning = WarningMessagesManager.alreadySignedHashesWarning
val manager = store.state.globalState.warningManager ?: return
if (manager.containsWarning(alreadySignedHashesWarning)) return
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) return
val scanResponse = store.state.globalState.scanResponse
val card = scanResponse?.card
if (card == null || preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) return
if (scanResponse.cardTypesResolver.isTangemTwins() || scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
return
}
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator
scope.launch {
val signedHashes = card.wallets.firstOrNull()?.totalSignedHashes ?: 0
val result = validator?.validateSignatureCount(signedHashes)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
}
is SimpleResult.Failure ->
if (signedHashes > 0 || result.error is BlockchainSdkError.SignatureCountNotMatched) {
alreadySignedHashesWarning.isHidden = false
addWarningMessage(alreadySignedHashesWarning, true)
}
null -> Unit
}
}
}
}
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) setWarningMessages()
}
private fun setWarningMessages() {
store.dispatchOnMain(WalletAction.Warnings.Set(getWarnings()))
}
private fun getWarnings(): List<WarningMessage> {
val warningManager = store.state.globalState.warningManager ?: return emptyList()
return warningManager.getWarnings(
WarningMessage.Location.MainScreen,
store.state.walletState.blockchains,
)
}
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.wallet.redux.models
import com.tangem.blockchain.common.Amount
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.wallet.R
sealed interface WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>) : WalletDialog
object SignedHashesMultiWalletDialog : WalletDialog
data class ChooseTradeActionDialog(
val buyAllowed: Boolean,
val sellAllowed: Boolean,
val swapAllowed: Boolean,
) : WalletDialog
data class CurrencySelectionDialog(
val currenciesList: List<FiatCurrency>,
val currentAppCurrency: FiatCurrency,
) : WalletDialog
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,
) : WalletDialog {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
}
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String,
) : WalletDialog {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
data class RussianCardholdersWarningDialog(val data: Data?) : WalletDialog {
data class Data(val topUpUrl: String)
}
}

View file

@ -1,14 +0,0 @@
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

@ -1,31 +0,0 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
class MultiWalletReducer {
@Suppress("LongMethod", "ComplexMethod")
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.SelectWallet -> {
state.copy(selectedCurrency = action.currency)
}
is WalletAction.MultiWallet.TryToRemoveWallet -> state
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(
missingDerivations = action.blockchains,
)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading)
is WalletAction.MultiWallet.CheckForBackupWarning -> state.copy(
showBackupWarning = action.card.settings.isBackupAllowed &&
action.card.backupStatus == CardDTO.BackupStatus.NoBackup,
)
else -> state
}
}
}

View file

@ -1,172 +0,0 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.userwallets.Artwork
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
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.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.userWalletsListManager
import org.rekotlin.Action
object WalletReducer {
fun reduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState =
internalReduce(action, state, appStateHolder)
}
@Suppress("LongMethod", "ComplexMethod")
private fun internalReduce(action: Action, state: AppState, appStateHolder: AppStateHolder): WalletState {
val multiWalletReducer = MultiWalletReducer()
val appCurrencyReducer = AppCurrencyReducer()
if (action !is WalletAction) return state.walletState
var newState = state.walletState
when (action) {
is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState)
is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState)
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
)
}
is TapError.UnknownBlockchain -> {
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.UnknownBlockchain,
)
}
else -> {
newState = newState.copy(
state = ProgressState.Error,
)
}
}
}
is WalletAction.LoadData -> {
newState = newState.copy(
state = ProgressState.Loading,
error = null,
)
}
is WalletAction.LoadData.Refresh -> {
newState = newState.copy(
state = ProgressState.Refreshing,
error = null,
)
}
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
is WalletAction.UserWalletChanged -> with(action.userWallet) {
val card = scanResponse.card
newState = WalletState(
isMultiwalletAllowed = isMultiCurrency,
cardImage = Artwork(
artworkId = artworkUrl,
),
isTestnet = card.isTestCard,
state = ProgressState.Loading,
showBackupWarning = isMultiCurrency &&
card.settings.isBackupAllowed &&
card.backupStatus == CardDTO.BackupStatus.NoBackup,
walletCardsCount = card.findCardsCount(),
walletsStores = newState.walletsStores,
totalBalance = if (isMultiCurrency) {
newState.totalBalance
} else {
null
},
)
}
is WalletAction.WalletStoresChanged -> {
newState = newState.copy(
walletsStores = action.walletStores,
selectedCurrency = findSelectedCurrency(
walletsStores = action.walletStores,
currentSelectedCurrency = newState.selectedCurrency,
isMultiWalletAllowed = newState.isMultiwalletAllowed,
),
)
}
is WalletAction.TotalFiatBalanceChanged -> {
newState = newState.copy(
totalBalance = action.balance,
)
}
is WalletAction.LoadData.Success -> {
newState = newState.copy(state = ProgressState.Done)
}
is WalletAction.UpdateCanSaveUserWallets -> {
newState = newState.copy(canSaveUserWallets = action.canSaveUserWallets)
}
is WalletAction.SetArtworkUrl -> {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync?.walletId
if (selectedUserWallet == action.userWalletId) {
newState = newState.copy(
cardImage = Artwork(artworkId = action.url),
)
}
}
else -> Unit
}
appStateHolder.walletState = newState
return newState
}
fun findSelectedCurrency(
walletsStores: List<WalletStoreModel>,
currentSelectedCurrency: Currency?,
isMultiWalletAllowed: Boolean,
): Currency? = if (isMultiWalletAllowed) {
currentSelectedCurrency
} else {
walletsStores.firstOrNull()
?.walletsData
?.firstOrNull()
?.currency
}
private fun CardDTO.findCardsCount(): Int? {
return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc()
}
fun Wallet.createAddressesData(): List<WalletDataModel.AddressData> {
val listOfAddressData = mutableListOf<WalletDataModel.AddressData>()
// put a defaultAddress at the first place
addresses.forEach {
val addressData = WalletDataModel.AddressData(
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value),
)
if (it.type == AddressType.Default) {
listOfAddressData.add(0, addressData)
} else {
listOfAddressData.add(addressData)
}
}
return listOfAddressData
}
private fun handleCheckSignedHashesActions(action: WalletAction.Warnings, state: WalletState): WalletState {
return when (action) {
is WalletAction.Warnings.Set -> state.copy(mainWarningsList = action.warningList)
else -> state
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.tap.features.wallet.redux.utils
const val UNKNOWN_AMOUNT_SIGN = ""
const val ROUGH_SIGN = ""
const val CAN_BE_LOWER_SIGN = "<"

View file

@ -1,123 +0,0 @@
package com.tangem.tap.features.wallet.ui
import androidx.annotation.IdRes
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount
import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.CardBalanceBinding
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
class BalanceWidget(
private val binding: CardBalanceBinding,
private val fragment: WalletFragment,
private val blockchainWalletData: WalletDataModel,
private val tokenWalletData: WalletDataModel?,
) {
@Suppress("LongMethod", "ComplexMethod")
fun setup() {
when (blockchainWalletData.status) {
is WalletDataModel.Loading -> {
with(binding) {
lBalance.root.show()
lBalanceError.root.hide()
lBalance.tvFiatAmount.hide()
lBalance.tvCurrency.text = blockchainWalletData.currency.currencyName
lBalance.tvAmount.text = ""
}
showStatus(R.id.tv_status_loading)
if (tokenWalletData != null) {
showBalanceWithToken(blockchainWalletData, false)
} else {
showBalanceWithoutToken(blockchainWalletData, false)
}
}
is WalletDataModel.VerifiedOnline,
is WalletDataModel.TransactionInProgress,
-> with(binding.lBalance) {
root.show()
binding.lBalanceError.root.hide()
val statusView = if (blockchainWalletData.status is WalletDataModel.VerifiedOnline) {
R.id.tv_status_verified
} else {
// tvStatusError.text = fragment.getText(R.string.wallet_balance_tx_in_progress)
R.id.group_error
}
showStatus(statusView)
// tvStatusErrorMessage.hide()
if (tokenWalletData != null) {
showBalanceWithToken(blockchainWalletData, true)
} else {
showBalanceWithoutToken(blockchainWalletData, true)
}
}
is WalletDataModel.Unreachable -> with(binding.lBalance) {
root.show()
binding.lBalanceError.root.hide()
tvFiatAmount.hide()
groupBaseCurrency.hide()
val currency = tokenWalletData?.currency?.currencySymbol
?: blockchainWalletData.currency.currencyName
tvCurrency.text = currency
tvAmount.text = ""
// tvStatusErrorMessage.text = blockchainWalletData.status.errorMessage
// TODO: Delete with WalletFeatureToggles
// tvStatusError.text = fragment.getString(R.string.wallet_balance_blockchain_unreachable)
showStatus(R.id.group_error)
// tvStatusErrorMessage.show(!blockchainWalletData.status.errorMessage.isNullOrBlank())
}
is WalletDataModel.NoAccount -> with(binding.lBalanceError) {
binding.lBalance.root.hide()
binding.lBalanceError.root.show()
tvErrorTitle.text = fragment.getText(R.string.wallet_error_no_account)
tvErrorDescriptions.text =
fragment.getString(
R.string.no_account_generic,
blockchainWalletData.status.amountToCreateAccount,
blockchainWalletData.currency.currencySymbol,
)
}
else -> {}
}
}
private fun showStatus(@IdRes viewRes: Int) = with(binding.lBalance) {
// groupError.show(viewRes == R.id.group_error)
tvStatusLoading.show(viewRes == R.id.tv_status_loading)
tvStatusVerified.show(viewRes == R.id.tv_status_verified)
}
private fun showBalanceWithToken(data: WalletDataModel, showAmount: Boolean) = with(binding.lBalance) {
groupBaseCurrency.show()
tvCurrency.text = tokenWalletData?.currency?.currencySymbol
tvBaseCurrency.text = data.currency.currencyName
tvAmount.text = if (showAmount) tokenWalletData?.getFormattedCryptoAmount() else ""
tvBaseAmount.text = if (showAmount) data.getFormattedCryptoAmount() else ""
if (showAmount) {
tvFiatAmount.show()
tvFiatAmount.text = tokenWalletData?.getFormattedFiatAmount(store.state.globalState.appCurrency)
}
}
private fun showBalanceWithoutToken(data: WalletDataModel, showAmount: Boolean) = with(binding.lBalance) {
groupBaseCurrency.hide()
tvCurrency.text = data.currency.currencyName
tvAmount.text = if (showAmount) data.getFormattedCryptoAmount() else ""
if (showAmount) {
tvFiatAmount.show()
tvFiatAmount.text = data.getFormattedFiatAmount(store.state.globalState.appCurrency)
}
}
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.wallet.ui
import android.view.View
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.wallet.R
object MultipleAddressUiHelper {
private val blockchainsSupportingSplit = listOf(
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.Litecoin,
Blockchain.BitcoinCash,
Blockchain.Cardano,
)
fun typeToId(type: AddressType, blockchain: Blockchain): Int {
return if (blockchain in blockchainsSupportingSplit) {
if (type == AddressType.Legacy) {
R.id.chip_legacy
} else {
R.id.chip_default
}
} else {
View.NO_ID
}
}
fun idToType(id: Int, blockchain: Blockchain): AddressType? {
return when (blockchain) {
in blockchainsSupportingSplit -> {
when (id) {
R.id.chip_default -> AddressType.Default
R.id.chip_legacy -> AddressType.Legacy
else -> null
}
}
else -> null
}
}
}

View file

@ -1,509 +0,0 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.*
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
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.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.sdk.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.analytics.events.DetailsScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.utils.SafeStoreSubscriber
import com.tangem.tap.domain.model.WalletDataModel
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.models.WalletWarning
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.images.load
import com.tangem.tap.features.wallet.ui.test.TestWallet
import com.tangem.tap.features.wallet.ui.utils.*
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.tap.walletCurrenciesManager
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
/**
* Wallet details fragment - use only for MultiWallet
*/
// TODO: Delete with WalletFeatureToggles
@Suppress("LargeClass", "MagicNumber")
@Deprecated(message = "Used only in old wallet screen")
@AndroidEntryPoint
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeStoreSubscriber<WalletState> {
@Inject
lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
private val walletDataWatcher: ModelWatcher<WalletDataModel> = modelWatcher {
val addressCardStrategy: DiffStrategy<WalletDataModel> = { old, new ->
old.currency != new.currency || old.walletAddresses != new.walletAddresses
}
WalletDataModel::currency {
handleCurrencyIcon(it)
}
WalletDataModel::walletAddresses { walletAddresses ->
setupCopyAndShareButtons(walletAddresses?.selectedAddress?.address)
}
WalletDataModel::currency { currency ->
setupCurrency(currency)
}
watch({ it }, addressCardStrategy) { walletData ->
setupAddressCard(
shouldShowMultipleAddress = walletData.shouldShowMultipleAddress(),
selectedAddress = walletData.walletAddresses?.selectedAddress,
currency = walletData.currency,
)
}
}
private val walletStateWatcher: ModelWatcher<WalletState> = modelWatcher {
val walletDataStrategy: DiffStrategy<WalletState> = { old, new ->
new.walletsStores.isNotEmpty() &&
new.selectedCurrency != null &&
(old.selectedCurrency != new.selectedCurrency || old.walletsStores != new.walletsStores)
}
watch({ it }, walletDataStrategy) { state ->
val selectedWallet = state.selectedWalletData
if (selectedWallet != null) {
setupBalanceData(selectedWallet)
setupSwipeRefresh(selectedWallet)
walletDataWatcher.invoke(selectedWallet)
val walletStore = state.getWalletStore(state.selectedCurrency)
if (walletStore != null) {
handleWarnings(
selectedWallet.assembleWarnings(
blockchainAmount = walletStore.blockchainWalletData.status.amount,
blockchainWalletRent = walletStore.walletRent,
),
)
}
}
}
(WalletState::selectedWalletData or WalletState::isExchangeServiceFeatureOn) { state ->
val selectedWallet = state.selectedWalletData
if (selectedWallet != null) {
val blockchainAmount: BigDecimal = state.getBlockchainAmount(selectedWallet.currency)
setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn, blockchainAmount)
}
}
(WalletState::state or WalletState::error) { state ->
setupNoInternetHandling(state.state, state.error)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
Analytics.send(DetailsScreen.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
}
},
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
setupTransactionsRecyclerView()
setupButtons()
setupWarningsRecyclerView()
setupTestActionButton()
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state -> state.select(AppState::walletState) }
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onDestroyView() {
super.onDestroyView()
clearWatchers()
}
private fun setupTransactionsRecyclerView() = with(binding) {
pendingTransactionAdapter = PendingTransactionsAdapter()
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
rvPendingTransaction.adapter = pendingTransactionAdapter
}
private fun setupWarningsRecyclerView() = with(binding) {
warningMessagesAdapter = WalletDetailWarningMessagesAdapter()
rvWarningMessages.layoutManager = LinearLayoutManager(requireContext())
rvWarningMessages.adapter = warningMessagesAdapter
rvWarningMessages.addItemDecoration(SpaceItemDecoration.vertical(8f))
}
private fun setupButtons() {
binding.rowButtons.onSendClick = { store.dispatch(WalletAction.Send()) }
}
private fun setupTestActionButton() {
view?.findViewById<View>(R.id.l_balance)?.let { view ->
TestActions.initFor(view = view, actions = TestWallet.solanaRentExemptWarning())
}
}
override fun newStateOnMain(state: WalletState) {
if (activity == null || view == null) return
if (state.selectedWalletData == null) return
walletStateWatcher.invoke(state)
updateViewMeasurements()
}
private fun updateViewMeasurements() {
val tvFiatAmount = binding.lWalletDetails.lBalance.tvFiatAmount
val paddingStart = if (tvFiatAmount.text == UNKNOWN_AMOUNT_SIGN) 16f else 12f
tvFiatAmount.setPadding(
tvFiatAmount.dpToPx(paddingStart).toInt(),
tvFiatAmount.paddingTop,
tvFiatAmount.paddingEnd,
tvFiatAmount.paddingBottom,
)
}
private fun setupCurrency(currency: Currency) = with(binding) {
tvCurrencyTitle.text = currency.currencyName
if (currency is Currency.Token) {
tvCurrencySubtitle.text = tvCurrencySubtitle.getString(
R.string.wallet_currency_subtitle,
currency.blockchain.fullName,
)
tvCurrencySubtitle.show()
} else {
tvCurrencySubtitle.hide()
}
}
private fun setupSwipeRefresh(walletData: WalletDataModel) {
binding.srlWalletDetails.setOnRefreshListener {
if (walletData.status !is WalletDataModel.Loading) {
Analytics.send(Token.Refreshed())
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard {
Timber.e("Unable to refresh wallet details screen, no user wallet selected")
return@setOnRefreshListener
}
binding.srlWalletDetails.isRefreshing = true
lifecycleScope.launch(Dispatchers.Default) {
walletCurrenciesManager.update(selectedUserWallet, walletData.currency).doOnResult {
withMainContext {
binding.srlWalletDetails.isRefreshing = false
}
}
}
}
}
}
private fun setupCopyAndShareButtons(walletAddress: String?) {
binding.lWalletDetails.btnCopy.setOnClickListener {
if (walletAddress != null) store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext()))
}
binding.lWalletDetails.btnShare.setOnClickListener {
if (walletAddress != null) store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext()))
}
}
private fun setupButtonsRow(
selectedWallet: WalletDataModel,
isExchangeServiceFeatureOn: Boolean,
blockchainAmount: BigDecimal,
) {
val exchangeManager = store.state.globalState.exchangeManager
binding.rowButtons.apply {
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager),
sellAllowed = selectedWallet.isAvailableToSell(exchangeManager),
swapAllowed = selectedWallet.isAvailableToSwap(
swapFeatureToggleManager = swapFeatureToggleManager,
swapInteractor = swapInteractor,
isSingleWallet = false,
),
),
)
}
}
val actions = selectedWallet.getAvailableActions(
swapInteractor = swapInteractor,
exchangeManager = exchangeManager,
swapFeatureToggleManager = swapFeatureToggleManager,
isSingleWallet = false,
)
binding.rowButtons.updateButtonsVisibility(
actions = actions,
exchangeServiceFeatureOn = isExchangeServiceFeatureOn,
sendAllowed = selectedWallet.mainButton(blockchainAmount).enabled,
)
}
private fun handleWarnings(warnings: List<WalletWarning>) = with(binding) {
val converter = WalletWarningConverter(requireContext())
val warningDetails = warnings.map { converter.convert(it) }
warningMessagesAdapter.submitList(warningDetails)
rvWarningMessages.show(warningDetails.isNotEmpty())
}
private fun handleCurrencyIcon(currency: Currency) = with(binding.lWalletDetails.lBalance) {
ivCurrency.load(
currency = currency,
derivationStyle = store.state.globalState.scanResponse
?.derivationStyleProvider?.getDerivationStyle(),
)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
val knownTransactions = pendingTransactions.filterNot { it.type == PendingTransactionType.Unknown }
pendingTransactionAdapter.submitList(knownTransactions)
binding.rvPendingTransaction.show(knownTransactions.isNotEmpty())
}
private fun setupAddressCard(
shouldShowMultipleAddress: Boolean,
selectedAddress: WalletDataModel.AddressData?,
currency: Currency,
) = with(binding.lWalletDetails) {
if (selectedAddress == null) return@with
setupAddressTypeChips(shouldShowMultipleAddress, selectedAddress, currency)
tvAddress.text = selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(selectedAddress.exploreUrl, requireContext()))
}
ivQrCode.setImageBitmap(selectedAddress.shareUrl.toQrCode())
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: WalletDataModel.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, currency.blockchain)
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()
}
}
private fun setupNoInternetHandling(progressState: ProgressState, errorType: ErrorType?) {
if (progressState == ProgressState.Error) {
if (errorType == ErrorType.NoInternetConnection) {
binding.srlWalletDetails.isRefreshing = false
(activity as? SnackbarHandler)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry,
) { store.dispatch(WalletAction.LoadData) }
}
} else {
(activity as? SnackbarHandler)?.dismissSnackbar()
}
}
private fun setupBalanceData(walletData: WalletDataModel) = with(binding.lWalletDetails) {
when (val status = walletData.status) {
is WalletDataModel.Loading -> {
lBalanceError.root.hide()
lBalance.root.show()
lBalance.groupBalance.show()
lBalance.tvError.hide()
lBalance.tvAmount.text = walletData.getFormattedCryptoAmount()
lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency)
lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading)
}
is WalletDataModel.VerifiedOnline,
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,
-> {
lBalanceError.root.hide()
lBalance.root.show()
lBalance.groupBalance.show()
lBalance.tvError.hide()
lBalance.tvAmount.text = walletData.getFormattedCryptoAmount()
lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency)
when (status) {
is WalletDataModel.VerifiedOnline,
is WalletDataModel.SameCurrencyTransactionInProgress,
-> {
lBalance.tvStatus.setVerifiedBalanceStatus(R.string.wallet_balance_verified)
showPendingTransactionsIfPresent(status.pendingTransactions)
}
is WalletDataModel.TransactionInProgress -> {
lBalance.tvStatus.setWarningStatus(R.string.wallet_balance_tx_in_progress)
showPendingTransactionsIfPresent(status.pendingTransactions)
}
else -> Unit
}
}
is WalletDataModel.Unreachable -> {
lBalanceError.root.hide()
lBalance.root.show()
lBalance.groupBalance.hide()
lBalance.tvError.show()
// TODO: Delete with WalletFeatureToggles
// lBalance.tvError.setWarningStatus(
// R.string.wallet_balance_blockchain_unreachable,
// status.errorMessage,
// )
}
is WalletDataModel.NoAccount -> {
lBalance.root.hide()
lBalanceError.root.show()
lBalanceError.tvErrorTitle.text = getText(R.string.wallet_error_no_account)
lBalanceError.tvErrorDescriptions.text =
getString(
R.string.no_account_generic,
status.amountToCreateAccount,
walletData.currency.currencySymbol,
)
}
else -> Unit
}
}
@Deprecated("Deprecated in Java")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_remove -> {
store.state.walletState.selectedWalletData?.let { walletData ->
store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData.currency))
true
}
false
}
else -> super.onOptionsItemSelected(item)
}
}
@Deprecated(
message = "Deprecated in Java",
replaceWith = ReplaceWith("inflater.inflate(R.menu.menu_wallet_details, menu)", "com.tangem.wallet.R"),
)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
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)
}
private fun TextView.setVerifiedBalanceStatus(mainMessage: Int) {
setStatus(getString(mainMessage), R.color.accent, R.drawable.ic_ok)
}
private fun TextView.setLoadingStatus(mainMessage: Int) {
setStatus(getString(mainMessage), R.color.darkGray4, null)
}
private fun TextView.setStatus(text: String, @ColorRes color: Int, @DrawableRes drawable: Int?) {
this.text = text
setTextColor(getColor(color))
setCompoundDrawablesWithIntrinsicBounds(drawable ?: 0, 0, 0, 0)
}
}

View file

@ -1,326 +0,0 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import coil.size.Scale
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.utils.SafeStoreSubscriber
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<WalletState> {
@Inject
lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
private lateinit var warningsAdapter: WarningMessagesAdapter
private val binding: FragmentWalletBinding by viewBinding(FragmentWalletBinding::bind)
private var walletView: WalletView = MultiWalletView()
private val viewModel by viewModels<WalletViewModel>()
private val totalBalanceWatcher = modelWatcher {
(WalletState::totalBalance) { totalBalance ->
totalBalance?.let {
viewModel.onBalanceLoaded(totalBalance)
store.state.globalState.topUpController?.totalBalanceStateChanged(it)
}
}
}
private val isNetworkConnectionError = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.lifecycle?.addObserver(viewModel)
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(WalletAction.PopBackToInitialScreen)
}
},
)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_secondary)
subscribeOnNetworkStateChanging()
store.subscribe(this) { state ->
state.select { it.walletState }
}
walletView.setFragment(this, binding)
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
walletView.removeFragment()
}
override fun onDestroy() {
walletView.onDestroyFragment()
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener(
OneTouchClickListener {
store.dispatch(WalletAction.ChangeWallet(scope = requireActivity().lifecycleScope))
},
)
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
addCustomActionOnCard()
}
private fun addCustomActionOnCard() {
if (!BuildConfig.TEST_ACTION_ENABLED) return
binding.ivCard.setOnClickListener {
printScanResponseState()
printWalletState()
}
}
@Suppress("MagicNumber")
private fun setupWarningsRecyclerView() {
warningsAdapter = WarningMessagesAdapter()
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
with(binding) {
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
}
}
@Suppress("ComplexMethod")
override fun newStateOnMain(state: WalletState) {
if (activity == null || view == null) return
when {
state.isMultiwalletAllowed && walletView !is MultiWalletView -> {
walletView.onViewDestroy()
walletView = MultiWalletView()
walletView.changeWalletView(this, binding)
}
!state.isMultiwalletAllowed && walletView !is SingleWalletView -> {
walletView.onViewDestroy()
walletView = SingleWalletView()
walletView.changeWalletView(this, binding)
}
else -> {} // we keep the same view unless we scan a card that requires a different view
}
totalBalanceWatcher.invoke(state)
walletView.swapInteractor = swapInteractor
walletView.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state)
if (binding.toolbar.menu.findItem(R.id.details_menu) == null) {
binding.toolbar.inflateMenu(R.menu.menu_wallet)
}
setupCardImage(state)
showWarningsIfPresent(state.mainWarningsList)
setupPullToRefreshLayout(state)
binding.toolbar.setNavigationIcon(
if (state.canSaveUserWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24,
)
// showLearn2earnView()
}
// private fun showLearn2earnView() {
// val isShowing = learn2earnViewModel.uiState.mainScreenState.isVisible
// if (!isShowing) return
//
// binding.composeLearnToEarnContainer.show(true) { binding.llWarnings.beginDelayedTransition() }
// binding.composeLearnToEarnContainer.apply {
// setViewCompositionStrategy(
// strategy = ViewCompositionStrategy.DisposeOnLifecycleDestroyed(
// lifecycle = [REDACTED_EMAIL],
// ),
// )
// setContent {
// TangemTheme {
// Learn2earnMainPageScreen(learn2earnViewModel.uiState)
// }
// }
// }
// }
private fun setupPullToRefreshLayout(state: WalletState) {
setupErrorPullToRefreshState(state)
binding.pullToRefreshLayout.isRefreshing = state.state == ProgressState.Refreshing
binding.pullToRefreshLayout.setOnRefreshListener {
if (state.state != ProgressState.Loading && state.state != ProgressState.Refreshing) {
refreshWalletData()
}
}
}
private fun setupErrorPullToRefreshState(state: WalletState) {
if (state.state == ProgressState.Error) {
when (state.error) {
ErrorType.NoInternetConnection -> {
isNetworkConnectionError.value = true
binding.pullToRefreshLayout.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry,
)
// because was added logic of autoupdate mainscreen data, remove retry
// TODO("remove comment after release 4.6")
// { store.dispatch(WalletAction.LoadData) }
}
else -> isNetworkConnectionError.value = false
}
} else {
isNetworkConnectionError.value = false
(activity as? MainActivity)?.dismissSnackbar()
}
}
private fun refreshWalletData() {
Analytics.send(Portfolio.Refreshed())
store.dispatch(WalletAction.LoadData.Refresh)
// learn2earnViewModel.onMainScreenRefreshed()
}
private fun showWarningsIfPresent(warnings: List<WarningMessage>) {
warningsAdapter.submitList(warnings)
binding.rvWarningMessages.show(warnings.isNotEmpty())
}
private fun setupCardImage(state: WalletState) {
binding.ivCard.load(state.cardImage?.artworkId) {
scale(Scale.FIT)
crossfade(enable = true)
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
private fun subscribeOnNetworkStateChanging() {
viewLifecycleOwner.lifecycleScope.launch {
networkConnectionManager.isOnlineFlow
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.distinctUntilChanged()
.collect { isOnline ->
if (isOnline) {
(activity as? MainActivity)?.dismissSnackbar()
} else {
isNetworkConnectionError.value = true
binding.pullToRefreshLayout.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry,
)
}
if (isOnline && isNetworkConnectionError.value) {
refreshWalletData()
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true
}
else -> super.onOptionsItemSelected(item)
}
}
@Deprecated(
message = "Deprecated in Java",
replaceWith = ReplaceWith(
"if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.menu_wallet, menu)",
"com.tangem.tap.store",
"com.tangem.wallet.R",
),
)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_wallet, menu)
}
}

View file

@ -1,132 +0,0 @@
package com.tangem.tap.features.wallet.ui
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store
import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// TODO: Kill me, please
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
internal class WalletViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), StoreSubscriber<UserWalletsListManager?>, DefaultLifecycleObserver {
private var observeWalletStoresUpdatesJob: Job? = null
set(value) {
field?.cancel()
field = value
}
private val walletAnalyticsEventsMapper = WalletAnalyticsEventsMapper()
init {
subscribeToUserWalletsListManagerUpdates()
}
override fun onCleared() {
store.unsubscribe(this)
}
override fun newState(state: UserWalletsListManager?) {
// Restarting observing of wallet store updates when the manager changes
if (state != null) {
bootstrapSelectedWalletStoresChanges(state)
}
}
override fun onCreate(owner: LifecycleOwner) {
launch()
val scanResponse = store.state.globalState.scanResponse
if (scanResponse != null) {
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
val signInType = store.state.signInState.type
if (currency != null && signInType != null) {
analyticsEventHandler.send(
Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
}
override fun onStart(owner: LifecycleOwner) {
analyticsEventHandler.send(MainScreen.ScreenOpened())
}
fun onBalanceLoaded(totalBalance: TotalFiatBalance?) {
if (totalBalance != null) {
walletAnalyticsEventsMapper.convert(totalBalance)?.let { balanceParam ->
analyticsEventHandler.send(
Basic.BalanceLoaded(
balance = balanceParam,
),
)
}
}
}
private fun launch() {
val manager = store.state.globalState.userWalletsListManager
if (manager != null) {
bootstrapSelectedWalletStoresChanges(manager)
}
bootstrapShowSaveWalletIfNeeded()
}
@OptIn(FlowPreview::class)
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet
.map { it.walletId }
.flatMapLatest(walletStoresManager::get)
.debounce { walletStores ->
if (walletStores.isNotEmpty()) WALLET_STORES_DEBOUNCE_TIMEOUT else 0
}
.onEach { walletStores ->
store.dispatchOnMain(WalletAction.WalletStoresChanged(walletStores))
}
.launchIn(viewModelScope)
}
private fun bootstrapShowSaveWalletIfNeeded() {
viewModelScope.launch {
delay(timeMillis = 1_800)
store.dispatchOnMain(WalletAction.ShowSaveWalletIfNeeded)
}
}
private fun subscribeToUserWalletsListManagerUpdates() {
store.subscribe(this) { appState ->
appState
.skip { old, new ->
old.globalState.userWalletsListManager == new.globalState.userWalletsListManager
}
.select { it.globalState.userWalletsListManager }
}
}
companion object {
private const val WALLET_STORES_DEBOUNCE_TIMEOUT = 100L
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.tap.features.wallet.ui
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.WalletWarningDescription
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
class WalletWarningConverter(
private val context: Context,
) : ModuleMessageConverter<WalletWarning, WalletWarningDescription> {
override fun convert(message: WalletWarning): WalletWarningDescription {
// val warningMessage = when (message) {
// is WalletWarning.ExistentialDeposit -> {
// context.getString(
// R.string.warning_existential_deposit_message,
// message.currencyName,
// message.edStringValueWithSymbol,
// )
// }
// is WalletWarning.BalanceNotEnoughForFee -> {
// context.getString(
// R.string.token_details_send_blocked_fee_format,
// message.currencyName,
// message.blockchainFullName,
// message.currencyName,
// message.blockchainFullName,
// message.blockchainSymbol,
// )
// }
// is WalletWarning.TransactionInProgress -> {
// context.getString(
// R.string.token_details_send_blocked_tx_format,
// message.currencyName,
// )
// }
// is WalletWarning.Rent -> {
// context.getString(
// R.string.solana_rent_warning,
// message.walletRent.rent,
// message.walletRent.exemptionAmount,
// )
// }
// }
return WalletWarningDescription(context.getString(R.string.common_warning), "")
}
}

View file

@ -1,74 +0,0 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemPendingTransactionBinding
class PendingTransactionsAdapter :
ListAdapter<PendingTransaction, PendingTransactionsAdapter.TransactionsViewHolder>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionsViewHolder {
val binding = ItemPendingTransactionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
return TransactionsViewHolder(binding)
}
override fun onBindViewHolder(holder: TransactionsViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<PendingTransaction>() {
override fun areContentsTheSame(oldItem: PendingTransaction, newItem: PendingTransaction) = oldItem == newItem
override fun areItemsTheSame(oldItem: PendingTransaction, newItem: PendingTransaction) = oldItem == newItem
}
class TransactionsViewHolder(val binding: ItemPendingTransactionBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(transaction: PendingTransaction) {
if (transaction.type == PendingTransactionType.Unknown) {
binding.root.hide()
}
val transactionDescriptionRes = when (transaction.type) {
PendingTransactionType.Incoming -> R.string.wallet_pending_tx_receiving
PendingTransactionType.Outgoing -> R.string.wallet_pending_tx_sending
PendingTransactionType.Unknown -> return
}
val transactionAddressRes = when (transaction.type) {
PendingTransactionType.Incoming -> R.string.wallet_pending_tx_receiving_address_format
PendingTransactionType.Outgoing -> R.string.wallet_pending_tx_sending_address_format
PendingTransactionType.Unknown -> return
}
val image = when (transaction.type) {
PendingTransactionType.Incoming -> R.drawable.ic_arrow_left
PendingTransactionType.Outgoing -> R.drawable.ic_arrow_right_20
PendingTransactionType.Unknown -> return
}
binding.tvPendingTransaction.text =
binding.root.getString(transactionDescriptionRes).let { "$it " }
transaction.amountValueUi?.let { binding.tvPendingTransactionAmount.text = "$it " }
binding.tvPendingTransactionCurrency.text = transaction.currency
if (transaction.address != null) {
binding.tvPendingTransactionAddress.text =
binding.root.getString(transactionAddressRes, transaction.address)
}
binding.ivPendingTransaction.setImageDrawable(binding.root.context.getDrawableCompat(image))
}
}
}

View file

@ -1,110 +0,0 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.images.load
import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount
import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount
import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatRate
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemCurrencyWalletBinding
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
class WalletAdapter : ListAdapter<WalletDataModel, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return currentList[position].currency.currencySymbol.hashCode().toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder {
val layout = ItemCurrencyWalletBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
return WalletsViewHolder(layout)
}
override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<WalletDataModel>() {
override fun areContentsTheSame(oldItem: WalletDataModel, newItem: WalletDataModel) = oldItem == newItem
override fun areItemsTheSame(oldItem: WalletDataModel, newItem: WalletDataModel) = oldItem == newItem
}
class WalletsViewHolder(val binding: ItemCurrencyWalletBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(wallet: WalletDataModel) = with(binding) {
val status = wallet.status
val fiatCurrency = store.state.globalState.appCurrency
val statusMessage = when (status) {
is WalletDataModel.TransactionInProgress -> {
root.getString(R.string.wallet_balance_tx_in_progress)
}
is WalletDataModel.Unreachable -> {
// TODO: Delete with WalletFeatureToggles
// root.getString(R.string.wallet_balance_blockchain_unreachable)
}
is WalletDataModel.MissedDerivation -> {
root.getString(R.string.wallet_balance_missing_derivation)
}
else -> null
}
if (status is WalletDataModel.Loading) {
lContent.root.hide()
lShimmer.root.veil()
} else {
lShimmer.root.unVeil()
lContent.root.show()
}
ivCurrency.load(
currency = wallet.currency,
derivationStyle = store.state.globalState.scanResponse
?.derivationStyleProvider?.getDerivationStyle(),
)
lContent.tvCurrency.text = wallet.currency.currencyName
lContent.tvAmountFiat.text = wallet.getFormattedFiatAmount(fiatCurrency)
lContent.tvAmount.text = wallet.getFormattedCryptoAmount()
lContent.tvStatus.isVisible = statusMessage != null
// lContent.tvStatus.text = statusMessage
lContent.tvExchangeRate.isVisible = statusMessage == null
lContent.tvExchangeRate.text = wallet.getFormattedFiatRate(
fiatCurrency = fiatCurrency,
noRateValue = root.getString(id = R.string.token_item_no_rate),
)
if (wallet.walletAddresses != null) {
cardWallet.setOnClickListener {
Analytics.send(Portfolio.TokenTapped())
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet.currency))
}
} else {
cardWallet.setOnClickListener(null)
}
}
}
}

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.features.wallet.models.WalletWarningDescription
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutWarningCardBinding
/**
[REDACTED_AUTHOR]
*/
class WalletDetailWarningMessagesAdapter :
ListAdapter<WalletWarningDescription, WalletDetailsWarningMessageVH>(DiffUtilCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletDetailsWarningMessageVH {
val inflater = LayoutInflater.from(parent.context)
val binding = LayoutWarningCardBinding.inflate(inflater, parent, false)
return WalletDetailsWarningMessageVH(binding)
}
override fun onBindViewHolder(holder: WalletDetailsWarningMessageVH, position: Int) {
holder.bind(currentList[position])
}
private class DiffUtilCallback : DiffUtil.ItemCallback<WalletWarningDescription>() {
override fun areContentsTheSame(oldItem: WalletWarningDescription, newItem: WalletWarningDescription) =
oldItem == newItem
override fun areItemsTheSame(oldItem: WalletWarningDescription, newItem: WalletWarningDescription) =
oldItem == newItem
}
}
class WalletDetailsWarningMessageVH(
val binding: LayoutWarningCardBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(warning: WalletWarningDescription) {
binding.warningCard.setCardBackgroundColor(binding.root.getColor(R.color.darkGray2))
setText(warning)
}
private fun setText(warning: WalletWarningDescription) = with(binding.warningContentContainer) {
tvTitle.text = warning.title
tvMessage.text = warning.message
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.wallet.ui.analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
class WalletAnalyticsEventsMapper : Converter<TotalFiatBalance, AnalyticsParam.CardBalanceState?> {
override fun convert(value: TotalFiatBalance): AnalyticsParam.CardBalanceState? {
return when (value) {
is TotalFiatBalance.Failed -> AnalyticsParam.CardBalanceState.BlockchainError
is TotalFiatBalance.Loaded -> when {
value.isWarning -> AnalyticsParam.CardBalanceState.CustomToken
value.amount.isGreaterThan(BigDecimal.ZERO) -> AnalyticsParam.CardBalanceState.Full
else -> AnalyticsParam.CardBalanceState.Empty
}
is TotalFiatBalance.Loading -> null
}
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.recyclerview.widget.*
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.blockchain.common.Amount
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.DialogWalletSendBinding
import com.tangem.wallet.databinding.ItemWalletAmountToSendBinding
class AmountToSendBottomSheetDialog(
context: Context,
private val dialog: WalletDialog.SelectAmountToSendDialog,
) : BottomSheetDialog(context) {
var binding: DialogWalletSendBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogWalletSendBinding.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
}
override fun show() {
super.show()
setOnDismissListener {
binding = null
store.dispatch(WalletAction.DialogAction.Hide)
}
binding!!.rvAmountsToSend.layoutManager = LinearLayoutManager(context)
val dividerItemDecoration = DividerItemDecoration(
ContextThemeWrapper(binding!!.root.context, R.style.AppTheme),
DividerItemDecoration.VERTICAL,
)
binding!!.rvAmountsToSend.addItemDecoration(dividerItemDecoration)
val viewAdapter = ChooseAmountAdapter()
binding!!.rvAmountsToSend.adapter = viewAdapter
viewAdapter.submitList(dialog.amounts)
}
}
private class ChooseAmountAdapter : ListAdapter<Amount, ChooseAmountAdapter.AmountViewHolder>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AmountViewHolder {
val binding = ItemWalletAmountToSendBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
return AmountViewHolder(binding)
}
override fun onBindViewHolder(holder: AmountViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<Amount>() {
override fun areContentsTheSame(oldItem: Amount, newItem: Amount) =
oldItem.currencySymbol == newItem.currencySymbol
override fun areItemsTheSame(oldItem: Amount, newItem: Amount) = oldItem == newItem
}
class AmountViewHolder(val binding: ItemWalletAmountToSendBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(amount: Amount) = with(binding) {
tvCurrencySymbol.text = amount.currencySymbol
tvAmount.text = amount.value?.toFormattedString(amount.decimals)
root.setOnClickListener {
store.dispatch(WalletAction.DialogAction.Hide)
store.dispatch(WalletAction.Send(amount))
}
}
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogWalletTradeBinding
class ChooseTradeActionBottomSheetDialog(
context: Context,
private val dialogData: WalletDialog.ChooseTradeActionDialog,
) : BottomSheetDialog(context) {
var binding: DialogWalletTradeBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogWalletTradeBinding.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
}
override fun show() {
super.show()
this.setOnDismissListener {
binding = null
store.dispatch(WalletAction.DialogAction.Hide)
}
binding?.let {
with(it) {
dialogBtnBuy.show(dialogData.buyAllowed)
dialogBtnSell.show(dialogData.sellAllowed)
dialogBtnSwap.show(dialogData.swapAllowed)
dialogBtnBuy.setOnClickListener {
dismiss()
store.dispatch(TradeCryptoAction.Buy())
}
dialogBtnSell.setOnClickListener {
dismiss()
store.dispatch(TradeCryptoAction.Sell)
}
dialogBtnSwap.setOnClickListener {
dismiss()
store.dispatch(TradeCryptoAction.Swap)
}
}
}
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
class RussianCardholdersWarningBottomSheetDialog(
context: Context,
private val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data?,
) : BottomSheetDialog(context) {
private var binding: DialogRussiansCardholdersWarningBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Token.Topup.P2PScreenOpened())
binding = DialogRussiansCardholdersWarningBinding
.inflate(LayoutInflater.from(context))
.also { setContentView(it.root) }
}
override fun show() {
super.show()
setOnDismissListener {
binding = null
store.dispatchDialogHide()
}
binding?.btnYes?.setOnClickListener {
if (dialogData != null) {
store.dispatchOpenUrl(dialogData.topUpUrl)
Analytics.send(Token.Topup.ScreenOpened())
} else {
store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false))
}
dismiss()
}
binding?.btnNo?.setOnClickListener {
store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL))
dismiss()
}
}
companion object {
private const val INSTRUCTION_URL = "https://tangem.com/howtobuy.html"
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object ScanFailsDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
Analytics.send(IntroductionProcess.ButtonRequestSupport())
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
}
setNeutralButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
// TODO: Delete with WalletFeatureToggles
@Deprecated(message = "Used only in old wallet screen")
object SignedHashesWarningDialog {
fun create(context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
// setTitle(context.getString(R.string.warning_important_security_info, "\u26A0"))
// setMessage(R.string.alert_signed_hashes_message)
setPositiveButton(R.string.common_understand) { _, _ ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
store.dispatch(
GlobalAction.HideWarningMessage(WarningMessagesManager.signedHashesMultiWalletWarning),
)
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener {
store.dispatch(WalletAction.DialogAction.Hide)
}
}.create()
}
}

View file

@ -1,75 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object SimpleOkDialog {
fun create(dialog: AppDialog.SimpleOkDialog, context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(dialog.header)
setMessage(dialog.message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatchDialogHide()
dialog.onOk?.invoke()
}
}.create()
}
fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog {
val message = if (dialog.args.isEmpty()) {
context.getString(dialog.messageId)
} else {
context.getString(dialog.messageId, *dialog.args.toTypedArray())
}
return AlertDialog.Builder(context).apply {
setTitle(context.getString(dialog.headerId))
setMessage(message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatchDialogHide()
dialog.onOk?.invoke()
}
}.create()
}
fun create(dialog: AppDialog.SimpleOkErrorDialog, context: Context): AlertDialog = create(
dialog = AppDialog.SimpleOkDialog(
header = context.getString(R.string.common_error),
message = dialog.message,
onOk = dialog.onOk,
),
context = context,
)
fun create(dialog: AppDialog.SimpleOkWarningDialog, context: Context): AlertDialog = create(
dialog = AppDialog.SimpleOkDialog(
header = context.getString(R.string.common_warning),
message = dialog.message,
onOk = dialog.onOk,
),
context = context,
)
fun create(dialog: AppDialog.OkCancelDialogRes, context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(dialog.headerId))
setMessage(dialog.messageId)
setPositiveButton(dialog.okButton.title) { _, _ -> dialog.okButton.action?.invoke() }
setNegativeButton(dialog.cancelButton.title) { _, _ -> dialog.cancelButton.action?.invoke() }
setOnDismissListener {
store.dispatchDialogHide()
dialog.cancelButton.action?.invoke()
}
}.create()
}
}

View file

@ -1,172 +0,0 @@
package com.tangem.tap.features.wallet.ui.images
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import coil.imageLoader
import coil.request.ImageRequest
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getTextColor
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.wallet.R
private const val QCX = "QCX"
private const val VOYR = "VOYRME"
class CurrencyIconRequest(
private val currencyImageView: ImageFilterView,
private val currencyTextView: TextView?,
private val token: Token?,
private val blockchain: Blockchain,
private val getLocalImage: Boolean = false,
) {
fun load() {
when {
token == null && blockchain.isTestnet() -> loadTestnetBlockchainIcon()
token == null -> loadBlockchainIcon()
blockchain.isTestnet() -> loadTestnetTokenIcon()
else -> loadTokenIcon()
}
}
private fun loadBlockchainIcon() {
loadBlockchainIconBase(
onStart = {
currencyImageView.colorFilter = null
},
)
}
private fun loadTestnetBlockchainIcon() {
loadBlockchainIconBase(
onStart = {
currencyImageView.saturation = 0f
},
)
}
private fun loadTokenIcon() {
loadTokenIconBase(
onStart = {
currencyImageView.setColorFilter(it.getColor())
},
onSuccess = {
currencyImageView.colorFilter = null
},
onError = {
currencyImageView.setColorFilter(it.getColor())
currencyTextView?.setTextColor(it.getTextColor())
},
)
}
private fun loadTestnetTokenIcon() {
loadTokenIconBase(
onStart = {
currencyImageView.saturation = 0f
},
onError = {
currencyImageView.saturation = 0f
currencyImageView.setColorFilter(it.getColor(true))
currencyTextView?.setTextColor(it.getTextColor(true))
},
)
}
private inline fun loadBlockchainIconBase(
crossinline onStart: (Blockchain) -> Unit = {},
crossinline onSuccess: (Blockchain) -> Unit = {},
crossinline onError: (Blockchain) -> Unit = {},
) {
currencyImageView.loadIcon(
data = getBlockchainIconData(blockchain),
placeholderRes = getActiveIconRes(blockchain.id),
onStart = { onStart(blockchain) },
onSuccess = { onSuccess(blockchain) },
onError = { onError(blockchain) },
)
}
private fun getBlockchainIconData(blockchain: Blockchain): Any {
return if (getLocalImage) {
when (blockchain) {
Blockchain.TerraV1, Blockchain.TerraV2 -> getActiveIconRes(blockchain.toCoinId())
else -> getActiveIconRes(blockchain.id)
}
} else {
when (blockchain) {
Blockchain.TerraV1, Blockchain.TerraV2 -> getIconUrl(blockchain.toCoinId())
else -> getIconUrl(blockchain.toNetworkId())
}
}
}
private inline fun loadTokenIconBase(
crossinline onStart: (Token) -> Unit = {},
crossinline onSuccess: (Token) -> Unit = {},
crossinline onError: (Token) -> Unit = {},
) {
if (token == null) return
currencyImageView.loadIcon(
data = getTokenIcon(token, blockchain),
placeholderRes = R.drawable.shape_circle,
onStart = {
currencyTextView?.text = token.name.take(1)
currencyTextView?.setTextColor(token.getTextColor())
onStart(token)
},
onSuccess = {
currencyTextView?.text = null
onSuccess(token)
},
onError = {
// for some reason the onStart doesn't call if an error occurs
currencyTextView?.text = token.name.take(1)
onError(token)
},
)
}
}
private inline fun ImageView.loadIcon(
data: Any?,
placeholderRes: Int,
crossinline onStart: () -> Unit = {},
crossinline onSuccess: () -> Unit = {},
crossinline onError: () -> Unit = {},
) {
ImageRequest.Builder(context)
.data(data)
.placeholder(placeholderRes)
.error(placeholderRes)
.fallback(placeholderRes)
.listener(
onStart = { onStart() },
onSuccess = { _, _ -> onSuccess() },
onError = { _, _ -> onError() },
)
.target(imageView = this)
.build()
.also(context.imageLoader::enqueue)
}
private fun getTokenIcon(token: Token, blockchain: Blockchain): Any? {
return when (token.symbol) {
QCX -> R.drawable.ic_qcx
VOYR -> R.drawable.ic_voyr
else -> {
token.id?.let(::getIconUrl)
?: token.getCustomIconUrl()
?: IconsUtil.getTokenIconUri(blockchain, token)
?.toString()
}
}
}

View file

@ -1,74 +0,0 @@
package com.tangem.tap.features.wallet.ui.images
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.sdk.extensions.dpToPx
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.wallet.databinding.ViewCurrencyIconBinding
import kotlin.math.roundToInt
@Suppress("MagicNumber")
class CurrencyIconView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewCurrencyIconBinding.inflate(
LayoutInflater.from(context),
this,
)
val currencyImageView: ImageFilterView
get() = binding.ivCurrency
val currencyTextView: TextView
get() = binding.tvTokenLetter
val blockchainBadge: ImageFilterView
get() = binding.ivBlockchainBadge
var isBlockchainBadgeVisible: Boolean
get() = binding.ivBlockchainBadge.isVisible
set(value) = binding.ivBlockchainBadge::isVisible.set(value)
var isCustomCurrencyBadgeVisible: Boolean
get() = binding.customBadge.isVisible
set(value) = binding.customBadge::isVisible.set(value)
init {
minWidth = dpToPx(48f).roundToInt()
minHeight = dpToPx(48f).roundToInt()
}
}
fun CurrencyIconView.load(currency: Currency, derivationStyle: DerivationStyle?) {
isCustomCurrencyBadgeVisible = currency.isCustomCurrency(derivationStyle)
CurrencyIconRequest(
currencyImageView = currencyImageView,
currencyTextView = currencyTextView,
token = (currency as? Currency.Token)?.token,
blockchain = currency.blockchain,
).load()
if (currency.isToken()) {
// load a blockchain icon into the blockchain badge
isBlockchainBadgeVisible = true
CurrencyIconRequest(
currencyImageView = blockchainBadge,
currencyTextView = null,
token = null,
blockchain = currency.blockchain,
getLocalImage = true,
).load()
} else {
isBlockchainBadgeVisible = false
}
}

View file

@ -1,160 +0,0 @@
package com.tangem.tap.features.wallet.ui.test
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.tap.common.TestAction
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import java.math.BigDecimal
import kotlin.random.Random
/**
[REDACTED_AUTHOR]
*/
object TestWallet {
fun solanaRentExemptWarning(): List<TestAction> {
val checker = SolanaRentWarningActionEmitter()
return listOf(
"BALANCE = 0.0" to { checker.setZeroBalance() },
"BALANCE < 0.00089088" to { checker.setLessThanRentExempt() },
"BALANCE > 0.00089088" to { checker.setMoreThanRentExempt() },
"BALANCE = 0.00089087" to { checker.setLessThanRentExemptByOne() },
"BALANCE = 0.00089088 (rent exempt)" to { checker.setForRentExemptBarrier() },
"BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() },
)
}
fun getBlockchainBalanceActions(blockchainNetwork: BlockchainNetwork): List<TestAction> {
return getBlockchainBalanceActions(
getWalletManager(blockchainNetwork),
blockchainNetwork.blockchain.decimals(),
)
}
fun getTokenBalanceAction(blockchainNetwork: BlockchainNetwork, token: Token): List<TestAction> {
return getTokenBalanceAction(getWalletManager(blockchainNetwork), token)
}
fun getBlockchainBalanceActions(walletManager: WalletManager?, decimals: Int): List<TestAction> {
val minValue = BigDecimal.ONE.movePointLeft(decimals)
val averageValue = BigDecimal.ONE.movePointRight(decimals)
.divide(BigDecimal(2)).movePointLeft(decimals)
val maxValue = BigDecimal(2).pow(32)
return listOf(
"0.0" to { setBalance(walletManager, BigDecimal.ZERO) },
"Min value" to { setBalance(walletManager, minValue) },
"Average value" to { setBalance(walletManager, averageValue) },
"Max value" to { setBalance(walletManager, maxValue) },
"1234567890123.01234567890123" to { setBalance(walletManager, BigDecimal.ZERO) },
)
}
fun getTokenBalanceAction(walletManager: WalletManager?, token: Token): List<TestAction> {
val minValue = BigDecimal.ONE.movePointLeft(token.decimals)
val averageValue = BigDecimal.ONE.movePointRight(token.decimals)
.divide(BigDecimal(2)).movePointLeft(token.decimals)
val maxValue = BigDecimal(2).pow(32)
return listOf(
"0.0" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) },
"Min value" to { setTokenBalance(walletManager, minValue, token) },
"Average value" to { setTokenBalance(walletManager, averageValue, token) },
"Max value" to { setTokenBalance(walletManager, maxValue, token) },
"1234567890123.01234567890123" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) },
)
}
fun setBalance(walletManager: WalletManager?, value: BigDecimal) {
val manager = walletManager.guard {
store.dispatchDebugErrorNotification("WalletManager not found")
return
}
val amount = manager.wallet.amounts[AmountType.Coin] ?: Amount(manager.wallet.blockchain)
setBalance(manager, amount, value)
}
private fun setTokenBalance(walletManager: WalletManager?, value: BigDecimal, token: Token? = null) {
val manager = walletManager.guard {
store.dispatchDebugErrorNotification("WalletManager not found")
return
}
val token = token.guard {
store.dispatchDebugErrorNotification("Token not found")
return
}
val amount = manager.wallet.amounts[AmountType.Token(token)] ?: Amount(token)
setBalance(manager, amount, value)
}
private fun setBalance(walletManager: WalletManager, amount: Amount, value: BigDecimal) {
TestActions.testAmountInjectionForWalletManagerEnabled = true
walletManager.wallet.setAmount(amount.copy(value = value))
store.dispatch(WalletAction.LoadData)
}
private fun getWalletManager(blockchainNetwork: BlockchainNetwork): WalletManager? {
return store.state.walletState.getWalletManager(blockchainNetwork)
}
}
private class SolanaRentWarningActionEmitter {
private val zero = BigDecimal.ZERO
private val one = BigDecimal(0.00000001)
private val rentExemptBarrier = BigDecimal(0.00089088)
private val lessThanRentExemptByOne = rentExemptBarrier.minus(one)
private val moreThanRentExemptByOne = rentExemptBarrier.plus(one)
private val moreThanRentExempt = rentExemptBarrier.plus(Random.nextDouble().toBigDecimal())
private val lessThanRentExempt = rentExemptBarrier
.minus(Random.nextDouble(0.0, rentExemptBarrier.minus(one).toDouble()).toBigDecimal())
fun setZeroBalance() {
setBalance(zero)
}
fun setForRentExemptBarrier() {
setBalance(rentExemptBarrier)
}
fun setLessThanRentExemptByOne() {
setBalance(lessThanRentExemptByOne)
}
fun setMoreThanRentExemptByOne() {
setBalance(moreThanRentExemptByOne)
}
fun setMoreThanRentExempt() {
setBalance(moreThanRentExempt)
}
fun setLessThanRentExempt() {
setBalance(lessThanRentExempt)
}
private fun setBalance(value: BigDecimal) {
val amount = Amount(getBlockchainNetwork().blockchain).copy(value = value)
getWalletManager().apply {
TestActions.testAmountInjectionForWalletManagerEnabled = true
wallet.setAmount(amount)
}
store.dispatch(WalletAction.LoadData)
}
private fun getWalletManager(): WalletManager {
return store.state.walletState.getWalletManager(getBlockchainNetwork())!!
}
private fun getBlockchainNetwork(): BlockchainNetwork {
val currency = store.state.walletState.selectedWalletData!!.currency
return BlockchainNetwork(currency.blockchain, currency.derivationPath, listOf())
}
}

View file

@ -1,164 +0,0 @@
package com.tangem.tap.features.wallet.ui.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCryptoCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import java.math.BigDecimal
internal fun WalletDataModel.mainButton(blockchainAmount: BigDecimal): WalletMainButton = WalletMainButton.SendButton(
enabled = !isEmptyAmount &&
hasPendingTransactions() &&
!blockchainAmount.isZero(),
)
internal fun WalletDataModel.hasPendingTransactions(): Boolean {
// for now check pending ongoing only just for BTC, later test and add other utxo networks
val isBitcoinBlockchain =
currency.blockchain == Blockchain.Bitcoin || currency.blockchain == Blockchain.BitcoinTestnet
if (currency.isBlockchain() && isBitcoinBlockchain) {
val outgoingTransactions = status.pendingTransactions.filter {
it.type == PendingTransactionType.Outgoing
}
return outgoingTransactions.isEmpty()
}
return status.pendingTransactions.isEmpty()
}
internal fun WalletDataModel.getFormattedCryptoAmount(): String {
return status.amount.toFormattedCryptoCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
}
internal fun WalletDataModel.getFormattedFiatAmount(
fiatCurrency: FiatCurrency,
unknownAmountSign: String = UNKNOWN_AMOUNT_SIGN,
): String {
return this.fiatRate?.let { status.amount.toFiatValue(it) }
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(fiatCurrencyName = fiatCurrency.symbol, fiatCode = fiatCurrency.code)
?: unknownAmountSign
}
internal fun WalletDataModel.getFormattedFiatRate(fiatCurrency: FiatCurrency, noRateValue: String): String {
return fiatRate?.toFiatRateString(fiatCurrency.symbol, fiatCurrency.code)
?: noRateValue
}
internal fun WalletDataModel.isAvailableToBuy(exchangeManager: CurrencyExchangeManager): Boolean {
return exchangeManager.availableForBuy(currency)
}
internal fun WalletDataModel.isAvailableToSell(exchangeManager: CurrencyExchangeManager): Boolean {
return exchangeManager.availableForSell(currency)
}
internal fun WalletDataModel.isAvailableToSwap(
swapFeatureToggleManager: SwapFeatureToggleManager,
swapInteractor: SwapInteractor,
isSingleWallet: Boolean,
): Boolean {
if (isSingleWallet) {
return false
}
if (currency.blockchain.id == Blockchain.Optimism.id && !swapFeatureToggleManager.isOptimismSwapEnabled) {
return false
}
return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) &&
!currency.isCustomCurrency(null)
}
internal fun WalletDataModel.getAvailableActions(
swapInteractor: SwapInteractor,
exchangeManager: CurrencyExchangeManager,
swapFeatureToggleManager: SwapFeatureToggleManager,
isSingleWallet: Boolean,
): Set<CurrencyAction> {
return setOfNotNull(
if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null,
if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null,
if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor, isSingleWallet)) CurrencyAction.Swap else null,
)
}
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1
}
internal fun WalletDataModel.assembleWarnings(
blockchainAmount: BigDecimal,
blockchainWalletRent: WalletStoreModel.WalletRent?,
): List<WalletWarning> {
val walletWarnings = mutableListOf<WalletWarning>()
assembleNonTypedWarnings(walletWarnings, blockchainWalletRent)
assembleBlockchainWarnings(walletWarnings)
assembleTokenWarnings(walletWarnings, blockchainAmount)
return walletWarnings.sortedBy { it.showingPosition }
}
private fun WalletDataModel.assembleNonTypedWarnings(
walletWarnings: MutableList<WalletWarning>,
walletRent: WalletStoreModel.WalletRent?,
) {
if (this.status is WalletDataModel.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress(currency.currencyName))
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}
}
private fun WalletDataModel.assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) {
with(currency) {
if (!isBlockchain()) return
if (existentialDeposit != null) {
val warning = WalletWarning.ExistentialDeposit(
currencyName = currencyName,
edStringValueWithSymbol = "${existentialDeposit.toPlainString()} $currencySymbol",
)
walletWarnings.add(warning)
}
}
}
private fun WalletDataModel.assembleTokenWarnings(
walletWarnings: MutableList<WalletWarning>,
blockchainAmount: BigDecimal,
) {
if (!currency.isToken()) return
if (!this.isEmptyAmount && blockchainAmount.isZero()) {
walletWarnings.add(
WalletWarning.BalanceNotEnoughForFee(
currencyName = currency.currencyName,
blockchainFullName = currency.blockchain.fullName,
blockchainSymbol = currency.blockchain.currency,
),
)
}
}
private val WalletDataModel.isEmptyAmount: Boolean
get() = this.status.amount.isZero()
enum class CurrencyAction {
Buy, Sell, Swap
}

View file

@ -1,355 +0,0 @@
package com.tangem.tap.features.wallet.ui.view
import android.content.Context
import android.util.AttributeSet
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.wallet.R
import com.valentinilk.shimmer.shimmer
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
internal class TotalBalanceCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractComposeView(context, attrs, defStyleAttr) {
private var state by mutableStateOf<TotalBalanceCardState>(TotalBalanceCardState.Empty)
var status: TotalFiatBalance? = null
set(value) {
if (field == value) return
field = value
updateState(value, fiatCurrency, onChangeFiatCurrencyClick)
}
var onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
set(value) {
if (field == value) return
field = value
updateState(status, fiatCurrency, value)
}
var fiatCurrency: FiatCurrency = FiatCurrency.Default
set(value) {
if (field == value) return
field = value
updateState(status, value, onChangeFiatCurrencyClick)
}
@Composable
override fun Content() {
TangemTheme {
TotalBalanceCardContent(state = state)
}
}
override fun getAccessibilityClassName(): CharSequence {
return javaClass.name
}
private fun updateState(status: TotalFiatBalance?, fiatCurrency: FiatCurrency, onChangeCurrencyClick: () -> Unit) {
state = when (status) {
null -> TotalBalanceCardState.Empty
is TotalFiatBalance.Failed -> TotalBalanceCardState.Failure(
fiatCurrency = fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
is TotalFiatBalance.Loading -> TotalBalanceCardState.Loading(
fiatCurrency = fiatCurrency,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
)
is TotalFiatBalance.Loaded -> TotalBalanceCardState.Success(
amount = status.amount,
showWarning = status.isWarning,
onChangeFiatCurrencyClick = onChangeCurrencyClick,
fiatCurrency = fiatCurrency,
)
}
}
}
@Composable
private fun TotalBalanceCardContent(state: TotalBalanceCardState, modifier: Modifier = Modifier) {
TotalBalanceCardScaffold(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.main_page_balance),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
amount = {
when (state) {
is TotalBalanceCardState.Empty,
is TotalBalanceCardState.Loading,
-> LoadingAmount()
is TotalBalanceCardState.Failure,
is TotalBalanceCardState.Success,
-> LoadedAmount(
amount = buildAmountString(
amount = state.amount,
fiatCurrency = state.fiatCurrency,
),
)
}
},
currencySelector = {
if (state !is TotalBalanceCardState.Empty) {
SelectorButton(
text = state.fiatCurrency.code,
onClick = state.onChangeFiatCurrencyClick,
)
}
},
warningText = {
AnimatedVisibility(visible = state.showWarning) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.main_processing_full_amount),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.attention,
)
}
},
)
}
@Composable
private fun TotalBalanceCardScaffold(
title: @Composable () -> Unit,
amount: @Composable () -> Unit,
currencySelector: @Composable () -> Unit,
warningText: @Composable () -> Unit,
modifier: Modifier = Modifier,
amountWeight: Float = 0.8f,
) {
Surface(
modifier = modifier,
shape = TangemTheme.shapes.roundedCornersMedium,
color = TangemTheme.colors.background.plain,
elevation = TangemTheme.dimens.elevation1,
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
SpacerW16()
Column(
modifier = Modifier.weight(amountWeight),
) {
SpacerH12()
title()
SpacerH4()
amount()
}
currencySelector()
SpacerW4()
}
SpacerH4()
Box(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing16,
),
) {
warningText()
}
SpacerH12()
}
}
}
@Composable
private fun LoadingAmount(modifier: Modifier = Modifier) {
Box(modifier = modifier.shimmer()) {
Box(
modifier = Modifier
.width(TangemTheme.dimens.size116)
.height(TangemTheme.dimens.size32)
.background(
color = TangemTheme.colors.stroke.primary,
shape = TangemTheme.shapes.roundedCornersSmall2,
),
)
}
}
@Composable
private fun LoadedAmount(amount: AnnotatedString, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Text(
text = amount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
@Composable
private fun buildAmountString(amount: BigDecimal?, fiatCurrency: FiatCurrency): AnnotatedString {
if (amount == null) return AnnotatedString(text = UNKNOWN_AMOUNT_SIGN)
val locale = Locale.getDefault()
val fractionDigits = 2
val formatter = NumberFormat.getCurrencyInstance(locale) as? DecimalFormat
?: return AnnotatedString("${amount.toPlainString()} ${fiatCurrency.symbol}")
val currencyToShow = "${fiatCurrency.symbol}"
val scaledAmount = try {
Currency.getInstance(fiatCurrency.code)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = fractionDigits
formatter.minimumFractionDigits = fractionDigits
formatter.isGroupingUsed = true
formatter.roundingMode = RoundingMode.HALF_UP
formatter.format(amount).replace(currency.symbol, currencyToShow)
} ?: formatter.format(amount)
} catch (e: IllegalArgumentException) {
Timber.e("TotalBalanceCard buildAmountString currencyCode is not a supported ISO 4217 code: $e")
formatter.currency?.let {
formatter.format(amount).replace(it.symbol, currencyToShow)
} ?: formatter.format(amount)
}
val integer = scaledAmount.substringBefore(formatter.decimalFormatSymbols.decimalSeparator)
var reminder = scaledAmount.substringAfter(formatter.decimalFormatSymbols.decimalSeparator)
// if locale formatted currency at the end, remember it and place out of AnnotatedString
val currency = if (reminder.endsWith(currencyToShow)) {
reminder = reminder.dropLast(currencyToShow.length)
currencyToShow
} else {
""
}
return buildAnnotatedString {
append(integer)
append(formatter.decimalFormatSymbols.decimalSeparator)
append(
AnnotatedString(
text = reminder,
spanStyle = TangemTheme.typography.h3.toSpanStyle(),
),
)
append(currency) // it is not empty if was placed at the end after locale formatting
}
}
private sealed interface TotalBalanceCardState {
val amount: BigDecimal?
val showWarning: Boolean
val fiatCurrency: FiatCurrency
val onChangeFiatCurrencyClick: () -> Unit
object Empty : TotalBalanceCardState {
override val amount: BigDecimal? = null
override val showWarning: Boolean = false
override val fiatCurrency: FiatCurrency = FiatCurrency.Default
override val onChangeFiatCurrencyClick: () -> Unit = { /* no-op */ }
}
data class Loading(
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState {
override val amount: BigDecimal = BigDecimal.ZERO
override val showWarning: Boolean = false
}
data class Failure(
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState {
override val amount: BigDecimal? = null
override val showWarning: Boolean = true
}
data class Success(
override val amount: BigDecimal,
override val showWarning: Boolean,
override val fiatCurrency: FiatCurrency,
override val onChangeFiatCurrencyClick: () -> Unit,
) : TotalBalanceCardState
}
// region Preview
@Composable
private fun TotalBalanceCardContentSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.padding(all = TangemTheme.dimens.spacing16),
) {
TotalBalanceCardContent(state = TotalBalanceCardState.Empty)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Loading(FiatCurrency("USD", "USD", "$")) {},
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Failure(
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TotalBalanceCardContent(
state = TotalBalanceCardState.Success(
amount = BigDecimal("9917.72"),
showWarning = false,
onChangeFiatCurrencyClick = {},
fiatCurrency = FiatCurrency("USD", "USD", "$"),
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Light() {
TangemTheme {
TotalBalanceCardContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TotalBalanceCardContentPreview_Dark() {
TangemTheme(isDark = true) {
TotalBalanceCardContentSample()
}
}
// endregion Preview

View file

@ -1,77 +0,0 @@
package com.tangem.tap.features.wallet.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.ui.utils.CurrencyAction
import com.tangem.wallet.databinding.ViewWalletDetailsButtonsRowBinding
internal class WalletDetailsButtonsRow @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding = ViewWalletDetailsButtonsRowBinding.inflate(
LayoutInflater.from(context),
this,
)
var onBuyClick: (() -> Unit)? = null
var onSellClick: (() -> Unit)? = null
var onTradeClick: (() -> Unit)? = null
var onSwapClick: (() -> Unit)? = null
var onSendClick: (() -> Unit)? = null
init {
orientation = HORIZONTAL
with(binding) {
btnBuy.setOnClickListener { onBuyClick?.invoke() }
btnSell.setOnClickListener { onSellClick?.invoke() }
btnSwap.setOnClickListener { onSwapClick?.invoke() }
btnTrade.setOnClickListener { onTradeClick?.invoke() }
btnSend.setOnClickListener { onSendClick?.invoke() }
}
}
fun updateButtonsVisibility(
actions: Set<CurrencyAction>,
exchangeServiceFeatureOn: Boolean,
sendAllowed: Boolean,
) = with(binding) {
containerActionButtons.isVisible = exchangeServiceFeatureOn
when {
actions.isEmpty() -> {
containerActionButtons.hide()
}
actions.size == 1 -> {
val action = actions.first()
btnTrade.hide()
btnBuy.show(action == CurrencyAction.Buy)
btnSell.show(action == CurrencyAction.Sell)
btnSwap.show(action == CurrencyAction.Swap)
}
else -> {
btnBuy.hide()
btnSell.hide()
btnSwap.hide()
btnTrade.show()
}
}
if (containerActionButtons.isVisible) {
btnSend.gravity = Gravity.START or Gravity.CENTER_VERTICAL
btnSend.iconGravity = MaterialButton.ICON_GRAVITY_END
} else {
btnSend.gravity = Gravity.CENTER
btnSend.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
}
btnSend.isEnabled = sendAllowed
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
object CurrencySelectionDialog {
fun create(dialog: WalletDialog.CurrencySelectionDialog, context: Context): AlertDialog {
val currenciesToShow = dialog.currenciesList
.map { it.displayName }
.toTypedArray()
val currentSelection = dialog.currenciesList
.indexOfFirst { it.code == dialog.currentAppCurrency.code }
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog)
.setTitle(context.getString(R.string.details_row_title_currency))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> /* no-op */ }
.setOnDismissListener {
store.dispatch(WalletAction.DialogAction.Hide)
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
dialog.currenciesList.getOrNull(which)?.let { selectedCurrency ->
store.dispatch(
WalletAction.AppCurrencyAction.SelectAppCurrency(
fiatCurrency = selectedCurrency,
),
)
store.dispatch(WalletAction.DialogAction.Hide)
}
}
.create()
}
}

View file

@ -1,191 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
private val watcher = modelWatcher<WalletState> {
// !!! Workaround !!!
// Checking state properties instead of state params can reduce application performance,
// but here it is necessary because the WalletStore has an unsuitable equals method
WalletState::walletsDataFromStores {
walletsAdapter.submitList(it)
}
WalletState::loadingUserTokens {
binding?.pbLoadingUserTokens?.show(it)
}
WalletState::walletCardsCount { walletCardsCount ->
binding?.let {
setupWalletCardNumber(it, walletCardsCount)
}
}
WalletState::missingDerivations { missingDerivations ->
binding?.let {
handleRescanWarning(it, missingDerivations.isNotEmpty())
}
}
WalletState::showBackupWarning { showBackupWarnings ->
binding?.let {
handleBackupWarning(it, showBackupWarnings)
}
}
(WalletState::totalBalance or WalletState::walletsDataFromStores) { walletState ->
binding?.let {
handleTotalBalance(
binding = it,
totalBalance = walletState.totalBalance,
walletsCount = walletState.walletsDataFromStores.size,
appFiatCurrency = store.state.globalState.appCurrency,
)
}
}
}
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
setFragment(fragment, binding)
onViewCreated()
showMultiWalletView(binding)
}
private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) {
watcher.clear()
tvTwinCardNumber.hide()
rvPendingTransaction.hide()
lCardBalance.root.hide()
lAddress.root.hide()
rowButtons.hide()
lSingleWalletBalance.root.hide()
lCardTotalBalance.show()
rvMultiwallet.show()
btnAddToken.show()
}
override fun onViewCreated() {
setupWalletsRecyclerView()
}
private fun setupWalletsRecyclerView() {
val fragment = fragment ?: return
walletsAdapter = WalletAdapter()
walletsAdapter.setHasStableIds(true)
binding?.rvMultiwallet?.layoutManager = LinearLayoutManager(fragment.requireContext())
binding?.rvMultiwallet?.adapter = walletsAdapter
binding?.rvMultiwallet?.itemAnimator = null
}
override fun onNewState(state: WalletState) {
val fragment = fragment ?: return
val binding = binding ?: return
watcher.invoke(state)
binding.btnAddToken.setOnClickListener {
Analytics.send(Portfolio.ButtonManageTokens())
store.dispatch(action = TokensAction.SetArgs.ManageAccess)
store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.ManageTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)
}
private fun setupWalletCardNumber(binding: FragmentWalletBinding, walletCardsCount: Int?) = with(binding) {
if (walletCardsCount != null) {
tvTwinCardNumber.show()
tvTwinCardNumber.text =
tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, walletCardsCount)
} else {
tvTwinCardNumber.hide()
}
}
private fun handleBackupWarning(binding: FragmentWalletBinding, showBackupWarning: Boolean) =
with(binding.lWalletBackupWarning) {
root.isVisible = showBackupWarning
root.setOnClickListener {
Analytics.send(MainScreen.NoticeBackupYourWalletTapped())
store.dispatch(WalletAction.MultiWallet.BackupWallet)
}
}
private fun handleRescanWarning(binding: FragmentWalletBinding, showRescanWarning: Boolean) =
with(binding.lWalletRescanWarning) {
root.isVisible = showRescanWarning
root.setOnClickListener {
Analytics.send(MainScreen.NoticeScanYourCardTapped())
store.dispatch(WalletAction.MultiWallet.ScanToGetDerivations)
}
}
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalFiatBalance?,
walletsCount: Int,
appFiatCurrency: FiatCurrency,
) = with(binding.lCardTotalBalance) {
isVisible = walletsCount > 0
onChangeFiatCurrencyClick = {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
status = totalBalance
fiatCurrency = appFiatCurrency
}
private fun handleErrorStates(state: WalletState, binding: FragmentWalletBinding, fragment: WalletFragment) {
when (state.error) {
ErrorType.UnknownBlockchain -> {
showErrorState(
binding,
fragment.getText(R.string.wallet_error_unsupported_blockchain),
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle),
)
}
else -> Unit
}
}
private fun showErrorState(
binding: FragmentWalletBinding,
errorTitle: CharSequence,
errorDescription: CharSequence,
) = with(binding) {
lCardBalance.root.show()
with(lCardBalance) {
lBalance.root.hide()
lBalanceError.root.show()
rvMultiwallet.show()
btnAddToken.hide()
lBalanceError.tvErrorTitle.text = errorTitle
lBalanceError.tvErrorDescriptions.text = errorDescription
}
}
override fun onDestroyFragment() {
super.onDestroyFragment()
watcher.clear()
}
}

View file

@ -1,235 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.core.analytics.Analytics
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.model.WalletDataModel
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.WalletState
import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.utils.*
import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
class SingleWalletView : WalletView() {
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
// FIXME: Move to model watcher
private var watchedPrimaryWalletForAddressCard: WalletDataModel? = null
private var watchedPrimaryWalletForBalance: WalletDataModel? = null
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
setFragment(fragment, binding)
onViewCreated()
showSingleWalletView(binding)
}
private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) {
watchedPrimaryWalletForAddressCard = null
watchedPrimaryWalletForBalance = null
tvTwinCardNumber.hide()
rvMultiwallet.hide()
btnAddToken.hide()
rvPendingTransaction.hide()
pbLoadingUserTokens.hide()
lCardTotalBalance.hide()
lSingleWalletBalance.root.hide()
lWalletRescanWarning.root.hide()
lWalletBackupWarning.root.hide()
lCardBalance.root.show()
lAddress.root.show()
rowButtons.show()
}
override fun onViewCreated() {
setupTransactionsRecyclerView()
}
override fun onDestroyFragment() {
super.onDestroyFragment()
watchedPrimaryWalletForAddressCard = null
watchedPrimaryWalletForBalance = null
}
private fun setupTransactionsRecyclerView() {
val fragment = fragment ?: return
pendingTransactionAdapter = PendingTransactionsAdapter()
binding?.rvPendingTransaction?.layoutManager =
LinearLayoutManager(fragment.requireContext())
binding?.rvPendingTransaction?.adapter = pendingTransactionAdapter
}
override fun onNewState(state: WalletState) {
val binding = binding ?: return
val primaryWalletData = state.primaryWalletData ?: return
setupTwinCards(state.twinCardsState, binding)
setupButtons(primaryWalletData, binding, state.isExchangeServiceFeatureOn)
setupAddressCard(state, binding)
showPendingTransactionsIfPresent(primaryWalletData.status.pendingTransactions)
setupBalance(state, primaryWalletData)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
val knownTransactions = pendingTransactions.filterNot {
it.type == PendingTransactionType.Unknown
}
pendingTransactionAdapter.submitList(knownTransactions)
binding?.rvPendingTransaction?.show(knownTransactions.isNotEmpty())
}
private fun setupBalance(state: WalletState, primaryWallet: WalletDataModel) {
if (watchedPrimaryWalletForBalance == primaryWallet) return
watchedPrimaryWalletForBalance = primaryWallet
val fragment = fragment ?: return
binding?.apply {
lCardBalance.lBalance.root.show()
BalanceWidget(
binding = this.lCardBalance,
fragment = fragment,
blockchainWalletData = primaryWallet,
tokenWalletData = state.primaryTokenData,
).setup()
}
}
private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) {
if (twinCardsState?.cardNumber == null) {
tvTwinCardNumber.hide()
} else {
tvTwinCardNumber.show()
tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, 2)
}
}
private fun setupButtons(
walletData: WalletDataModel,
binding: FragmentWalletBinding,
isExchangeServiceFeatureEnabled: Boolean,
) = with(binding) {
setupRowButtons(walletData, rowButtons, isExchangeServiceFeatureEnabled)
lAddress.btnCopy.setOnClickListener {
walletData.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, fragment!!.requireContext()))
}
}
lAddress.btnShowQr.setOnClickListener {
Analytics.send(Token.ShowWalletAddress)
walletData.walletAddresses?.selectedAddress?.let { selectedAddress ->
store.dispatch(
WalletAction.DialogAction.QrCode(
currency = walletData.currency,
selectedAddress = selectedAddress,
),
)
}
}
}
private fun setupRowButtons(
walletData: WalletDataModel,
rowButtons: WalletDetailsButtonsRow,
isExchangeServiceFeatureEnabled: Boolean,
) {
val swapInteractor = this.swapInteractor ?: return
val swapFeatureToggleManager = this.swapFeatureToggleManager ?: return
val exchangeManager = store.state.globalState.exchangeManager
binding?.rowButtons?.apply {
onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) }
onSellClick = { store.dispatch(TradeCryptoAction.Sell) }
onSwapClick = { store.dispatch(TradeCryptoAction.Swap) }
onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = walletData.isAvailableToBuy(exchangeManager),
sellAllowed = walletData.isAvailableToSell(exchangeManager),
swapAllowed = false, // always disable for single wallet
),
)
}
}
val actions = walletData.getAvailableActions(
swapInteractor = swapInteractor,
exchangeManager = exchangeManager,
swapFeatureToggleManager = swapFeatureToggleManager,
isSingleWallet = true,
)
binding?.rowButtons?.updateButtonsVisibility(
actions = actions,
exchangeServiceFeatureOn = isExchangeServiceFeatureEnabled,
sendAllowed = walletData.mainButton(walletData.status.amount).enabled,
)
rowButtons.onSendClick = { store.dispatch(WalletAction.Send()) }
}
private fun setupAddressCard(state: WalletState, binding: FragmentWalletBinding) = with(binding.lAddress) {
val primaryWallet = state.primaryWalletData
if (primaryWallet == watchedPrimaryWalletForAddressCard) return@with
watchedPrimaryWalletForAddressCard = primaryWallet
if (primaryWallet?.walletAddresses != null && primaryWallet.currency is Currency.Blockchain) {
binding.lAddress.root.show()
if (primaryWallet.shouldShowMultipleAddress()) {
(binding.lAddress.root as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
val checkedId = MultipleAddressUiHelper.typeToId(
primaryWallet.walletAddresses.selectedAddress.type,
primaryWallet.currency.blockchain,
)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
chipGroupAddressType.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type = MultipleAddressUiHelper.idToType(checkedId, primaryWallet.currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chipGroupAddressType.hide()
}
tvAddress.text = primaryWallet.walletAddresses.selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(
WalletAction.ExploreAddress(
primaryWallet.walletAddresses.selectedAddress.exploreUrl,
fragment!!.requireContext(),
),
)
}
setupCardInfo(primaryWallet)
} else {
binding.lAddress.root.hide()
}
}
private fun setupCardInfo(walletData: WalletDataModel) {
val textView = binding?.lAddress?.tvInfo
val blockchain = walletData.currency.blockchain
if (textView != null) {
textView.text = textView.getString(
id = R.string.address_qr_code_message_format,
blockchain.fullName,
blockchain.currency,
blockchain.fullName,
)
}
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView {
var swapInteractor: SwapInteractor? = null
var swapFeatureToggleManager: SwapFeatureToggleManager? = null
protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null
fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
this.fragment = fragment
this.binding = binding
}
fun removeFragment() {
fragment = null
binding = null
}
open fun onViewDestroy() {
removeFragment()
}
open fun onDestroyFragment() {}
abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding)
abstract fun onViewCreated()
abstract fun onNewState(state: WalletState)
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.walletSelector.ui.model
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
internal sealed interface UserWalletItem {
val id: UserWalletId
@ -11,7 +11,7 @@ internal sealed interface UserWalletItem {
val isLocked: Boolean
sealed class Balance {
open val amount: String = UNKNOWN_AMOUNT_SIGN
open val amount: String = BigDecimalFormatter.EMPTY_BALANCE_SIGN
open val showWarning: Boolean = false
object Loading : Balance()

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.wallet.converters
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token

View file

@ -2,7 +2,6 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter
class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager {

View file

@ -16,7 +16,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.exchangeServices.ExchangeService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -43,7 +42,6 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl
@Deprecated("Use scan response from selected user wallet")
var scanResponse: ScanResponse? = null
var walletState: WalletState? = null
var userTokensRepository: UserTokensRepository? = null
var mainStore: Store<AppState>? = null
var tangemSdkManager: TangemSdkManager? = null

View file

@ -15,10 +15,8 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
@ -39,7 +37,6 @@ class TransactionManagerImpl(
private val analytics: AnalyticsEventHandler,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val walletFeatureToggles: WalletFeatureToggles,
) : TransactionManager {
override suspend fun sendApproveTransaction(
@ -413,19 +410,15 @@ class TransactionManagerImpl(
}
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val walletManager = if (walletFeatureToggles.isRedesignedScreenEnabled) {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
} else {
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
appStateHolder.walletState?.getWalletManager(blockchainNetwork)
}
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
return requireNotNull(walletManager) { "no wallet manager found" }
}

View file

@ -11,15 +11,12 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NativeToken
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFiatCurrency
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import com.tangem.tap.walletStoresManager
@ -31,7 +28,6 @@ import com.tangem.tap.features.wallet.models.Currency as WalletCurrency
class UserWalletManagerImpl(
private val appStateHolder: AppStateHolder,
private val walletManagersFacade: WalletManagersFacade,
private val walletFeatureToggles: WalletFeatureToggles,
) : UserWalletManager {
override suspend fun getUserTokens(
@ -216,28 +212,17 @@ class UserWalletManagerImpl(
)
}
override fun refreshWallet() {
// workaround, should update wallet after transaction
appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh)
}
@Throws(IllegalArgumentException::class)
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val walletManager = if (walletFeatureToggles.isRedesignedScreenEnabled) {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
} else {
val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList())
return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) {
"No wallet manager found"
}
}
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "userWallet or userWalletsListManager is null" }
val walletManager = walletManagersFacade.getOrCreateWalletManager(
selectedUserWallet.walletId,
blockchain,
derivationPath,
)
return requireNotNull(walletManager) {
"No wallet manager found"
}

View file

@ -10,7 +10,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
@ -39,12 +38,10 @@ class ProxyModule {
fun provideUserWalletManager(
appStateHolder: AppStateHolder,
walletManagersFacade: WalletManagersFacade,
walletFeatureToggles: WalletFeatureToggles,
): UserWalletManager {
return UserWalletManagerImpl(
appStateHolder = appStateHolder,
walletManagersFacade = walletManagersFacade,
walletFeatureToggles = walletFeatureToggles,
)
}
@ -55,14 +52,12 @@ class ProxyModule {
analytics: AnalyticsEventHandler,
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
walletFeatureToggles: WalletFeatureToggles,
): TransactionManager {
return TransactionManagerImpl(
appStateHolder = appStateHolder,
analytics = analytics,
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
walletFeatureToggles = walletFeatureToggles,
)
}

View file

@ -42,8 +42,6 @@ interface UserWalletManager {
suspend fun hideAllTokens()
fun refreshWallet()
/**
* Returns wallet public address for token
*