Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-10 15:56:05 +03:00
commit 8bba8e1afa
825 changed files with 9837 additions and 22210 deletions

View file

@ -23,7 +23,7 @@ import com.tangem.wallet.R
*/
abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackPressedHandler {
protected lateinit var mainView: View
private lateinit var mainView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.customtoken.impl.data
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.toNetworkId
@ -39,6 +40,7 @@ class DefaultCustomTokenRepository(
contractAddress = address,
networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","),
)
.getOrThrow()
.coins.firstNotNullOfOrNull { coin ->
val networksWithTheSameAddress = coin.networks.filter { network ->
(network.contractAddress != null || network.decimalCount != null) &&

View file

@ -1,7 +1,9 @@
package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
@ -25,8 +27,10 @@ internal object CustomTokenInteractorModule {
fun provideCustomTokenInteractor(
tangemTechApi: TangemTechApi,
appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
reduxStateHolder: AppStateHolder,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
testerFeatureToggles: TesterFeatureToggles,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -35,6 +39,8 @@ internal object CustomTokenInteractorModule {
reduxStateHolder = reduxStateHolder,
),
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
testerFeatureToggles = testerFeatureToggles,
)
}
}

View file

@ -9,6 +9,10 @@ import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
*/
interface CustomTokenRepository {
/** Find token by [address] and [networkId] */
/**
* Find token by [address] and [networkId]
*
* @throws com.tangem.datasource.api.common.response.ApiResponseError
* */
suspend fun findToken(address: String, networkId: String?): FoundToken
}

View file

@ -6,11 +6,10 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
@ -20,21 +19,22 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
/**
@ -47,6 +47,8 @@ import timber.log.Timber
class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val testerFeatureToggles: TesterFeatureToggles,
) : CustomTokenInteractor {
// TODO: Move to DI
@ -66,19 +68,29 @@ class DefaultCustomTokenInteractor(
override suspend fun saveToken(customCurrency: CustomCurrency) {
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
submitAdd(userWallet, currency)
// TODO: delete [REDACTED_JIRA]
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
}
}
}
@ -179,69 +191,36 @@ class DefaultCustomTokenInteractor(
private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) {
val scanResponse = userWallet.scanResponse
val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
val userWalletId = userWallet.walletId
if (walletFeatureToggles.isRedesignedScreenEnabled) {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
val currencyList = listOfNotNull(element = currency.toCryptoCurrency(scanResponse))
submitNewAdd(
userWalletId = userWallet.walletId,
updatedScanResponse = scanResponse,
currencyList = listOfNotNull(
when (currency) {
is Currency.Blockchain -> {
cryptoCurrencyFactory.createCoin(
blockchain = currency.blockchain,
extraDerivationPath = currency.derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
is Currency.Token -> {
cryptoCurrencyFactory.createToken(
sdkToken = currency.token,
blockchain = currency.blockchain,
extraDerivationPath = currency.derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
},
),
)
} else {
submitLegacyAdd(scanResponse = scanResponse, currency = currency)
userWalletsListManager.update(userWalletId) {
it.copy(scanResponse = scanResponse)
}
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
private suspend fun submitLegacyAdd(scanResponse: ScanResponse, currency: Currency) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected")
return
}
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet -> userWallet.copy(scanResponse = scanResponse) },
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = listOf(currency),
return when (this) {
is Currency.Blockchain -> {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
is Currency.Token -> {
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)
}
}
private fun submitNewAdd(
userWalletId: UserWalletId,
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}
}

View file

@ -25,8 +25,6 @@ import com.tangem.domain.features.addCustomToken.CustomCurrency
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 +36,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
@ -71,7 +67,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
private val featureInteractor: CustomTokenInteractor,
private val dispatchers: AppCoroutineDispatcherProvider,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val walletFeatureToggles: WalletFeatureToggles,
) : ViewModel(), DefaultLifecycleObserver {
private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler)
@ -88,18 +83,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
private var foundToken: FoundToken? = null
init {
if (walletFeatureToggles.isRedesignedScreenEnabled) {
viewModelScope.launch(dispatchers.main) {
currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold(
ifLeft = { emptyList() },
ifRight = { selectedWallet ->
getCurrenciesUseCase(selectedWallet.walletId).fold(
ifLeft = { emptyList() },
ifRight = { it },
)
},
)
}
viewModelScope.launch(dispatchers.main) {
currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold(
ifLeft = { emptyList() },
ifRight = { selectedWallet ->
getCurrenciesUseCase(selectedWallet.walletId).fold(
ifLeft = { emptyList() },
ifRight = { it },
)
},
)
}
}
@ -591,14 +584,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 +606,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,22 +615,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) }
}
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
when {
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {

View file

@ -2,13 +2,11 @@ package com.tangem.tap.features.demo
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.dispatchNotification
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
@ -22,10 +20,7 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
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.domain.model.Currency
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

@ -1,10 +0,0 @@
package com.tangem.tap.features.details
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
class DarkThemeFeatureToggle(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isDarkThemeEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "DARK_THEME_ENABLED")
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.details.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : DetailsFeatureToggles {
override val isRedesignedAppCurrencySelectorEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED")
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.details.featuretoggles
interface DetailsFeatureToggles {
val isRedesignedAppCurrencySelectorEnabled: Boolean
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.details.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object DetailsFeatureTogglesModule {
@Provides
fun provideDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
return DefaultDetailsFeatureToggles(featureTogglesManager)
}
}

View file

@ -2,18 +2,17 @@ package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
val scanResponse: ScanResponse,
val darkThemeSwitchEnabled: Boolean,
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
@ -92,9 +91,9 @@ sealed class DetailsAction : Action {
) : AppSettings()
data class ChangeAppCurrency(
val fiatCurrency: FiatCurrency,
val currency: AppCurrency,
) : AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction()
}

View file

@ -36,8 +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
import com.tangem.utils.coroutines.saveIn
@ -233,9 +231,8 @@ class DetailsMiddleware {
changeBalanceHiding(action.hideBalance)
}
is DetailsAction.AppSettings.ChangeAppCurrency -> {
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(GlobalAction.ChangeAppCurrency(action.currency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.currency))
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
@ -381,8 +378,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")
@ -391,7 +386,6 @@ class DetailsMiddleware {
private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
@ -399,7 +393,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

@ -45,7 +45,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
is DetailsAction.ChangeAppCurrency -> detailsState.copy(
appSettingsState = detailsState.appSettingsState.copy(
selectedFiatCurrency = action.fiatCurrency,
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
@ -74,7 +74,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = action.shouldSaveUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
selectedFiatCurrency = store.state.globalState.appCurrency,
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = runBlocking {
store.state.daggerGraphState
.get { appThemeModeRepository }.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT
@ -83,7 +83,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
store.state.daggerGraphState
.get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings
},
darkThemeSwitchEnabled = action.darkThemeSwitchEnabled,
),
)
}
@ -258,7 +257,7 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
)
is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedFiatCurrency = action.fiatCurrency,
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
import java.util.EnumSet
@ -62,9 +62,8 @@ data class AppSettingsState(
val needEnrollBiometrics: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
val selectedAppCurrency: AppCurrency = AppCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
val darkThemeSwitchEnabled: Boolean = false,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -74,8 +74,6 @@ sealed class WalletConnectAction : Action {
) :
WalletConnectAction()
data class SetDataToSend(val transactionData: WcTransactionData) : WalletConnectAction()
data class HandlePersonalSignRequest(
val message: WCEthereumSignMessage,
val session: WalletConnectSession,

View file

@ -1,20 +1,24 @@
package com.tangem.tap.features.details.redux.walletconnect
import androidx.core.os.bundleOf
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
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.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -29,11 +33,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
@ -97,7 +101,14 @@ class WalletConnectMiddleware {
if (uri != null && isWalletConnectUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
store.dispatchOnMain(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
),
),
)
}
}
is WalletConnectAction.SelectNetwork -> {
@ -115,7 +126,6 @@ class WalletConnectMiddleware {
scope.launch {
prepareWalletManager(
scanResponse = data.scanResponse,
walletState = store.state.walletState,
blockchain = action.blockchain,
session = data.session,
walletConnectManager = walletConnectManager,
@ -176,8 +186,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 +290,6 @@ class WalletConnectMiddleware {
val walletManager = getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = store.state.walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -372,19 +387,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 +403,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 +456,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 +485,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 +495,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

@ -73,9 +73,7 @@ data class WalletForSession(
} else if (other.derivedPublicKey != null) return false
if (derivationPath != other.derivationPath) return false
if (isTestNet != other.isTestNet) return false
if (blockchain != other.blockchain) return false
return true
return blockchain == other.blockchain
}
override fun hashCode(): Int {

View file

@ -7,7 +7,6 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
@ -21,14 +20,11 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
lateinit var detailsFeatureToggles: DetailsFeatureToggles
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) {
AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository)
AppSettingsViewModel(store, appCurrencyRepository)
}
@Composable

View file

@ -11,16 +11,13 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.entities.FiatCurrency
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.details.featuretoggles.DetailsFeatureToggles
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
@ -32,7 +29,6 @@ import org.rekotlin.Store
internal class AppSettingsViewModel(
private val store: Store<AppState>,
private val detailsFeatureToggles: DetailsFeatureToggles,
private val appCurrencyRepository: AppCurrencyRepository,
) {
@ -45,9 +41,7 @@ internal class AppSettingsViewModel(
private set
init {
if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
bootstrapAppCurrencyUpdates()
}
bootstrapAppCurrencyUpdates()
}
fun updateState(state: DetailsState) {
@ -64,11 +58,12 @@ internal class AppSettingsViewModel(
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
val items = buildList {
if (state.needEnrollBiometrics) {
Analytics.send(Settings.AppSettings.EnableBiometrics)
itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add)
}
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = state.selectedFiatCurrency.name,
currentAppCurrencyName = state.selectedAppCurrency.name,
onClick = ::showAppCurrencySelector,
).let(::add)
@ -94,11 +89,9 @@ internal class AppSettingsViewModel(
onCheckedChange = ::onFlipToHideBalanceToggled,
).let(::add)
if (state.darkThemeSwitchEnabled) {
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
showThemeModeSelector(state.selectedThemeMode)
}.let(::add)
}
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
showThemeModeSelector(state.selectedThemeMode)
}.let(::add)
}
return items.toImmutableList()
@ -109,13 +102,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) {
@ -179,6 +166,9 @@ internal class AppSettingsViewModel(
}
private fun onFlipToHideBalanceToggled(enable: Boolean) {
val param = AnalyticsParam.OnOffState(enable)
Analytics.send(Settings.AppSettings.HideBalanceChanged(param))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable))
}
@ -192,8 +182,7 @@ internal class AppSettingsViewModel(
.onEach {
if (it.code == store.state.globalState.appCurrency.code) return@onEach
val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) }
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency))
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(it))
}
.launchIn(scope)
.saveIn(appCurrencyUpdatesJobHolder)

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

@ -9,7 +9,6 @@ import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.DarkThemeFeatureToggle
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
@ -22,9 +21,6 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle
@Inject
lateinit var walletsRepository: WalletsRepository
@ -32,7 +28,7 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
detailsViewModel = DetailsViewModel(store, darkThemeFeatureToggle, walletsRepository)
detailsViewModel = DetailsViewModel(store, walletsRepository)
Analytics.send(Settings.ScreenOpened())
}

View file

@ -122,7 +122,6 @@ internal data class SocialNetworkLink(
internal sealed class EventError {
object Empty : EventError()
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
}
sealed class SocialNetwork(val id: String, val iconRes: Int) {

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,19 +13,18 @@ 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
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.DarkThemeFeatureToggle
import com.tangem.tap.features.details.redux.DetailsAction
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,11 +37,11 @@ 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(
private val store: Store<AppState>,
private val darkThemeFeatureToggle: DarkThemeFeatureToggle,
private val walletsRepository: WalletsRepository,
) {
@ -158,7 +158,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() {
@ -192,7 +200,6 @@ internal class DetailsViewModel(
store.dispatchWithMain(
DetailsAction.PrepareScreen(
scanResponse = selectedUserWallet.scanResponse,
darkThemeSwitchEnabled = darkThemeFeatureToggle.isDarkThemeEnabled,
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
),
)

View file

@ -1,108 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.common.util.concurrent.ListenableFuture
import com.otaliastudios.cameraview.CameraView
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PreviewBinder
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
internal class QrScanFragment : Fragment(R.layout.layout_qr_scanning) {
private val binding: LayoutQrScanningBinding by viewBinding(LayoutQrScanningBinding::bind)
private val binder = PreviewBinder()
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var cameraExecutor: ExecutorService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFitSystemWindows(fit = true)
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
},
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!permissionIsGranted()) requestPermission()
cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraExecutor = Executors.newSingleThreadExecutor()
cameraProviderFuture?.addListener(
{
val cameraProvider = cameraProviderFuture?.get()
binder.bindPreview(
context = requireContext(),
binding = binding,
lifecycleOwner = this,
cameraProvider = requireNotNull(cameraProvider),
cameraExecutor = requireNotNull(cameraExecutor),
onScanned = { result ->
store.dispatch(NavigationAction.PopBackTo())
setFitSystemWindows(fit = false)
if (result.isNotBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result))
}
},
)
},
ContextCompat.getMainExecutor(requireContext()),
)
binding.overlay.post {
binding.overlay.setViewFinder()
}
}
override fun onDestroy() {
super.onDestroy()
setFitSystemWindows(fit = false)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
store.dispatch(WalletConnectAction.NotifyCameraPermissionIsRequired)
store.dispatch(NavigationAction.PopBackTo())
}
}
private fun permissionIsGranted(): Boolean {
val cameraPermission = ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
return cameraPermission == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE)
}
private fun setFitSystemWindows(fit: Boolean) {
activity?.window?.let {
WindowCompat.setDecorFitsSystemWindows(it, fit)
}
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.screen.ComposeFragment
@ -23,22 +24,25 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<Wallet
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = WalletConnectViewModel(store)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
private val viewModel: WalletConnectViewModel by viewModels()
private var screenState: MutableState<WalletConnectScreenState>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(WalletConnect.ScreenOpened())
lifecycle.addObserver(viewModel)
screenState = mutableStateOf(viewModel.updateState(store.state.walletConnectState))
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val state = screenState?.value ?: return
WalletConnectScreen(
modifier = modifier,
state = screenState.value,
state = state,
onBackClick = {
if (screenState.value.isLoading) {
if (state.isLoading) {
store.dispatch(
WalletConnectAction.FailureEstablishingSession(
store.state.walletConnectState.newSessionData?.session?.session,
@ -66,6 +70,6 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<Wallet
override fun newState(state: WalletConnectState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state)
screenState?.value = viewModel.updateState(state)
}
}

View file

@ -1,14 +1,34 @@
package com.tangem.tap.features.details.ui.walletconnect
import com.tangem.tap.common.redux.AppState
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.feature.qrscanning.SourceType
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Store
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class WalletConnectViewModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
) : ViewModel(), DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
viewModelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.flowWithLifecycle(owner.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect { store.dispatch(WalletConnectAction.OpenSession(it)) }
}
}
internal class WalletConnectViewModel(private val store: Store<AppState>) {
fun updateState(state: WalletConnectState): WalletConnectScreenState {
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions

View file

@ -2,9 +2,12 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
@ -19,7 +22,14 @@ object ClipboardOrScanQrDialog {
store.dispatch(WalletConnectAction.OpenSession(wcUri))
}
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan))
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
),
),
)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)

View file

@ -1,69 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import android.util.Size
import android.view.OrientationEventListener
import android.view.Surface
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.lifecycle.LifecycleOwner
import com.tangem.tap.common.qrCodeScan.MLKitBarcodeAnalyzer
import com.tangem.wallet.databinding.LayoutQrScanningBinding
import java.util.concurrent.ExecutorService
internal class PreviewBinder {
@Suppress("LongParameterList")
fun bindPreview(
context: Context,
binding: LayoutQrScanningBinding,
lifecycleOwner: LifecycleOwner,
cameraProvider: ProcessCameraProvider,
cameraExecutor: ExecutorService,
onScanned: (String) -> Unit,
) {
cameraProvider.unbindAll()
val preview: Preview = Preview.Builder()
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(binding.cameraPreview.width, binding.cameraPreview.height))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val orientationEventListener = object : OrientationEventListener(context) {
@Suppress("MagicNumber")
override fun onOrientationChanged(orientation: Int) {
val rotation: Int = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageAnalysis.targetRotation = rotation
}
}
orientationEventListener.enable()
val analyzer: ImageAnalysis.Analyzer = MLKitBarcodeAnalyzer {
imageAnalysis.clearAnalyzer()
onScanned.invoke(it)
}
cameraExecutor.let {
imageAnalysis.setAnalyzer(it, analyzer)
}
preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
}
}

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

@ -34,8 +34,6 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
// private val learn2earnViewModel by activityViewModels<Learn2earnViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate)
@ -44,11 +42,6 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Composable
override fun ScreenContent(modifier: Modifier) {
// sync adding story before screen creation
// if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
// store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
// homeState.value = store.state.homeState
// }
BackHandler(onBack = requireActivity()::finish)
SystemBarsEffect {
setSystemBarsColor(color = Color.Transparent, darkIcons = false)
@ -83,7 +76,6 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
private fun ScreenContent() {
StoriesScreen(
homeState = homeState,
onLearn2earnClick = {}, // learn2earnViewModel.uiState.storyScreenState.onClick,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.home
import android.content.Context
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.PHONE_TYPE_CDMA
import androidx.compose.ui.text.intl.Locale
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
@ -13,39 +9,8 @@ interface RegionProvider {
fun getRegion(): String?
}
class RegionService(
private val providers: List<RegionProvider>,
) : RegionProvider {
override fun getRegion(): String? {
for (provider in providers) {
val region = provider.getRegion()
if (region != null) return region
}
return null
}
}
class TelephonyManagerRegionProvider(context: Context) : RegionProvider {
private val wContext: WeakReference<Context> = WeakReference(context)
override fun getRegion(): String? {
val tm = wContext.get()?.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager ?: return null
val region = when (tm.phoneType) {
PHONE_TYPE_CDMA -> {
// Result may be unreliable
tm.networkCountryIso
}
else -> tm.networkCountryIso
}
return region.ifEmpty { return null }
}
}
class LocaleRegionProvider : RegionProvider {
override fun getRegion(): String = Locale.current.region
}
const val RUSSIA_COUNTRY_CODE = "ru"
const val BELARUS_COUNTRY_CODE = "by"
const val RUSSIA_COUNTRY_CODE = "ru"

View file

@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.learn2earn.presentation.ui.Learn2earnStoriesScreen
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton
@ -33,7 +32,6 @@ import kotlin.math.max
@Composable
fun StoriesScreen(
homeState: MutableState<HomeState>,
onLearn2earnClick: () -> Unit,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit,
onSearchTokensClick: () -> Unit,
@ -65,7 +63,6 @@ fun StoriesScreen(
isScanInProgress = homeState.value.scanInProgress,
onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory,
onLearn2earnClick = onLearn2earnClick,
onSearchTokensClick = onSearchTokensClick,
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick,
@ -152,7 +149,6 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
.align(Alignment.Start),
)
when (config.currentStory) {
Stories.OneInchPromo -> Learn2earnStoriesScreen(config.onLearn2earnClick)
Stories.TangemIntro -> FirstStoriesContent(
isPaused = isPaused,
duration = currentStoryDuration,
@ -187,18 +183,12 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
)
}
AnimatedVisibility(
visible = config.currentStory != Stories.OneInchPromo,
enter = fadeIn(),
exit = fadeOut(),
) {
HomeButtons(
modifier = Modifier.fillMaxWidth(),
btnScanStateInProgress = config.isScanInProgress,
onScanButtonClick = config.onScanButtonClick,
onShopButtonClick = config.onShopButtonClick,
)
}
HomeButtons(
modifier = Modifier.fillMaxWidth(),
btnScanStateInProgress = config.isScanInProgress,
onScanButtonClick = config.onScanButtonClick,
onShopButtonClick = config.onShopButtonClick,
)
}
}
}
@ -210,7 +200,6 @@ private data class StoriesScreenContentConfig(
val isScanInProgress: Boolean,
val onGoToPreviousStory: () -> Unit = {},
val onGoToNextStory: () -> Unit = {},
val onLearn2earnClick: () -> Unit = {},
val onSearchTokensClick: () -> Unit = {},
val onScanButtonClick: () -> Unit = {},
val onShopButtonClick: () -> Unit = {},
@ -265,12 +254,6 @@ private class StoriesScreenContentConfigProvider : CollectionPreviewParameterPro
currentStory = Stories.WalletForEveryone,
isScanInProgress = false,
),
StoriesScreenContentConfig(
storiesSize = 6,
currentStoryIndex = 6,
currentStory = Stories.OneInchPromo,
isScanInProgress = false,
),
),
)
// endregion Preview

View file

@ -37,7 +37,6 @@ import timber.log.Timber
object HomeMiddleware {
val handler = homeMiddleware
const val BUY_WALLET_URL = "https://tangem.com/ru/resellers/"
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/"
}

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.tap.common.entities.IndeterminateProgressButton
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
@ -17,9 +16,6 @@ data class HomeState(
val firstStory: Stories
get() = stories[0]
val btnScanStateInProgress: Boolean
get() = btnScanState.progressState == ProgressState.Loading
fun stepOf(story: Stories): Int = stories.indexOf(story)
fun onCountryCodeUpdate(homeState: HomeState, countryCode: String) {
@ -52,7 +48,6 @@ sealed class Stories(
val duration: Int,
val isNewWalletAvailable: MutableState<Boolean> = mutableStateOf(HomeState.isNewWalletAvailableInit()),
) {
object OneInchPromo : Stories(duration = 8000)
object TangemIntro : Stories(duration = 6000)
object RevolutionaryWallet : Stories(duration = 6000)
object UltraSecureBackup : Stories(duration = 6000)

View file

@ -15,15 +15,11 @@ class IntentProcessor {
intentHandlers.add(handler)
}
fun removeIntentHandler(handler: IntentHandler) {
intentHandlers.remove(handler)
}
fun removeAll() {
intentHandlers.clear()
}
suspend fun handleIntent(intent: Intent?) {
fun handleIntent(intent: Intent?) {
intentHandlers.forEach {
it.handleIntent(intent)
}

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: [REDACTED_JIRA]
// 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,11 +1,7 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -13,33 +9,36 @@ import timber.log.Timber
class SellCurrencyIntentHandler : IntentHandler {
override fun handleIntent(intent: Intent?): Boolean {
return try {
val intentData = intent?.data ?: return false
val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
// FIXME: [REDACTED_JIRA]
// return try {
// val intentData = intent?.data ?: return false
// val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
// val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
// val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
// val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
//
// Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
// store.dispatchOnMain(
// TradeCryptoAction.SendCrypto(
// currencyId = currency,
// amount = amount,
// destinationAddress = destinationAddress,
// transactionId = transactionID,
// ),
// )
// true
// } catch (exception: Exception) {
// Timber.d("Not MoonPay URL")
// false
// }
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatchOnMain(
TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
true
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
false
}
return false
}
private companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
}
// private companion object {
// private const val TRANSACTION_ID_PARAM = "transactionId"
// private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
// private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
// private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
// }
}

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

@ -54,7 +54,7 @@ object OnboardingHelper {
}
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
return when (scanResponse.productType) {
return when (val type = scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Wallet,
ProductType.Wallet2,
@ -65,9 +65,9 @@ object OnboardingHelper {
AppScreen.OnboardingOther
}
ProductType.Twins -> AppScreen.OnboardingTwins
ProductType.Start2Coin -> throw java.lang.UnsupportedOperationException(
"Onboarding for Start2Coin cards is not supported",
)
ProductType.Start2Coin,
ProductType.Visa,
-> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported")
}
}

View file

@ -5,17 +5,17 @@ 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.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.domain.model.Currency
import com.tangem.tap.domain.model.hasPendingTransactions
import com.tangem.tap.features.demo.isDemoCard
import timber.log.Timber
import java.math.BigDecimal
@ -25,11 +25,10 @@ import java.math.BigDecimal
*/
class OnboardingManager(
var scanResponse: ScanResponse,
val usedCardsPrefStorage: UsedCardsPrefStorage,
private val usedCardsPrefStorage: UsedCardsPrefStorage,
) {
var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
private set
private var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
suspend fun loadArtworkUrl(): String {
val cardInfo = cardInfo
@ -85,10 +84,6 @@ class OnboardingManager(
usedCardsPrefStorage.activationFinished(cardId)
}
fun isActivationFinished(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationFinished(cardId)
}
fun isActivationStarted(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationStarted(cardId)
}

View file

@ -8,20 +8,18 @@ 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
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.Currency
import com.tangem.tap.features.demo.DemoHelper
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
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -104,7 +102,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> Unit
@ -153,8 +150,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(scanResponse)
store.state.globalState.topUpController?.send(scanResponse, AnalyticsParam.CardBalanceState.Full)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
@ -178,8 +173,8 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}

View file

@ -1,22 +1,16 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -89,40 +83,6 @@ private fun handleOtherCardsAction(action: Action) {
val updatedCard = updatedResponse.card
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedCard.cardId)
store.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
val primaryBlockchain = updatedResponse.cardTypesResolver.getBlockchain()
val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) {
val primaryToken = updatedResponse.cardTypesResolver.getPrimaryToken()
val blockchainNetwork =
BlockchainNetwork(
blockchain = primaryBlockchain,
derivationStyleProvider = updatedResponse.derivationStyleProvider,
)
.updateTokens(
listOfNotNull(primaryToken),
)
listOf(blockchainNetwork)
} else {
listOf(
BlockchainNetwork(
blockchain = Blockchain.Bitcoin,
derivationStyleProvider = updatedResponse.derivationStyleProvider,
),
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
derivationStyleProvider = updatedResponse.derivationStyleProvider,
),
)
}
scope.launch {
// TODO: Use new repo [REDACTED_JIRA]
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))

View file

@ -14,20 +14,18 @@ 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
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.Currency
import com.tangem.tap.domain.twins.TwinCardsManager
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
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@ -202,7 +200,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
is Result.Success -> {
Analytics.send(Onboarding.Twins.SetupFinished())
updateScanResponse(result.data)
store.state.globalState.topUpController?.registerEmptyWallet(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
@ -261,8 +258,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
OnboardingHelper.sendToppedUpEvent(getScanResponse())
store.state.globalState.topUpController?.send(getScanResponse(), AnalyticsParam.CardBalanceState.Full)
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
@ -285,8 +280,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}

View file

@ -207,7 +207,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
tvHeader.setText(R.string.common_warning)
tvBody.setText(R.string.twins_recreate_warning)
chbUnderstand.setOnCheckedChangeListener { buttonView, isChecked ->
chbUnderstand.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
@ -11,11 +10,9 @@ import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
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.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.Artwork
@ -36,7 +33,6 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
@ -142,26 +138,7 @@ private fun handleWalletAction(action: Action) {
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
store.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
val blockchainNetworks = if (DemoHelper.isDemoCardId(result.data.card.cardId)) {
DemoHelper.config.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationStyleProvider = updatedResponse.derivationStyleProvider,
)
}
scope.launch {
// TODO: Use new repo [REDACTED_JIRA]
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}

View file

@ -250,7 +250,7 @@ class TestBackupAnimation(
}
@Suppress("MagicNumber")
fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) {
private fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) {
steps = step
when (steps) {
0 -> setupCreateWalletState()

View file

@ -140,7 +140,7 @@ class OnboardingWalletFragment :
}
private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) {
cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor) { 200f * deviceScaleFactor }
cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor)
animator = if (isTest) {
TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding)
} else {

View file

@ -7,19 +7,16 @@ import android.view.View
import android.widget.ImageView
import androidx.core.animation.doOnEnd
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
val getTopOfAnchorViewForActivateState: () -> Float,
) {
private val animDuration: Long = 400
var currentState: WidgetState? = null
private set
private var currentState: WidgetState? = null
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
if (currentState == WidgetState.WELCOME) return
@ -239,15 +236,5 @@ private data class CardProperties(
)
}
companion object {
fun from(leapViewState: LeapViewState): CardProperties {
val leapViewProperties = leapViewState.properties
return CardProperties(
yTranslation = leapViewProperties.yTranslation,
elevation = leapViewProperties.elevationEnd,
scale = leapViewProperties.scale,
)
}
}
companion object
}

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

@ -11,7 +11,6 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
@ -39,20 +38,15 @@ import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.wallet.models.Currency
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 com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.EnumSet
/**
@ -277,9 +271,6 @@ private fun sendTransaction(
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
updateAfterTransaction(walletManager)
}
}
is SimpleResult.Failure -> {
updateFeedbackManagerInfo(
@ -418,32 +409,4 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}
private suspend fun updateAfterTransaction(walletManager: WalletManager) {
val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
updateWalletsLegacy(walletManager)
}
}
private suspend fun updateWalletsLegacy(walletManager: WalletManager) {
updateWallet(walletManager)
delay(timeMillis = 11000) // more than 10000 to avoid throttling
updateWallet(walletManager)
}
private suspend fun updateWallet(walletManager: WalletManager) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to update wallet, no user wallet selected")
return
}
val wallet = walletManager.wallet
walletCurrenciesManager.update(
userWallet = selectedUserWallet,
currency = Currency.Blockchain(
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
)
}

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

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
@ -128,7 +128,7 @@ data class AmountState(
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, AppCurrency.Default.code),
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val hideBalance: Boolean = false,

View file

@ -3,30 +3,38 @@
package com.tangem.tap.features.send.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.method.DigitsKeyListener
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.os.bundleOf
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
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 arrow.core.getOrElse
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.SourceType
import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
@ -40,8 +48,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
@ -49,8 +57,11 @@ import com.tangem.wallet.databinding.FragmentSendBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.text.DecimalFormatSymbols
import javax.inject.Inject
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
@ -73,11 +84,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
sendSubscriber.initViewModel(viewModel)
Analytics.send(Token.Send.ScreenOpened())
listenToQrCode()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -145,13 +160,34 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
startActivityForResult(
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE,
store.dispatchOnMain(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.SEND,
),
),
)
}
}
private fun listenToQrCode() {
lifecycleScope.launch {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect {
delay(200)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
onCodeScanned(it)
}
}
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
@ -200,27 +236,16 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
private fun onCodeScanned(scannedCode: String) {
if (scannedCode.isEmpty()) return
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
binding.lSendAddress.imvQrCode.postDelayed(
{
store.dispatch(
PasteAddress(
data = scannedCode,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
},
200,
store.dispatch(
PasteAddress(
data = scannedCode,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
}
private fun setupAmountLayout() {
@ -328,7 +353,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun restoreMainCurrency(): MainCurrencyType {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
val mainCurrency = sp.getString("mainCurrency", FiatCurrency.Default.code)
val mainCurrency = sp.getString("mainCurrency", AppCurrency.Default.code)
return MainCurrencyType.values()
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
?: MainCurrencyType.CRYPTO

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,27 +80,15 @@ 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()
val buttonAction =
when (warning.titleResId) {
// R.string.warning_important_security_info -> {
// View.OnClickListener {
// store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog)
// }
// }
else -> {
View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}
}
val buttonAction = View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
val buttonTitle = binding.root.getString(
warning.buttonTextId ?: R.string.how_to_got_it_button,
)
@ -110,37 +97,19 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
}
WarningMessage.Type.AppRating -> {
binding.groupControlsTemporary.hide()
binding.groupControlsRating.show()
binding.btnClose.show()
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))
// store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
// store.dispatch(GlobalAction.HideWarningMessage(warning))
// store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
// }
binding.btnReallyCool.setOnClickListener {
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 {
if (it.isSuccessful) {
val reviewFlow = reviewManager.launchReviewFlow(activity, it.result)
reviewFlow.addOnCompleteListener {
if (it.isSuccessful) {
// send review was succeed
} else {
// send fails
}
}
} else {
if (!it.isSuccessful) {
Timber.e(task.exception)
}
}.addOnFailureListener {

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,6 +3,7 @@ package com.tangem.tap.features.tokens.impl.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.toNetworkId
@ -50,7 +51,7 @@ internal class TangemApiTokensPagingSource(
searchText = searchText,
offset = page * params.loadSize,
limit = params.loadSize,
)
).getOrThrow()
}.fold(
onSuccess = { response ->
LoadResult.Page(

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.tokens.impl.data.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.utils.converter.Converter
@ -14,6 +13,8 @@ import com.tangem.utils.converter.Converter
*/
internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
private const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
override fun convert(value: CoinsResponse): List<Token> {
return value.coins.map { token ->
Token(
@ -35,4 +36,8 @@ internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
)
}
}
fun getIconUrl(id: String, imageHost: String? = null): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.tokens.impl.data.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.utils.converter.Converter
@ -20,7 +19,7 @@ internal object TestnetTokensConfigConverter : Converter<TestnetTokensConfig, Li
id = token.id,
name = token.name,
symbol = token.symbol,
iconUrl = getIconUrl(id = token.id, imageHost = null),
iconUrl = CoinsResponseConverter.getIconUrl(token.id),
networks = token.networks?.mapNotNull { network ->
val blockchain = Blockchain.fromNetworkId(network.id) ?: return@mapNotNull null
@ -28,7 +27,7 @@ internal object TestnetTokensConfigConverter : Converter<TestnetTokensConfig, Li
id = network.id,
blockchain = blockchain,
address = network.address,
iconUrl = getIconUrl(id = network.id, imageHost = null),
iconUrl = CoinsResponseConverter.getIconUrl(network.id),
decimalCount = network.decimalCount,
)
}.orEmpty(),

View file

@ -15,6 +15,8 @@ internal interface TokensListRepository {
* Get available tokens list
*
* @param searchText search text
*
* @throws com.tangem.datasource.api.common.response.ApiResponseError
*/
fun getAvailableTokens(searchText: String?): Flow<PagingData<Token>>
}

View file

@ -6,7 +6,6 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -32,13 +31,13 @@ internal class DefaultTokensListRouter : TokensListRouter {
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String) {
store.dispatchDialogShow(
dialog = WalletDialog.TokensAreLinkedDialog(currencyTitle = tokenName, currencySymbol = tokenSymbol),
dialog = AppDialog.TokensAreLinkedDialog(currencyTitle = tokenName, currencySymbol = tokenSymbol),
)
}
override fun openRemoveWalletAlert(tokenName: String, onOkClick: () -> Unit) {
store.dispatchDialogShow(
dialog = WalletDialog.RemoveWalletDialog(currencyTitle = tokenName, onOk = onOkClick),
dialog = AppDialog.RemoveWalletDialog(currencyTitle = tokenName, onOk = onOkClick),
)
}

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
@ -12,9 +11,6 @@ import com.tangem.domain.tokens.TokensAction
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
@ -22,12 +18,10 @@ import kotlin.properties.Delegates
/**
* Class that divide a new and legacy logic when user uses tokens list screen
*
* @property walletFeatureToggles wallet feature toggles
* @property getSelectedWalletSyncUseCase use case that returns selected wallet
* @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet
*/
internal class TokensListMigration(
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) {
@ -39,14 +33,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,60 +76,12 @@ 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>,
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
if (walletFeatureToggles.isRedesignedScreenEnabled) {
saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList)
} else {
saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList)
}
}
private fun saveByNewWay(
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
store.dispatch(
action = TokensAction.NewSaveChanges(
action = TokensAction.SaveChanges(
currentTokens = currentNewTokens,
currentCoins = currentNewCoins,
changedTokens = changedTokensList.mapNotNull {
@ -165,23 +103,4 @@ internal class TokensListMigration(
),
)
}
private fun saveByOldWay(
currentTokensList: List<TokenWithBlockchain>,
currentBlockchainList: List<Blockchain>,
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
val scanResponse = store.state.globalState.scanResponse ?: return
store.dispatch(
action = TokensAction.LegacySaveChanges(
currentTokens = currentTokensList,
currentBlockchains = currentBlockchainList,
changedTokens = changedTokensList,
changedBlockchains = changedBlockchainList,
scanResponse = scanResponse,
),
)
}
}

View file

@ -22,7 +22,6 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
@ -70,7 +69,6 @@ internal class TokensListViewModel @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
analyticsEventHandler: AnalyticsEventHandler,
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
walletFeatureToggles: WalletFeatureToggles,
) : ViewModel(), DefaultLifecycleObserver {
private val isManageAccess = store.state.tokensState.isManageAccess
@ -88,7 +86,6 @@ internal class TokensListViewModel @Inject constructor(
private var changedBlockchainList: MutableList<Blockchain> = mutableListOf()
private val tokensListMigration = TokensListMigration(
walletFeatureToggles = walletFeatureToggles,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getCurrenciesUseCase = getCurrenciesUseCase,
)
@ -307,8 +304,6 @@ internal class TokensListViewModel @Inject constructor(
fun onSaveButtonClick() {
analyticsSender.sendWhenSaveButtonClicked()
tokensListMigration.onSaveButtonClick(
currentTokensList = currentTokensList,
currentBlockchainList = currentBlockchainList,
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)

View file

@ -2,14 +2,11 @@ package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.core.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
@ -17,23 +14,22 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
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.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -53,15 +49,14 @@ object TokensMiddleware {
{ next ->
{ action ->
when (action) {
is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action)
is TokensAction.NewSaveChanges -> handleNewSaveChanges(action)
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
}
}
}
private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) {
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
scope.launch {
val scanResponse = action.userWallet.scanResponse
@ -74,7 +69,7 @@ object TokensMiddleware {
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeNewCurrenciesIfNeeded(
removeCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
@ -89,146 +84,40 @@ object TokensMiddleware {
val currencyList = blockchainsToAdd + tokensToAdd
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitNewAdd(
userWallet = action.userWallet,
updatedScanResponse = it,
currencyList = currencyList,
)
}
} else {
submitNewAdd(
userWallet = action.userWallet,
updatedScanResponse = scanResponse,
currencyList = currencyList,
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::testerFeatureToggles)
if (featureToggles.isDerivePublicKeysRefactoringEnabled) {
val derivePublicKeys = DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = store.state.daggerGraphState.get(DaggerGraphState::derivationsRepository),
)
}
}
}
private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) {
scope.launch {
val scanResponse = action.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentBlockchains
val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove =
currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } }
removeLegacyCurrenciesIfNeeded(
currencies = convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
),
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = convertToCurrencies(
blockchains = blockchainsToAdd,
tokens = tokensToAdd,
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
)
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitLegacyAdd(it, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitLegacyAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
}
private fun convertToCurrencies(
blockchains: List<Blockchain>,
tokens: List<TokenWithBlockchain>,
derivationStyle: DerivationStyle?,
): List<Currency> {
return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } +
tokens.map {
Currency.Token(
token = it.token,
blockchain = it.blockchain,
derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath,
)
}
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: (ScanResponse) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getLegacyDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
derivePublicKeys(userWalletId = action.userWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(
userWalletId = action.userWallet.walletId,
currencies = currencyList,
)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitAdd(
userWallet = action.userWallet,
updatedScanResponse = it,
currencyList = currencyList,
)
}
} else {
submitAdd(action.userWallet, scanResponse, currencyList)
}
}
}
}
@Deprecated(message = "Use DerivePublicKeysUseCase instead")
private fun deriveMissingCoins(
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
@ -237,7 +126,7 @@ object TokensMiddleware {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))
curve?.let { getNewDerivations(curve, scanResponse, currency) }
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
@ -286,42 +175,7 @@ object TokensMiddleware {
}
}
private fun getLegacyDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: Currency,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private fun getNewDerivations(
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
@ -359,28 +213,7 @@ object TokensMiddleware {
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected")
return
}
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(scanResponse = scanResponse)
},
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = currencyList,
)
}
}
}
private fun submitNewAdd(
private fun submitAdd(
userWallet: UserWallet,
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
@ -390,24 +223,13 @@ object TokensMiddleware {
userWalletId = userWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
).doOnSuccess {
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight {
store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet))
}
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList)
}
}
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isEmpty()) return
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove currencies, no user wallet selected")
return
}
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
}
private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)

View file

@ -1,69 +0,0 @@
package com.tangem.tap.features.wallet.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import com.tangem.utils.converter.TwoWayConverter
internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
override fun convert(value: Currency): CryptoCurrency {
return when (value) {
is Currency.Blockchain -> requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
derivationStyleProvider = requireNotNull(
store.state.globalState
.userWalletsListManager
?.selectedUserWalletSync
?.scanResponse
?.derivationStyleProvider,
),
),
)
is Currency.Token -> requireNotNull(
cryptoCurrencyFactory.createToken(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
derivationStyleProvider = requireNotNull(
store.state.globalState
.userWalletsListManager
?.selectedUserWalletSync
?.scanResponse
?.derivationStyleProvider,
),
),
)
}
}
override fun convertBack(value: CryptoCurrency): Currency {
val blockchain = Blockchain.fromId(value.network.id.value)
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain(
blockchain = blockchain,
derivationPath = value.network.derivationPath.value,
)
is CryptoCurrency.Token -> Currency.Token(
token = Token(
name = value.name,
symbol = value.symbol,
contractAddress = value.contractAddress,
decimals = value.decimals,
id = value.id.value,
),
blockchain = blockchain,
derivationPath = value.network.derivationPath.value,
)
}
}
}

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,131 +0,0 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.blockchain.common.Blockchain as SdkBlockchain
import com.tangem.blockchain.common.Token as SdkToken
sealed interface Currency {
val coinId: String?
get() = when (this) {
is Blockchain -> blockchain.toCoinId()
is Token -> token.id
}
val blockchain: SdkBlockchain
val currencySymbol: CryptoCurrencyName
val derivationPath: String?
val currencyName: String
get() = when (this) {
is Blockchain -> blockchain.fullName
is Token -> token.name
}
val decimals
get() = when (this) {
is Blockchain -> blockchain.decimals()
is Token -> token.decimals
}
data class Token(
val token: SdkToken,
override val blockchain: SdkBlockchain,
override val derivationPath: String?,
) : Currency {
override val currencySymbol = token.symbol
}
data class Blockchain(
override val blockchain: SdkBlockchain,
override val derivationPath: String?,
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
if (this is Token && this.token.id == null) return true
if (derivationPath == null || derivationStyle == null) return false
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
}
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
companion object {
fun fromBlockchainNetwork(blockchainNetwork: BlockchainNetwork, token: SdkToken? = null): Currency {
return if (token != null) {
Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath,
)
} else {
Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath,
)
}
}
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
}
}
fun fromTokenResponse(tokenBody: UserTokensResponse.Token): Currency? {
val blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenBody.networkId)
?: return null
return when {
tokenBody.contractAddress != null -> Token(
token = SdkToken(
name = tokenBody.name,
symbol = tokenBody.symbol,
contractAddress = tokenBody.contractAddress!!,
decimals = tokenBody.decimals,
id = tokenBody.id,
),
blockchain = blockchain,
derivationPath = tokenBody.derivationPath,
)
else -> Blockchain(
blockchain = blockchain,
derivationPath = tokenBody.derivationPath,
)
}
}
}
}
fun BlockchainNetwork.toCurrencies(): List<Currency> {
val blockchain = Currency.fromBlockchainNetwork(this)
val tokens = this.tokens.map { Currency.fromBlockchainNetwork(this, it) }
return listOf(blockchain) + tokens
}
fun List<BlockchainNetwork>.toCurrencies(): List<Currency> {
return flatMap { it.toCurrencies() }
}
fun List<Currency>.toBlockchainNetworks(): List<BlockchainNetwork> {
return this.filter { it.isBlockchain() }.map { BlockchainNetwork(it.blockchain, it.derivationPath, getTokens(it)) }
}
fun List<Currency>.getTokens(currency: Currency): List<SdkToken> {
return this
.filter { it.isToken() && it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
.mapNotNull { if (it is Currency.Token) it.token else null }
}

View file

@ -1,86 +0,0 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.extensions.toFormattedString
import java.math.BigDecimal
data class PendingTransaction(
val transactionData: TransactionData,
val type: PendingTransactionType,
) {
val address: String? = when (type) {
PendingTransactionType.Incoming -> nullIfUnknown(transactionData.sourceAddress)
PendingTransactionType.Outgoing -> nullIfUnknown(transactionData.destinationAddress)
PendingTransactionType.Unknown -> null
}
val amountValue: BigDecimal? = transactionData.amount.value
val amountValueUi: String? = amountValue?.toFormattedString(transactionData.amount.decimals)
val currency: String = transactionData.amount.currencySymbol
private fun nullIfUnknown(address: String): String? = if (address == "unknown") null else address
}
enum class PendingTransactionType { Incoming, Outgoing, Unknown }
fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransaction? {
if (this.status == TransactionStatus.Confirmed) return null
val type: PendingTransactionType = when {
this.sourceAddress == walletAddress -> PendingTransactionType.Outgoing
this.destinationAddress == walletAddress -> PendingTransactionType.Incoming
else -> PendingTransactionType.Unknown
}
return PendingTransaction(this, type)
}
fun List<TransactionData>.toPendingTransactions(walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransaction(walletAddress) }
}
fun List<PendingTransaction>.filterByCoin(): List<PendingTransaction> {
return this.filter { it.transactionData.amount.type == AmountType.Coin }
}
fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? {
if (this.amount.currencySymbol != token.symbol) return null
return this.toPendingTransaction(walletAddress)
}
fun Wallet.getPendingTransactions(type: PendingTransactionType? = null): List<PendingTransaction> {
val txs = recentTransactions.toPendingTransactions(address)
return when (type) {
null -> txs
else -> txs.filter { it.type == type }
}
}
fun Wallet.getPendingTransactions(token: Token): List<PendingTransaction> {
return recentTransactions.mapNotNull { it.toPendingTransactionForToken(token, address) }
}
fun Wallet.hasPendingTransactions(): Boolean {
return getPendingTransactions().isNotEmpty()
}
fun Wallet.getSendableAmounts(): List<Amount> {
return amounts.values
.filter { it.type != AmountType.Reserve }
.filter { it.isAboveZero() }
}
fun Wallet.hasSendableAmounts(): Boolean {
return getSendableAmounts().isNotEmpty()
}
fun Wallet.isSendableAmount(type: AmountType): Boolean {
return amounts[type]?.isAboveZero() == true
}

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

@ -5,7 +5,6 @@ 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
@ -14,45 +13,45 @@ 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.extensions.*
import com.tangem.tap.common.redux.AppDialog
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
import org.rekotlin.Middleware
@Suppress("LargeClass")
class TradeCryptoMiddleware {
@Deprecated("Will be removed soon")
object TradeCryptoMiddleware {
val middleware: Middleware<AppState> = { _, appState ->
{ nextDispatch ->
{ action ->
if (action is TradeCryptoAction) {
handle(appState, action)
}
nextDispatch(action)
}
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
private 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.SendCrypto -> preconfigureAndOpenSendScreen()
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(
@ -63,54 +62,6 @@ class TradeCryptoMiddleware {
}
}
@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
@ -132,15 +83,9 @@ class TradeCryptoMiddleware {
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = topUrl?.let {
WalletDialog.RussianCardholdersWarningDialog.Data(
topUpUrl = it,
)
AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it)
}
store.dispatchOnMain(
WalletAction.DialogAction.RussianCardholdersWarningDialog(
dialogData = dialogData,
),
)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(data = dialogData))
return
}
@ -174,29 +119,6 @@ class TradeCryptoMiddleware {
}
}
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
@ -217,41 +139,31 @@ class TradeCryptoMiddleware {
}
}
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 preconfigureAndOpenSendScreen() = scope.launch {
// FIXME: [REDACTED_JIRA]
// 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) {

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
}
}
}

Some files were not shown because too many files have changed in this diff Show more