Updated on 2026-08-14
This commit is contained in:
commit
6716989364
314 changed files with 2991 additions and 5318 deletions
|
|
@ -12,6 +12,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
|
|||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
|
|
@ -50,10 +51,8 @@ import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
|||
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -66,7 +65,7 @@ lateinit var store: Store<AppState>
|
|||
|
||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||
lateinit var activityResultCaller: ActivityResultCaller
|
||||
lateinit var preferencesStorage: PreferencesStorage
|
||||
lateinit var preferencesStorage: PreferencesDataSource
|
||||
lateinit var walletConnectRepository: WalletConnectRepository
|
||||
lateinit var shopService: TangemShopService
|
||||
lateinit var userTokensRepository: UserTokensRepository
|
||||
|
|
@ -129,10 +128,10 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
lateinit var networkConnectionManager: NetworkConnectionManager
|
||||
|
||||
@Inject
|
||||
lateinit var tokensListFeatureToggles: TokensListFeatureToggles
|
||||
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
|
||||
lateinit var preferencesDataSource: PreferencesDataSource
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
|
@ -142,7 +141,13 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
appReducer(action, state, appStateHolder)
|
||||
},
|
||||
middleware = AppState.getMiddleware(),
|
||||
state = AppState(),
|
||||
state = AppState(
|
||||
daggerGraphState = DaggerGraphState(
|
||||
assetReader = assetReader,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
customTokenFeatureToggles = customTokenFeatureToggles,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
|
@ -154,7 +159,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
DomainLayer.init()
|
||||
preferencesStorage = PreferencesStorage(this)
|
||||
preferencesStorage = preferencesDataSource
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
|
||||
|
|
@ -178,15 +183,6 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
appStateHolder.userTokensRepository = userTokensRepository
|
||||
appStateHolder.walletStoresManager = walletStoresManager
|
||||
|
||||
store.dispatch(
|
||||
action = DaggerGraphAction.SetApplicationDependencies(
|
||||
assetReader = assetReader,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
tokensListFeatureToggles = tokensListFeatureToggles,
|
||||
customTokenFeatureToggles = customTokenFeatureToggles,
|
||||
),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
featureTogglesManager.init()
|
||||
}
|
||||
|
|
@ -248,7 +244,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
private fun initFeedbackManager(
|
||||
context: Context,
|
||||
preferencesStorage: PreferencesStorage,
|
||||
preferencesStorage: PreferencesDataSource,
|
||||
foregroundActivityObserver: ForegroundActivityObserver,
|
||||
store: Store<AppState>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.common.analytics.topup
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
|
||||
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
|
@ -19,7 +21,6 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
|||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.persistence.ToppedUpWalletStorage
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -90,9 +91,9 @@ class TopUpController(
|
|||
|
||||
val isToppedUpInPast = findToppedUpCurrenciesInPast(walletDataModels).isNotEmpty()
|
||||
if (isToppedUpInPast) {
|
||||
val newWalletInfo = ToppedUpWalletStorage.TopupInfo(
|
||||
val newWalletInfo = DataSourceTopupInfo(
|
||||
walletId = userWalletId.stringValue,
|
||||
cardBalanceState = AnalyticsParam.CardBalanceState.Full,
|
||||
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full,
|
||||
)
|
||||
topupWalletStorage.save(newWalletInfo)
|
||||
return@launch
|
||||
|
|
@ -109,9 +110,9 @@ class TopUpController(
|
|||
fun registerEmptyWallet(scanResponse: ScanResponse) {
|
||||
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let {
|
||||
topupWalletStorage.save(
|
||||
ToppedUpWalletStorage.TopupInfo(
|
||||
DataSourceTopupInfo(
|
||||
walletId = it.stringValue,
|
||||
cardBalanceState = AnalyticsParam.CardBalanceState.Empty,
|
||||
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Empty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -129,17 +130,28 @@ class TopUpController(
|
|||
cardTypesResolver: CardTypesResolver,
|
||||
) {
|
||||
val topupInfo = topupWalletStorage.restore(userWalletId.stringValue).guard {
|
||||
val topupInfo = ToppedUpWalletStorage.TopupInfo(
|
||||
val topupInfo = DataSourceTopupInfo(
|
||||
walletId = userWalletId.stringValue,
|
||||
cardBalanceState = cardBalanceState,
|
||||
cardBalanceState = when (cardBalanceState) {
|
||||
AnalyticsParam.CardBalanceState.BlockchainError ->
|
||||
DataSourceTopupInfo.CardBalanceState.BlockchainError
|
||||
AnalyticsParam.CardBalanceState.CustomToken ->
|
||||
DataSourceTopupInfo.CardBalanceState.CustomToken
|
||||
AnalyticsParam.CardBalanceState.Empty ->
|
||||
DataSourceTopupInfo.CardBalanceState.Empty
|
||||
AnalyticsParam.CardBalanceState.Full ->
|
||||
DataSourceTopupInfo.CardBalanceState.Full
|
||||
},
|
||||
)
|
||||
topupWalletStorage.save(topupInfo)
|
||||
return
|
||||
}
|
||||
if (topupInfo.isToppedUp) return
|
||||
|
||||
if (!topupInfo.isToppedUp && cardBalanceState.isToppedUp()) {
|
||||
topupWalletStorage.save(topupInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
|
||||
val isToppedUp = topupInfo.cardBalanceState == DataSourceTopupInfo.CardBalanceState.Full
|
||||
if (isToppedUp) return
|
||||
|
||||
if (cardBalanceState.isToppedUp()) {
|
||||
topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full))
|
||||
TopUpEventConverter().convert(cardTypesResolver)?.let {
|
||||
Analytics.send(it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ package com.tangem.tap.common.chat
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tangem.tap.ForegroundActivityObserver
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
import com.tangem.tap.common.chat.opener.ChatOpener
|
||||
import com.tangem.datasource.config.models.SprinklrConfig
|
||||
import com.tangem.datasource.config.models.ZendeskConfig
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.tap.ForegroundActivityObserver
|
||||
import com.tangem.tap.common.chat.opener.ChatOpener
|
||||
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
|
||||
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import org.rekotlin.Store
|
||||
|
||||
class ChatManager(
|
||||
private val preferencesStorage: PreferencesStorage,
|
||||
private val preferencesStorage: PreferencesDataSource,
|
||||
private val foregroundActivityObserver: ForegroundActivityObserver,
|
||||
private val store: Store<AppState>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
|
|||
Blockchain.TON, Blockchain.TONTestnet -> R.drawable.ic_ton_no_color
|
||||
Blockchain.Kava, Blockchain.KavaTestnet -> R.drawable.ic_kava_no_color
|
||||
Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> R.drawable.ic_ravencoin_no_color
|
||||
Blockchain.Cosmos, Blockchain.CosmosTestnet -> R.drawable.ic_cosmos_no_color
|
||||
else -> R.drawable.ic_tangem_logo
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun <T> List<T>.containsAny(list: List<T>): Boolean {
|
||||
this.forEach { mainItem ->
|
||||
list.forEach { if (it == mainItem) return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
|
||||
val toRemove = this.filter(predicate)
|
||||
this.removeAll(toRemove)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.fragment.app.DialogFragment
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.tangem.feature.referral.ReferralFragment
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -28,7 +29,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
|
|||
import com.tangem.tap.features.send.ui.SendFragment
|
||||
import com.tangem.tap.features.shop.ui.ShopFragment
|
||||
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
|
||||
import com.tangem.tap.features.tokens.legacy.AddTokensFragment
|
||||
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
|
||||
import com.tangem.tap.features.wallet.ui.WalletFragment
|
||||
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
|
||||
|
|
@ -45,24 +45,35 @@ fun FragmentActivity.openFragment(
|
|||
bundle: Bundle? = null,
|
||||
fgShareTransition: FragmentShareTransition? = null,
|
||||
) {
|
||||
val transaction = this.supportFragmentManager.beginTransaction()
|
||||
val fragment = fragmentFactory(screen)
|
||||
fragment.arguments = bundle
|
||||
fgShareTransition?.apply {
|
||||
fragment.sharedElementEnterTransition = enterTransitionSet
|
||||
fragment.sharedElementReturnTransition = exitTransitionSet
|
||||
transaction.setReorderingAllowed(true)
|
||||
shareElements.forEach { shareElement ->
|
||||
shareElement.wView.get()?.let { view ->
|
||||
transaction.addSharedElement(view, shareElement.elementName)
|
||||
val transaction = supportFragmentManager.beginTransaction().apply {
|
||||
setReorderingAllowed(true)
|
||||
}
|
||||
|
||||
val fragment = fragmentFactory(screen).apply {
|
||||
arguments = bundle
|
||||
|
||||
if (fgShareTransition != null) {
|
||||
sharedElementEnterTransition = fgShareTransition.enterTransitionSet
|
||||
sharedElementReturnTransition = fgShareTransition.exitTransitionSet
|
||||
fgShareTransition.shareElements.forEach { shareElement ->
|
||||
shareElement.wView.get()?.let { view ->
|
||||
transaction.addSharedElement(view, shareElement.elementName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (screen.isDialogFragment) {
|
||||
(fragment as DialogFragment).show(transaction, screen.name)
|
||||
if (addToBackstack) {
|
||||
transaction.addToBackStack(screen.name)
|
||||
val dialogFragment = requireNotNull(fragment as? DialogFragment) {
|
||||
"If screen.isDialogFragment == true then fragment must be a DialogFragment"
|
||||
}
|
||||
|
||||
dialogFragment.showAllowingStateLoss(
|
||||
fragmentManager = supportFragmentManager,
|
||||
baseTransaction = transaction,
|
||||
tag = screen.name,
|
||||
addToBackstack = addToBackstack,
|
||||
)
|
||||
} else {
|
||||
transaction.replace(R.id.fragment_container, fragment, screen.name)
|
||||
if (addToBackstack) {
|
||||
|
|
@ -72,6 +83,29 @@ fun FragmentActivity.openFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun DialogFragment.showAllowingStateLoss(
|
||||
fragmentManager: FragmentManager,
|
||||
baseTransaction: FragmentTransaction,
|
||||
tag: String,
|
||||
addToBackstack: Boolean,
|
||||
) {
|
||||
runCatching {
|
||||
if (addToBackstack) baseTransaction.addToBackStack(tag)
|
||||
show(baseTransaction, tag)
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
if (throwable is IllegalStateException) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
transaction.add(this, tag)
|
||||
if (addToBackstack) transaction.addToBackStack(tag)
|
||||
|
||||
transaction.commitAllowingStateLoss()
|
||||
} else {
|
||||
Timber.e(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
|
||||
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
|
||||
try {
|
||||
|
|
@ -113,12 +147,8 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.ResetToFactory -> ResetCardFragment()
|
||||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
AppScreen.AddTokens -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::tokensListFeatureToggles,
|
||||
)
|
||||
if (featureToggles.isRedesignedScreenEnabled) TokensListFragment() else AddTokensFragment()
|
||||
}
|
||||
AppScreen.AddTokens -> TokensListFragment()
|
||||
|
||||
AppScreen.AddCustomToken -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::customTokenFeatureToggles,
|
||||
|
|
@ -129,6 +159,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AddCustomTokenFragment()
|
||||
}
|
||||
}
|
||||
|
||||
AppScreen.WalletDetails -> WalletDetailsFragment()
|
||||
AppScreen.WalletConnectSessions -> WalletConnectFragment()
|
||||
AppScreen.QrScan -> QrScanFragment()
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ data class AppState(
|
|||
SendMiddleware().sendMiddleware,
|
||||
DetailsMiddleware().detailsMiddleware,
|
||||
DisclaimerMiddleware().disclaimerMiddleware,
|
||||
TokensMiddleware().tokensMiddleware,
|
||||
TokensMiddleware.tokensMiddleware,
|
||||
WalletConnectMiddleware().walletConnectMiddleware,
|
||||
BackupMiddleware().backupMiddleware,
|
||||
ShopMiddleware().shopMiddleware,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import com.tangem.common.core.TangemSdkError
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -75,7 +76,9 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
is GlobalAction.RestoreAppCurrency -> {
|
||||
store.dispatch(
|
||||
GlobalAction.RestoreAppCurrency.Success(
|
||||
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency(),
|
||||
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
|
||||
?.run { FiatCurrency(code, name, symbol) }
|
||||
?: FiatCurrency.Default,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package com.tangem.tap.domain.configurable.warningMessage
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.common.extensions.containsAny
|
||||
import com.tangem.tap.common.extensions.removeBy
|
||||
import com.tangem.wallet.R
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class WarningMessagesManager {
|
||||
|
||||
private val warningsList: MutableList<WarningMessage> = mutableListOf()
|
||||
private val warningsList = CopyOnWriteArrayList<WarningMessage>()
|
||||
|
||||
fun addWarning(warning: WarningMessage) {
|
||||
if (findWarning(warning) == null) {
|
||||
|
|
@ -19,36 +19,27 @@ class WarningMessagesManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun getWarnings(
|
||||
location: WarningMessage.Location,
|
||||
forBlockchains: List<Blockchain> = emptyList(),
|
||||
): List<WarningMessage> {
|
||||
return warningsList
|
||||
.filter { !it.isHidden && it.location.contains(location) }
|
||||
.filter {
|
||||
val list = it.blockchainList
|
||||
when {
|
||||
list == null -> true
|
||||
list.containsAny(forBlockchains) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
fun getWarnings(location: WarningMessage.Location, blockchains: List<Blockchain>): List<WarningMessage> {
|
||||
return warningsList.filter { message ->
|
||||
val messageBlockchains = message.blockchainList
|
||||
val isCorrespondingMessageBlockchains = messageBlockchains == null ||
|
||||
messageBlockchains.any(blockchains::contains)
|
||||
val isCorrespondingMessageLocation = message.location.contains(location)
|
||||
|
||||
!message.isHidden && isCorrespondingMessageLocation && isCorrespondingMessageBlockchains
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWarning(warning: WarningMessage): Boolean {
|
||||
val foundWarning = findWarning(warning)
|
||||
return when {
|
||||
foundWarning == null -> false
|
||||
foundWarning.type == WarningMessage.Type.Temporary ||
|
||||
foundWarning.type == WarningMessage.Type.AppRating -> {
|
||||
if (foundWarning.isHidden) {
|
||||
false
|
||||
} else {
|
||||
foundWarning.isHidden = true
|
||||
true
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
val foundWarning = findWarning(warning) ?: return false
|
||||
val isCorrectType = foundWarning.type == WarningMessage.Type.Temporary ||
|
||||
foundWarning.type == WarningMessage.Type.AppRating
|
||||
|
||||
return if (!foundWarning.isHidden && isCorrectType) {
|
||||
foundWarning.isHidden = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,31 +63,31 @@ class WarningMessagesManager {
|
|||
companion object {
|
||||
const val REMAINING_SIGNATURES_WARNING = 10
|
||||
|
||||
fun devCardWarning(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
val devCardWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Permanent,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
null,
|
||||
R.string.common_warning,
|
||||
R.string.alert_developer_card,
|
||||
WarningMessage.Origin.Local,
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.alert_developer_card,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
val alreadySignedHashesWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Temporary,
|
||||
priority = WarningMessage.Priority.Info,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
null,
|
||||
R.string.common_warning,
|
||||
R.string.alert_card_signed_transactions,
|
||||
WarningMessage.Origin.Local,
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.alert_card_signed_transactions,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
|
||||
val signedHashesMultiWalletWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Temporary,
|
||||
|
|
@ -110,69 +101,71 @@ class WarningMessagesManager {
|
|||
titleFormatArg = "\u26A0",
|
||||
)
|
||||
|
||||
fun appRatingWarning(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
WarningMessage.Type.AppRating,
|
||||
WarningMessage.Priority.Info,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
null,
|
||||
R.string.warning_rate_app_title,
|
||||
R.string.warning_rate_app_message,
|
||||
WarningMessage.Origin.Local,
|
||||
val appRatingWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.AppRating,
|
||||
priority = WarningMessage.Priority.Info,
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.warning_rate_app_title,
|
||||
messageResId = R.string.warning_rate_app_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
|
||||
return warning.messageResId == R.string.alert_card_signed_transactions
|
||||
}
|
||||
|
||||
fun onlineVerificationFailed(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
type = WarningMessage.Type.Permanent,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
null,
|
||||
R.string.warning_failed_to_verify_card_title,
|
||||
R.string.warning_failed_to_verify_card_message,
|
||||
WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
|
||||
val onlineVerificationFailed = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Permanent,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.warning_low_signatures_format,
|
||||
titleResId = R.string.warning_failed_to_verify_card_title,
|
||||
messageResId = R.string.warning_failed_to_verify_card_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
messageFormatArg = remainingSignatures.toString(),
|
||||
)
|
||||
|
||||
fun testCardWarning(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
val testCardWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.TestCard,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
|
||||
null,
|
||||
R.string.common_warning,
|
||||
R.string.warning_testnet_card_message,
|
||||
WarningMessage.Origin.Local,
|
||||
location = listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.warning_testnet_card_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun demoCardWarning(): WarningMessage = WarningMessage(
|
||||
"",
|
||||
"",
|
||||
val demoCardWarning = WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Permanent,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
listOf(WarningMessage.Location.MainScreen),
|
||||
null,
|
||||
R.string.common_warning,
|
||||
R.string.alert_demo_message,
|
||||
WarningMessage.Origin.Local,
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.alert_demo_message,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
)
|
||||
|
||||
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage {
|
||||
return WarningMessage(
|
||||
title = "",
|
||||
message = "",
|
||||
type = WarningMessage.Type.Permanent,
|
||||
priority = WarningMessage.Priority.Critical,
|
||||
location = listOf(WarningMessage.Location.MainScreen),
|
||||
blockchains = null,
|
||||
titleResId = R.string.common_warning,
|
||||
messageResId = R.string.warning_low_signatures_format,
|
||||
origin = WarningMessage.Origin.Local,
|
||||
messageFormatArg = remainingSignatures.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
|
||||
return warning.messageResId == R.string.alert_card_signed_transactions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,12 @@ import com.tangem.common.core.TangemSdkError
|
|||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
|
|
@ -233,17 +233,38 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
||||
var isBlockchainsForCurvesExist = false
|
||||
createWalletResponse.forEach { response ->
|
||||
val blockchainsForCurve = getBlockchains(response.cardId, card).filter {
|
||||
it.getSupportedCurves().contains(response.wallet.curve)
|
||||
}
|
||||
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
|
||||
val derivationPaths = blockchainsForCurve.mapNotNull {
|
||||
isBlockchainsForCurvesExist = true
|
||||
it.derivationPath(card.derivationStyle)
|
||||
}
|
||||
if (derivationPaths.isNotEmpty()) {
|
||||
map[response.wallet.publicKey.toMapKey()] = derivationPaths
|
||||
}
|
||||
}
|
||||
val cardEnv = session.environment.card
|
||||
if (cardEnv == null) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.CardError()))
|
||||
return
|
||||
}
|
||||
if (map.isEmpty()) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
if (isBlockchainsForCurvesExist) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
} else {
|
||||
// if there is no blockchains to derive, just return success response with empty derivedKeys
|
||||
callback(
|
||||
CompletionResult.Success(
|
||||
CreateProductWalletTaskResponse(
|
||||
card = cardEnv,
|
||||
primaryCard = primaryCard,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +275,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
callback(
|
||||
CompletionResult.Success(
|
||||
CreateProductWalletTaskResponse(
|
||||
card = session.environment.card!!,
|
||||
card = cardEnv,
|
||||
derivedKeys = result.data.entries,
|
||||
primaryCard = primaryCard,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ import com.tangem.common.tlv.Tlv
|
|||
import com.tangem.common.tlv.TlvDecoder
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
|
|
@ -29,6 +26,9 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
|||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.PreflightReadMode
|
||||
import com.tangem.operations.PreflightReadTask
|
||||
import com.tangem.operations.ScanTask
|
||||
|
|
|
|||
|
|
@ -52,42 +52,36 @@ internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<Walle
|
|||
internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
|
||||
val pendingTransactions = wallet.getPendingTransactions()
|
||||
return this.copy(
|
||||
status = when (val currency = this.currency) {
|
||||
status = when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val amount = wallet.fundsAvailable(AmountType.Coin)
|
||||
if (pendingTransactions.isEmpty()) {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
WalletDataModel.VerifiedOnline(amount)
|
||||
} else {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
WalletDataModel.TransactionInProgress(amount, pendingTransactions)
|
||||
}
|
||||
}
|
||||
|
||||
is Currency.Token -> {
|
||||
val token = currency.token
|
||||
val amount = wallet.fundsAvailable(AmountType.Token(token))
|
||||
val hasTokenPendingTransactions = pendingTransactions
|
||||
.any { it.transactionData.amount.currencySymbol == token.symbol }
|
||||
val hasTokenPendingTransactions = pendingTransactions.any {
|
||||
it.transactionData.amount.currencySymbol == token.symbol
|
||||
}
|
||||
|
||||
when {
|
||||
hasTokenPendingTransactions -> {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
WalletDataModel.TransactionInProgress(amount, pendingTransactions)
|
||||
}
|
||||
|
||||
pendingTransactions.isNotEmpty() -> {
|
||||
WalletDataModel.SameCurrencyTransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
// FIXME: Necessary to avoid passing pending transactions
|
||||
// because SameCurrencyTransactionInProgress didn't use it. It used only to define main button
|
||||
// availability. UI layer turned off pending transaction visibility for this state.
|
||||
WalletDataModel.SameCurrencyTransactionInProgress(amount, pendingTransactions)
|
||||
}
|
||||
else -> {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
WalletDataModel.VerifiedOnline(amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
|
|||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
isActive = value.active,
|
||||
network = value.networks.firstOrNull()?.let { network ->
|
||||
FoundToken.Network(
|
||||
id = network.networkId,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,21 @@ package com.tangem.tap.features.customtoken.impl.domain.models
|
|||
/**
|
||||
* Found token model
|
||||
*
|
||||
* @property id id
|
||||
* @property name name
|
||||
* @property symbol symbol
|
||||
* @property network network
|
||||
* @property id id
|
||||
* @property name name
|
||||
* @property symbol symbol
|
||||
* @property isActive flag that determines status of token
|
||||
* @property network network
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class FoundToken(val id: String, val name: String, val symbol: String, val network: Network) {
|
||||
data class FoundToken(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val isActive: Boolean,
|
||||
val network: Network,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Found token network
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.transition.TransitionInflater
|
||||
|
|
@ -24,6 +25,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
internal class AddCustomTokenFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
|
||||
|
||||
with(TransitionInflater.from(requireContext())) {
|
||||
enterTransition = inflateTransition(R.transition.fade)
|
||||
exitTransition = inflateTransition(R.transition.fade)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -94,106 +92,94 @@ internal sealed interface AddCustomTokenInputField {
|
|||
/** Label */
|
||||
val label: TextReference
|
||||
|
||||
/** Input availability */
|
||||
val isEnabled: Boolean
|
||||
|
||||
/** Flag that determine if current value has error */
|
||||
val isError: Boolean
|
||||
|
||||
/** Placeholder (hint) */
|
||||
val placeholder: TextReference
|
||||
|
||||
/** Flag that determine the processing of current value */
|
||||
val isLoading: Boolean
|
||||
|
||||
/**
|
||||
* Input field model to enter the contract address
|
||||
*
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property isError flag that determine if current value has error
|
||||
* @property isLoading flag that determine the processing of current value
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property keyboardOptions keyboard options
|
||||
* @property label label
|
||||
* @property placeholder placeholder (hint)
|
||||
* @property isLoading flag that determine the processing of current value
|
||||
* @property isError flag that determine if current value has error
|
||||
* @property error error description
|
||||
*/
|
||||
data class ContactAddress(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val isError: Boolean,
|
||||
override val isLoading: Boolean,
|
||||
) : AddCustomTokenInputField {
|
||||
override val isEnabled = true
|
||||
override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
override val label = TextReference.Res(R.string.custom_token_contract_address_input_title)
|
||||
override val placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000")
|
||||
}
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val isLoading: Boolean,
|
||||
val isError: Boolean,
|
||||
val error: TextReference? = null,
|
||||
) : AddCustomTokenInputField
|
||||
|
||||
/**
|
||||
* Input field model to enter the token name
|
||||
*
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property isEnabled input availability
|
||||
* @property isError flag that determine if current value has error
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property keyboardOptions keyboard options
|
||||
* @property label label
|
||||
* @property placeholder placeholder (hint)
|
||||
* @property isEnabled input availability
|
||||
*/
|
||||
data class TokenName(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val isEnabled: Boolean,
|
||||
override val isError: Boolean,
|
||||
) : AddCustomTokenInputField {
|
||||
override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
override val label = TextReference.Res(R.string.custom_token_name_input_title)
|
||||
override val placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder)
|
||||
override val isLoading = false
|
||||
}
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenInputField
|
||||
|
||||
/**
|
||||
* Input field model to enter the token symbol
|
||||
*
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property isEnabled input availability
|
||||
* @property isError flag that determine if current value has error
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property keyboardOptions keyboard options
|
||||
* @property label label
|
||||
* @property placeholder placeholder (hint)
|
||||
* @property isEnabled input availability
|
||||
*/
|
||||
data class TokenSymbol(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val isEnabled: Boolean,
|
||||
override val isError: Boolean,
|
||||
) : AddCustomTokenInputField {
|
||||
override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
|
||||
override val label = TextReference.Res(R.string.custom_token_token_symbol_input_title)
|
||||
override val placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder)
|
||||
override val isLoading = false
|
||||
}
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenInputField
|
||||
|
||||
/**
|
||||
* Input field model to enter the token decimals
|
||||
*
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property isEnabled input availability
|
||||
* @property isError flag that determine if current value has error
|
||||
* @property value current value
|
||||
* @property onValueChange lambda be invoked when value is been changed
|
||||
* @property keyboardOptions keyboard options
|
||||
* @property label label
|
||||
* @property placeholder placeholder (hint)
|
||||
* @property isEnabled input availability
|
||||
*/
|
||||
data class Decimals(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val isEnabled: Boolean,
|
||||
override val isError: Boolean,
|
||||
) : AddCustomTokenInputField {
|
||||
override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next)
|
||||
override val label = TextReference.Res(R.string.custom_token_decimals_input_title)
|
||||
override val placeholder = TextReference.Str(value = "8")
|
||||
override val isLoading = false
|
||||
}
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenInputField
|
||||
}
|
||||
|
||||
/** Base selector field model of add custom token screen */
|
||||
internal sealed interface AddCustomTokenSelectorField {
|
||||
|
||||
/** Selection availability */
|
||||
val isEnabled: Boolean
|
||||
|
||||
/** Label string resource id */
|
||||
/** Label */
|
||||
val label: TextReference
|
||||
|
||||
/** Selected menu item */
|
||||
|
|
@ -208,35 +194,34 @@ internal sealed interface AddCustomTokenSelectorField {
|
|||
/**
|
||||
* Network selector model
|
||||
*
|
||||
* @property label label
|
||||
* @property selectedItem selected menu item
|
||||
* @property items menu items
|
||||
* @property onMenuItemClick lambda be invoked when menu item is been selected
|
||||
*/
|
||||
data class Network(
|
||||
override val label: TextReference,
|
||||
override val selectedItem: SelectorItem.Title,
|
||||
override val items: List<SelectorItem.Title>,
|
||||
override val onMenuItemClick: (Int) -> Unit,
|
||||
) : AddCustomTokenSelectorField {
|
||||
override val isEnabled = true
|
||||
override val label = TextReference.Res(R.string.custom_token_network_input_title)
|
||||
}
|
||||
) : AddCustomTokenSelectorField
|
||||
|
||||
/**
|
||||
* Derivation path selector model
|
||||
*
|
||||
* @property isEnabled selection availability
|
||||
* @property label label
|
||||
* @property selectedItem selected menu item
|
||||
* @property items menu items
|
||||
* @property onMenuItemClick lambda be invoked when menu item is been selected
|
||||
* @property isEnabled selection availability
|
||||
*/
|
||||
data class DerivationPath(
|
||||
override val isEnabled: Boolean,
|
||||
override val label: TextReference,
|
||||
override val selectedItem: SelectorItem.TitleWithSubtitle,
|
||||
override val items: List<SelectorItem.TitleWithSubtitle>,
|
||||
override val onMenuItemClick: (Int) -> Unit,
|
||||
) : AddCustomTokenSelectorField {
|
||||
override val label = TextReference.Res(R.string.custom_token_derivation_path_input_title)
|
||||
}
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenSelectorField
|
||||
|
||||
/** Base menu item model */
|
||||
sealed interface SelectorItem {
|
||||
|
|
@ -259,17 +244,40 @@ internal sealed interface AddCustomTokenSelectorField {
|
|||
* Menu item with title ans subtitle
|
||||
*
|
||||
* @property title title text
|
||||
* @property subtitle subtitle text
|
||||
* @property blockchain blockchain
|
||||
* @property subtitle subtitle text
|
||||
*/
|
||||
data class TitleWithSubtitle(
|
||||
override val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
override val blockchain: Blockchain,
|
||||
val subtitle: TextReference,
|
||||
) : SelectorItem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning model of add custom token screen
|
||||
*
|
||||
* @property description warning description
|
||||
*/
|
||||
internal sealed class AddCustomTokenWarning(val description: TextReference) {
|
||||
|
||||
/** Potential scam warning */
|
||||
object PotentialScamToken : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
|
||||
)
|
||||
|
||||
/** Token already added warning */
|
||||
object TokenAlreadyAdded : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
|
||||
)
|
||||
|
||||
/** Unsupported Solana token warning */
|
||||
object UnsupportedSolanaToken : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating button of add custom token screen
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
/** Custom token type */
|
||||
enum class CustomTokenType { TOKEN, BLOCKCHAIN }
|
||||
|
|
@ -9,4 +9,7 @@ internal interface CustomTokenRouter {
|
|||
|
||||
/** Return to last screen */
|
||||
fun popBackStack()
|
||||
|
||||
/** Open wallet (main) screen */
|
||||
fun openWalletScreen()
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.routers
|
||||
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.store
|
||||
|
||||
|
|
@ -9,4 +10,8 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter {
|
|||
override fun popBackStack() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
override fun openWalletScreen() {
|
||||
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
|
|||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
|
||||
/**
|
||||
* State holder of add custom token screen
|
||||
|
|
@ -24,7 +24,7 @@ internal sealed interface AddCustomTokenStateHolder {
|
|||
val form: AddCustomTokenForm
|
||||
|
||||
/** Warnings */
|
||||
val warnings: List<TextReference>
|
||||
val warnings: Set<AddCustomTokenWarning>
|
||||
|
||||
/** Floating button model */
|
||||
val floatingButton: AddCustomTokenFloatingButton
|
||||
|
|
@ -42,7 +42,7 @@ internal sealed interface AddCustomTokenStateHolder {
|
|||
onBackButtonClick: () -> Unit = this.onBackButtonClick,
|
||||
toolbar: AddCustomTokensToolbar = this.toolbar,
|
||||
form: AddCustomTokenForm = this.form,
|
||||
warnings: List<TextReference> = this.warnings,
|
||||
warnings: Set<AddCustomTokenWarning> = this.warnings,
|
||||
floatingButton: AddCustomTokenFloatingButton = this.floatingButton,
|
||||
): AddCustomTokenStateHolder {
|
||||
return when (this) {
|
||||
|
|
@ -64,7 +64,7 @@ internal sealed interface AddCustomTokenStateHolder {
|
|||
override val onBackButtonClick: () -> Unit,
|
||||
override val toolbar: AddCustomTokensToolbar,
|
||||
override val form: AddCustomTokenForm,
|
||||
override val warnings: List<TextReference>,
|
||||
override val warnings: Set<AddCustomTokenWarning>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
) : AddCustomTokenStateHolder
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ internal sealed interface AddCustomTokenStateHolder {
|
|||
override val onBackButtonClick: () -> Unit,
|
||||
override val toolbar: AddCustomTokensToolbar,
|
||||
override val form: AddCustomTokenForm,
|
||||
override val warnings: List<TextReference>,
|
||||
override val warnings: Set<AddCustomTokenWarning>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
val testBlock: AddCustomTokenTestBlock,
|
||||
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
|
||||
|
|
|
|||
|
|
@ -9,22 +9,21 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
|
||||
|
||||
/**
|
||||
* Add custom token content
|
||||
|
|
@ -37,6 +36,7 @@ import com.tangem.wallet.R
|
|||
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
|
||||
BackHandler(onBack = state.onBackButtonClick)
|
||||
|
||||
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AddCustomTokenToolbar(
|
||||
|
|
@ -44,78 +44,36 @@ internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
|
|||
onBackButtonClick = state.toolbar.onBackButtonClick,
|
||||
)
|
||||
},
|
||||
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
|
||||
floatingActionButton = {
|
||||
val density = LocalDensity.current
|
||||
val verticalPadding = TangemTheme.dimens.spacing32
|
||||
AddCustomTokenFloatingButton(
|
||||
model = state.floatingButton,
|
||||
modifier = Modifier.onSizeChanged {
|
||||
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues = it)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
.padding(bottom = floatingButtonHeight)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
AddCustomTokenForm(model = state.form)
|
||||
|
||||
state.warnings.forEach { description ->
|
||||
key(description) {
|
||||
AddCustomTokenWarning(description)
|
||||
}
|
||||
}
|
||||
AddCustomTokenWarnings(warnings = state.warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenContent() {
|
||||
TangemTheme {
|
||||
AddCustomTokenContent(
|
||||
state = AddCustomTokenStateHolder.Content(
|
||||
onBackButtonClick = {},
|
||||
toolbar = AddCustomTokensToolbar(
|
||||
title = TextReference.Res(R.string.add_custom_token_title),
|
||||
onBackButtonClick = {},
|
||||
),
|
||||
form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
|
||||
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isError = false,
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = AddCustomTokenSelectorField.Network(
|
||||
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
|
||||
title = TextReference.Str("Avalanche"),
|
||||
blockchain = Blockchain.Avalanche,
|
||||
),
|
||||
items = listOf(),
|
||||
onMenuItemClick = {},
|
||||
),
|
||||
tokenNameInputField = AddCustomTokenInputField.TokenName(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
decimalsInputField = AddCustomTokenInputField.Decimals(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
derivationPathSelectorField = null,
|
||||
),
|
||||
warnings = listOf(),
|
||||
floatingButton = AddCustomTokenFloatingButton(
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object AddCustomTokenPreviewData {
|
||||
|
||||
fun createWarnings(): Set<AddCustomTokenWarning> {
|
||||
return setOf(
|
||||
AddCustomTokenWarning.PotentialScamToken,
|
||||
AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
AddCustomTokenWarning.UnsupportedSolanaToken,
|
||||
)
|
||||
}
|
||||
|
||||
fun createDefaultForm(): AddCustomTokenForm {
|
||||
return AddCustomTokenForm(
|
||||
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"),
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
error = null,
|
||||
),
|
||||
networkSelectorField = AddCustomTokenSelectorField.Network(
|
||||
label = TextReference.Res(R.string.custom_token_network_input_title),
|
||||
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
|
||||
title = TextReference.Res(R.string.custom_token_network_input_not_selected),
|
||||
blockchain = Blockchain.Unknown,
|
||||
),
|
||||
items = emptyList(),
|
||||
onMenuItemClick = {},
|
||||
),
|
||||
tokenNameInputField = AddCustomTokenInputField.TokenName(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_name_input_title),
|
||||
placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder),
|
||||
isEnabled = false,
|
||||
),
|
||||
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
|
||||
isEnabled = false,
|
||||
),
|
||||
decimalsInputField = AddCustomTokenInputField.Decimals(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
|
||||
label = TextReference.Res(R.string.custom_token_decimals_input_title),
|
||||
placeholder = TextReference.Str(value = "8"),
|
||||
isEnabled = false,
|
||||
),
|
||||
derivationPathSelectorField = AddCustomTokenSelectorField.DerivationPath(
|
||||
label = TextReference.Res(R.string.custom_token_derivation_path_input_title),
|
||||
selectedItem = AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle(
|
||||
title = TextReference.Res(R.string.custom_token_derivation_path_default),
|
||||
subtitle = TextReference.Res(R.string.custom_token_derivation_path_default),
|
||||
blockchain = Blockchain.Unknown,
|
||||
),
|
||||
items = emptyList(),
|
||||
onMenuItemClick = {},
|
||||
isEnabled = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createTestContent(): AddCustomTokenStateHolder.TestContent {
|
||||
return AddCustomTokenStateHolder.TestContent(
|
||||
onBackButtonClick = {},
|
||||
toolbar = AddCustomTokensToolbar(
|
||||
title = TextReference.Res(R.string.add_custom_token_title),
|
||||
onBackButtonClick = {},
|
||||
),
|
||||
form = createDefaultForm(),
|
||||
warnings = createWarnings(),
|
||||
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
testBlock = AddCustomTokenTestBlock(
|
||||
chooseTokenButtonText = "Choose token",
|
||||
clearButtonText = "Clear address",
|
||||
resetButtonText = "Reset",
|
||||
onClearAddressButtonClick = {},
|
||||
onResetButtonClick = {},
|
||||
),
|
||||
bottomSheet = AddCustomTokenChooseTokenBottomSheet(categoriesBlocks = emptyList(), onTestTokenClick = {}),
|
||||
)
|
||||
}
|
||||
|
||||
fun createContent(): AddCustomTokenStateHolder.Content {
|
||||
return AddCustomTokenStateHolder.Content(
|
||||
onBackButtonClick = {},
|
||||
toolbar = AddCustomTokensToolbar(
|
||||
title = TextReference.Res(R.string.add_custom_token_title),
|
||||
onBackButtonClick = {},
|
||||
),
|
||||
form = createDefaultForm(),
|
||||
warnings = createWarnings(),
|
||||
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
|
||||
/**
|
||||
|
|
@ -10,11 +14,27 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
|
||||
when (stateHolder) {
|
||||
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
|
||||
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenScreen(
|
||||
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
|
||||
) {
|
||||
TangemTheme {
|
||||
AddCustomTokenScreen(stateHolder)
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenScreenProvider : CollectionPreviewParameterProvider<AddCustomTokenStateHolder>(
|
||||
collection = listOf(
|
||||
AddCustomTokenPreviewData.createContent(),
|
||||
AddCustomTokenPreviewData.createTestContent(),
|
||||
),
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -19,33 +20,33 @@ import androidx.compose.material.FabPosition
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -74,6 +75,7 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
|
|||
},
|
||||
)
|
||||
|
||||
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
|
||||
BottomSheetScaffold(
|
||||
sheetContent = {
|
||||
SheetContent(
|
||||
|
|
@ -95,7 +97,16 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
|
|||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
|
||||
floatingActionButton = {
|
||||
val density = LocalDensity.current
|
||||
val verticalPadding = TangemTheme.dimens.spacing32
|
||||
AddCustomTokenFloatingButton(
|
||||
model = state.floatingButton,
|
||||
modifier = Modifier.onSizeChanged {
|
||||
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
sheetBackgroundColor = TangemTheme.colors.background.secondary,
|
||||
sheetPeekHeight = TangemTheme.dimens.size0,
|
||||
|
|
@ -104,15 +115,19 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(it),
|
||||
.padding(paddingValues = it)
|
||||
.padding(bottom = floatingButtonHeight)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
TestBlock(
|
||||
model = state.testBlock,
|
||||
state.testBlock,
|
||||
coroutineScope,
|
||||
bottomSheetScaffoldState,
|
||||
)
|
||||
|
||||
AddCustomTokenForm(model = state.form)
|
||||
|
||||
AddCustomTokenWarnings(warnings = state.warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -243,69 +258,10 @@ private fun TestBlock(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenTestContent() {
|
||||
TangemTheme {
|
||||
AddCustomTokenTestContent(
|
||||
state = AddCustomTokenStateHolder.TestContent(
|
||||
onBackButtonClick = {},
|
||||
toolbar = AddCustomTokensToolbar(
|
||||
title = TextReference.Res(R.string.add_custom_token_title),
|
||||
onBackButtonClick = {},
|
||||
),
|
||||
form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
|
||||
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isError = false,
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = AddCustomTokenSelectorField.Network(
|
||||
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
|
||||
title = TextReference.Str(value = "Avalanche"),
|
||||
blockchain = Blockchain.Avalanche,
|
||||
),
|
||||
items = listOf(),
|
||||
onMenuItemClick = {},
|
||||
),
|
||||
tokenNameInputField = AddCustomTokenInputField.TokenName(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
decimalsInputField = AddCustomTokenInputField.Decimals(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
derivationPathSelectorField = null,
|
||||
),
|
||||
warnings = listOf(),
|
||||
floatingButton = AddCustomTokenFloatingButton(
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
testBlock = AddCustomTokenTestBlock(
|
||||
chooseTokenButtonText = "Choose token",
|
||||
clearButtonText = "Clear address",
|
||||
resetButtonText = "Reset",
|
||||
onClearAddressButtonClick = {},
|
||||
onResetButtonClick = {},
|
||||
),
|
||||
bottomSheet = AddCustomTokenChooseTokenBottomSheet(
|
||||
categoriesBlocks = listOf(),
|
||||
onTestTokenClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.components.PrimaryButtonIconLeft
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
|
|
@ -16,14 +18,15 @@ import com.tangem.wallet.R
|
|||
/**
|
||||
* Add custom token floating button. Attached above the keyboard.
|
||||
*
|
||||
* @param model button model
|
||||
* @param model button model
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
|
||||
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
|
||||
PrimaryButtonIconLeft(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
|
|
@ -36,16 +39,17 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenFloatingButton_Enabled() {
|
||||
private fun Preview_AddCustomTokenFloatingButton(
|
||||
@PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton,
|
||||
) {
|
||||
TangemTheme {
|
||||
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = true, onClick = {}))
|
||||
AddCustomTokenFloatingButton(model)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenFloatingButton_Disabled() {
|
||||
TangemTheme {
|
||||
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}))
|
||||
}
|
||||
}
|
||||
private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider<AddCustomTokenFloatingButton>(
|
||||
listOf(
|
||||
AddCustomTokenFloatingButton(isEnabled = true, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -14,6 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.ExposedDropdownMenuBox
|
||||
import androidx.compose.material.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -26,13 +31,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
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.tap.common.compose.TangemTextFieldsDefault
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
|
||||
/**
|
||||
|
|
@ -68,7 +74,37 @@ internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
|
|||
|
||||
@Composable
|
||||
private fun InputField(model: AddCustomTokenInputField) {
|
||||
Column {
|
||||
val isError = (model as? AddCustomTokenInputField.ContactAddress)?.isError ?: false
|
||||
|
||||
TextField(model, isError)
|
||||
|
||||
(model as? AddCustomTokenInputField.ContactAddress)?.error?.resolveReference()?.let {
|
||||
AnimatedVisibility(
|
||||
visible = isError,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colors.error,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
|
||||
Box {
|
||||
val isEnabled = when (model) {
|
||||
is AddCustomTokenInputField.ContactAddress -> true
|
||||
is AddCustomTokenInputField.Decimals -> model.isEnabled
|
||||
is AddCustomTokenInputField.TokenName -> model.isEnabled
|
||||
is AddCustomTokenInputField.TokenSymbol -> model.isEnabled
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = model.value,
|
||||
|
|
@ -79,8 +115,8 @@ private fun InputField(model: AddCustomTokenInputField) {
|
|||
text = model.label.resolveReference(),
|
||||
style = TangemTheme.typography.caption,
|
||||
color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
|
||||
enabled = model.isEnabled,
|
||||
error = model.isError,
|
||||
enabled = isEnabled,
|
||||
error = isError,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
).value,
|
||||
)
|
||||
|
|
@ -90,20 +126,20 @@ private fun InputField(model: AddCustomTokenInputField) {
|
|||
text = model.placeholder.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTextFieldsDefault.defaultTextFieldColors
|
||||
.placeholderColor(enabled = model.isEnabled)
|
||||
.placeholderColor(enabled = isEnabled)
|
||||
.value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
enabled = model.isEnabled,
|
||||
isError = model.isError,
|
||||
enabled = isEnabled,
|
||||
isError = isError,
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = model.isLoading,
|
||||
visible = (model as? AddCustomTokenInputField.ContactAddress)?.isLoading ?: false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
|
|
@ -124,6 +160,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
|
|||
expanded = isExpanded,
|
||||
onExpandedChange = { isExpanded = !isExpanded },
|
||||
) {
|
||||
val isEnabled = (model as? AddCustomTokenSelectorField.DerivationPath)?.isEnabled ?: true
|
||||
OutlinedTextField(
|
||||
value = when (val item = model.selectedItem) {
|
||||
is AddCustomTokenSelectorField.SelectorItem.Title -> item.title
|
||||
|
|
@ -132,14 +169,14 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
enabled = model.isEnabled,
|
||||
enabled = isEnabled,
|
||||
label = { Text(text = model.label.resolveReference()) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = isExpanded && model.isEnabled,
|
||||
expanded = isExpanded && isEnabled,
|
||||
onDismissRequest = { isExpanded = false },
|
||||
) {
|
||||
FocusRequester
|
||||
|
|
@ -173,44 +210,18 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenForm() {
|
||||
private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) {
|
||||
TangemTheme {
|
||||
AddCustomTokenForm(
|
||||
AddCustomTokenForm(
|
||||
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isError = false,
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = AddCustomTokenSelectorField.Network(
|
||||
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
|
||||
title = TextReference.Str(value = "Avalanche"),
|
||||
blockchain = Blockchain.Avalanche,
|
||||
),
|
||||
items = listOf(),
|
||||
onMenuItemClick = {},
|
||||
),
|
||||
tokenNameInputField = AddCustomTokenInputField.TokenName(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
decimalsInputField = AddCustomTokenInputField.Decimals(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
isEnabled = false,
|
||||
isError = false,
|
||||
),
|
||||
derivationPathSelectorField = null,
|
||||
),
|
||||
)
|
||||
AddCustomTokenForm(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenFormProvider : CollectionPreviewParameterProvider<AddCustomTokenForm>(
|
||||
collection = listOf(
|
||||
AddCustomTokenPreviewData.createDefaultForm(),
|
||||
AddCustomTokenPreviewData.createDefaultForm().copy(derivationPathSelectorField = null),
|
||||
AddCustomTokenPreviewData.createDefaultForm().let { form ->
|
||||
form.copy(contractAddressInputField = form.contractAddressInputField.copy(isLoading = true))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -2,39 +2,56 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
* Add custom token warning component
|
||||
* FIXME("Incorrect typography. Replace with typography from design system")
|
||||
* Add custom token warnings
|
||||
*
|
||||
* @param description warning description
|
||||
* @param modifier modifier
|
||||
* @param warnings warnings descriptions set
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifier = Modifier) {
|
||||
internal fun AddCustomTokenWarnings(warnings: Set<AddCustomTokenWarning>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
warnings.forEach { warning ->
|
||||
key(warning) { AddCustomTokenWarning(warning) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
backgroundColor = TangemColorPalette.Tangerine,
|
||||
contentColor = TangemColorPalette.White,
|
||||
elevation = TangemTheme.dimens.elevation4,
|
||||
) {
|
||||
// FIXME("Incorrect typography. Replace with typography from design system")
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
|
|
@ -45,10 +62,18 @@ internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifie
|
|||
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
text = description.resolveReference(),
|
||||
text = warning.description.resolveReference(),
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenWarnings() {
|
||||
TangemTheme {
|
||||
AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
||||
/** Analytics sender for tokens list screen */
|
||||
class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
|
||||
|
||||
fun sendWhenScreenOpened() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened)
|
||||
}
|
||||
|
||||
fun sendWhenAddTokenButtonClicked(currency: Currency, address: String) {
|
||||
analyticsEventHandler.send(
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
ManageTokens.CustomToken.TokenWasAdded.Blockchain(
|
||||
derivationPath = currency.derivationPath,
|
||||
blockchain = currency.blockchain,
|
||||
)
|
||||
}
|
||||
|
||||
is Currency.Token -> {
|
||||
ManageTokens.CustomToken.TokenWasAdded.Token(
|
||||
symbol = currency.currencySymbol,
|
||||
derivationPath = currency.derivationPath,
|
||||
blockchain = currency.blockchain,
|
||||
contractAddress = address,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,11 +15,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.compose.ClosePopupTrigger
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
|||
import com.tangem.domain.features.addCustomToken.redux.ViewStates
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.AddCustomTokenWarning
|
||||
import com.tangem.tap.common.compose.ClosePopupTrigger
|
||||
import com.tangem.tap.common.compose.ComposeDialogManager
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
|
||||
|
|
@ -174,12 +172,14 @@ fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
|||
Column {
|
||||
warnings.forEachIndexed { index, item ->
|
||||
val modifier = when (index) {
|
||||
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)
|
||||
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
|
||||
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
|
||||
0 -> Modifier.padding(vertical = 0.dp)
|
||||
warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp)
|
||||
else -> Modifier.padding(top = 8.dp, bottom = 0.dp)
|
||||
}
|
||||
AddCustomTokenWarning(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
warning = item,
|
||||
converter = warningConverter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.common.compose
|
||||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.common.compose
|
||||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -31,17 +31,18 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.SelectTokenNetworkDialog
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@Composable
|
||||
fun ComposeDialogManager() {
|
||||
internal fun ComposeDialogManager() {
|
||||
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
|
||||
val subscriber = remember {
|
||||
object : StoreSubscriber<DomainGlobalState> {
|
||||
|
|
@ -94,27 +95,22 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
|||
* Dialog with single item selection
|
||||
*/
|
||||
@Composable
|
||||
fun <T> SimpleDialog(
|
||||
fun SimpleDialog(
|
||||
title: String,
|
||||
items: List<T>,
|
||||
onSelect: (T) -> Unit,
|
||||
items: List<CoinsResponse.Coin.Network>,
|
||||
onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
itemContent: @Composable (T) -> Unit,
|
||||
itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
properties = DialogProperties(false, false),
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
onDismissRequest = { },
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(22.dp),
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) {
|
||||
DialogTitle(title = title)
|
||||
LazyColumn {
|
||||
items(items) { item ->
|
||||
items(items = items, key = CoinsResponse.Coin.Network::networkId) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -17,8 +17,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTo
|
|||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.BlockchainSpinner
|
||||
import com.tangem.tap.common.compose.ClosePopupTrigger
|
||||
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.tangem.tap.common.compose
|
||||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -12,6 +13,7 @@ import androidx.core.os.postDelayed
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.compose.TangemTextFieldsDefault
|
||||
import com.tangem.tap.common.extensions.ValueCallback
|
||||
|
||||
/**
|
||||
|
|
@ -20,7 +22,7 @@ import com.tangem.tap.common.extensions.ValueCallback
|
|||
@Suppress("MagicNumber")
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun <T> OutlinedSpinner(
|
||||
internal fun <T> OutlinedSpinner(
|
||||
label: String,
|
||||
itemList: List<T>,
|
||||
selectedItem: Field.Data<T>,
|
||||
|
|
@ -71,15 +73,11 @@ fun <T> OutlinedSpinner(
|
|||
)
|
||||
|
||||
if (isEnabled) {
|
||||
ExposedDropdownMenu(
|
||||
expanded = rIsExpanded.value,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) {
|
||||
itemList.forEach { item ->
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
when (dropdownItemView) {
|
||||
null -> Text(textFieldConverter(item))
|
||||
else -> dropdownItemView(item)
|
||||
key(item) {
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +89,6 @@ fun <T> OutlinedSpinner(
|
|||
class ClosePopupTrigger {
|
||||
var close: () -> Unit = {}
|
||||
var onCloseComplete: () -> Unit = {}
|
||||
var isTriggered = false
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.legacy.compose
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.tap.common.compose.SimpleDialog
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.features.disclaimer
|
||||
|
||||
import com.tangem.tap.persistence.DisclaimerPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.tap.features.disclaimer
|
||||
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.tap.persistence.DisclaimerPrefStorage
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
onSearchTokensClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonTokensList())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
store.dispatch(TokensAction.AllowToAddTokens(false))
|
||||
store.dispatch(TokensAction.LoadCurrencies())
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ 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.tap.persistence.UsedCardsPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.api.featuretoggles
|
||||
|
||||
/**
|
||||
* TokensList feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TokensListFeatureToggles {
|
||||
|
||||
/** Availability of redesigned screen (internal feature) */
|
||||
val isRedesignedScreenEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
|
||||
import com.tangem.tap.features.tokens.impl.featuretoggles.DefaultTokensListFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokensListFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTokensListFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokensListFeatureToggles {
|
||||
return DefaultTokensListFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||
import dagger.Module
|
||||
|
|
@ -17,5 +18,7 @@ internal object TokensListRouterModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
|
||||
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter {
|
||||
return DefaultTokensListRouter(customTokenFeatureToggles)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
|
||||
|
||||
/**
|
||||
* Default implementation of TokensList feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TokensListFeatureToggles {
|
||||
|
||||
override val isRedesignedScreenEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_TOKEN_LIST_SCREEN_ENABLED")
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
|
@ -35,7 +36,9 @@ internal class TokensListFragment : Fragment() {
|
|||
setContent {
|
||||
isTransitionGroup = true
|
||||
|
||||
val viewModel = hiltViewModel<TokensListViewModel>()
|
||||
val viewModel = hiltViewModel<TokensListViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
TangemTheme {
|
||||
TokensListScreen(stateHolder = viewModel.uiState)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.tap.store
|
|||
*/
|
||||
class TokensListArgs {
|
||||
/** Tokens list screen mode */
|
||||
val isManageAccess: Boolean get() = store.state.tokensState.allowToAdd
|
||||
val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess
|
||||
|
||||
/** Tokens list that accessible from the main screen */
|
||||
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package com.tangem.tap.features.tokens.impl.presentation.router
|
|||
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.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -15,15 +17,20 @@ import com.tangem.wallet.R
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListRouter : TokensListRouter {
|
||||
internal class DefaultTokensListRouter(
|
||||
private val customTokenFeatureToggles: CustomTokenFeatureToggles,
|
||||
) : TokensListRouter {
|
||||
|
||||
override fun popBackStack() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
}
|
||||
|
||||
override fun openAddCustomTokenScreen() {
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
if (customTokenFeatureToggles.isRedesignedScreenEnabled) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
} else {
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showAddressCopiedNotification() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import androidx.compose.runtime.MutableState
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
|
||||
/**
|
||||
* Network item state
|
||||
|
|
@ -62,7 +61,7 @@ sealed interface NetworkItemState {
|
|||
override val isMainNetwork: Boolean,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
val id: String,
|
||||
val address: ContractAddress?,
|
||||
val address: String?,
|
||||
val decimalCount: Int?,
|
||||
val blockchain: Blockchain,
|
||||
val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ private fun Icon(name: String, iconUrl: String, modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO(use CurrencyPlaceholderIcon here?)
|
||||
@Composable
|
||||
private fun PlaceholderIcon(name: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import com.tangem.tap.common.analytics.events.ManageTokens
|
|||
/** Analytics sender for tokens list screen */
|
||||
class TokensListAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
|
||||
|
||||
fun sendWhenScreenOpened() {
|
||||
analyticsEventHandler.send(event = ManageTokens.ScreenOpened())
|
||||
}
|
||||
|
||||
fun sendWhenTokenAdded(token: Token) {
|
||||
analyticsEventHandler.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.LoadState
|
||||
|
|
@ -59,7 +61,7 @@ internal class TokensListViewModel @Inject constructor(
|
|||
private val dispatchers: AppCoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel() {
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
private val args = TokensListArgs()
|
||||
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
|
||||
|
|
@ -72,6 +74,10 @@ internal class TokensListViewModel @Inject constructor(
|
|||
private val changedTokensList: MutableList<TokenWithBlockchain> = args.mainScreenTokenList.toMutableList()
|
||||
private val changedBlockchainList: MutableList<Blockchain> = args.mainScreenBlockchainList.toMutableList()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
if (args.isManageAccess) analyticsSender.sendWhenScreenOpened()
|
||||
}
|
||||
|
||||
private fun getInitialUiState(): TokensListStateHolder {
|
||||
return if (args.isManageAccess) {
|
||||
TokensListStateHolder.ManageContent(
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.extensions.copyToClipboard
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.BaseFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensState
|
||||
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrenciesScreen
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentAddTokensBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
|
||||
|
||||
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
|
||||
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
|
||||
private var searchInput = mutableStateOf("")
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
addBackPressHandler(this)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
|
||||
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
|
||||
|
||||
val onSaveChanges = { tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain> ->
|
||||
Analytics.send(ManageTokens.ButtonSaveChanges())
|
||||
store.dispatch(TokensAction.SaveChanges(tokens, blockchains))
|
||||
}
|
||||
val onNetworkItemClicked = { contractAddress: ContractAddress ->
|
||||
context?.copyToClipboard(contractAddress)
|
||||
store.dispatchNotification(R.string.contract_address_copied_message)
|
||||
}
|
||||
|
||||
val onLoadMore = {
|
||||
store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse))
|
||||
}
|
||||
|
||||
cvCurrencies.setContent {
|
||||
TangemTheme {
|
||||
CurrenciesScreen(
|
||||
tokensState = tokensState,
|
||||
onSaveChanges = onSaveChanges,
|
||||
onNetworkItemClick = onNetworkItemClicked,
|
||||
onLoadMore = onLoadMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state
|
||||
.skipRepeats { oldState, newState -> oldState.tokensState == newState.tokensState }
|
||||
.select { it.tokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_popular_tokens, menu)
|
||||
|
||||
val addCustomTokenAllowed = store.state.tokensState.allowToAdd
|
||||
menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed
|
||||
|
||||
val menuItem = menu.findItem(R.id.menu_search)
|
||||
val searchView: SearchView = menuItem.actionView as SearchView
|
||||
searchView.queryHint = searchView.getString(R.string.common_search)
|
||||
searchView.maxWidth = android.R.attr.width
|
||||
searchView.inputtedTextAsFlow()
|
||||
.debounce(800)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
searchInput.value = it.lowercase()
|
||||
dispatchOnMain(TokensAction.SetSearchInput(searchInput.value))
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
return super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_search -> true
|
||||
R.id.menu_navigate_add_custom_token -> {
|
||||
Analytics.send(ManageTokens.ButtonCustomToken())
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureTransitions() {
|
||||
super.configureTransitions()
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.slide_right)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
}
|
||||
|
||||
override fun newState(state: TokensState) {
|
||||
if (activity == null || view == null) return
|
||||
val toolbarTitle =
|
||||
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
|
||||
tokensState.value = state
|
||||
binding.toolbar.title = getString(toolbarTitle)
|
||||
}
|
||||
|
||||
private fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
|
||||
val watcher = setOnQueryTextListener(
|
||||
object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
trySend(newText ?: "")
|
||||
return false
|
||||
}
|
||||
},
|
||||
)
|
||||
awaitClose { watcher }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,37 +2,22 @@ package com.tangem.tap.features.tokens.legacy.redux
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class TokensAction : Action {
|
||||
sealed interface TokensAction : Action {
|
||||
|
||||
object ResetState : TokensAction()
|
||||
/** Single way to pass data to the screen */
|
||||
sealed interface SetArgs : TokensAction {
|
||||
|
||||
data class AllowToAddTokens(val allow: Boolean) : TokensAction()
|
||||
data class ManageAccess(val wallets: List<WalletDataModel>, val derivationStyle: DerivationStyle?) : SetArgs
|
||||
|
||||
data class LoadCurrencies(val scanResponse: ScanResponse? = null) : TokensAction() {
|
||||
data class Success(val currencies: List<Currency>, val loadMore: Boolean) : TokensAction()
|
||||
object Failure : TokensAction()
|
||||
object ReadAccess : SetArgs
|
||||
}
|
||||
|
||||
data class LoadMore(
|
||||
val scanResponse: ScanResponse? = null,
|
||||
) : TokensAction()
|
||||
// TODO: [REDACTED_TASK_KEY] Remove this action
|
||||
data class SaveChanges(val tokens: List<TokenWithBlockchain>, val blockchains: List<Blockchain>) : TokensAction
|
||||
|
||||
data class SetAddedCurrencies(
|
||||
val wallets: List<WalletDataModel>,
|
||||
val derivationStyle: DerivationStyle?,
|
||||
) : TokensAction()
|
||||
|
||||
data class SaveChanges(
|
||||
val addedTokens: List<TokenWithBlockchain>,
|
||||
val addedBlockchains: List<Blockchain>,
|
||||
) : TokensAction()
|
||||
|
||||
object PrepareAndNavigateToAddCustomToken : TokensAction()
|
||||
|
||||
data class SetSearchInput(val searchInput: String) : TokensAction()
|
||||
// TODO: Remove this action in 4.7 release
|
||||
object PrepareAndNavigateToAddCustomToken : TokensAction
|
||||
}
|
||||
|
|
@ -8,13 +8,10 @@ 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.common.services.Result
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.util.hasDerivation
|
||||
import com.tangem.domain.common.util.supportsHdWallet
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
|
|
@ -31,132 +28,77 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
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.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.walletCurrenciesManager
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TokensMiddleware {
|
||||
object TokensMiddleware {
|
||||
|
||||
val tokensMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
|
||||
is TokensAction.SaveChanges -> handleSaveChanges(action)
|
||||
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
|
||||
is TokensAction.SetSearchInput -> {
|
||||
Analytics.send(ManageTokens.TokenSearched())
|
||||
handleLoadCurrencies(
|
||||
scanResponse = store.state.globalState.scanResponse,
|
||||
newSearchInput = action.searchInput,
|
||||
)
|
||||
}
|
||||
is TokensAction.LoadMore -> {
|
||||
if (store.state.tokensState.needToLoadMore &&
|
||||
store.state.tokensState.currencies.isNotEmpty()
|
||||
) {
|
||||
handleLoadCurrencies(action.scanResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLoadCurrencies(scanResponse: ScanResponse?, newSearchInput: String? = null) {
|
||||
val tokensState = store.state.tokensState
|
||||
|
||||
val supportedBlockchains: List<Blockchain> = scanResponse?.card?.supportedBlockchains()
|
||||
?: Blockchain.values().toList().filterNot(Blockchain::isTestnet)
|
||||
|
||||
val loadCoinsService = LoadAvailableCoinsService(
|
||||
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader),
|
||||
)
|
||||
|
||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
||||
scope.launch {
|
||||
val result = loadCoinsService.getSupportedTokens(
|
||||
isTestNet = scanResponse?.card?.isTestCard ?: false,
|
||||
supportedBlockchains = supportedBlockchains,
|
||||
page = if (newSearchInput == null) tokensState.pageToLoad else 0,
|
||||
searchInput = if (newSearchInput == null) tokensState.searchInput else newSearchInput.ifBlank { null },
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return@launch
|
||||
|
||||
val currentTokens = store.state.tokensState.addedTokens
|
||||
val currentBlockchains = store.state.tokensState.addedBlockchains
|
||||
|
||||
val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains)
|
||||
val blockchainsToRemove =
|
||||
store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains)
|
||||
|
||||
val tokensToAdd = action.tokens.filterNot(currentTokens::contains)
|
||||
val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } }
|
||||
|
||||
removeCurrenciesIfNeeded(
|
||||
currencies = convertToCurrencies(
|
||||
blockchains = blockchainsToRemove,
|
||||
tokens = tokensToRemove,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatchOnMain(
|
||||
action = when (result) {
|
||||
is Result.Success -> {
|
||||
TokensAction.LoadCurrencies.Success(
|
||||
currencies = result.data.currencies.filter(supportedBlockchains.toSet()),
|
||||
loadMore = result.data.moreAvailable,
|
||||
)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
TokensAction.LoadCurrencies.Failure
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) = scope.launch {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return@launch
|
||||
|
||||
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
)
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
)
|
||||
|
||||
val blockchainsToAdd = action.addedBlockchains.filterNot(currentBlockchains::contains)
|
||||
val blockchainsToRemove = currentBlockchains.filterNot(action.addedBlockchains::contains)
|
||||
|
||||
val tokensToAdd = action.addedTokens.filterNot(currentTokens::contains)
|
||||
val tokensToRemove = currentTokens.filterNot { token -> action.addedTokens.any { it.token == token.token } }
|
||||
|
||||
removeCurrenciesIfNeeded(
|
||||
currencies = convertToCurrencies(
|
||||
blockchains = blockchainsToRemove,
|
||||
tokens = tokensToRemove,
|
||||
val currencyList = convertToCurrencies(
|
||||
blockchains = blockchainsToAdd,
|
||||
tokens = tokensToAdd,
|
||||
derivationStyle = scanResponse.card.derivationStyle,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
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.card.derivationStyle,
|
||||
)
|
||||
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAdd(it, currencyList)
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAdd(it, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
} else {
|
||||
submitAdd(scanResponse, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
} else {
|
||||
submitAdd(scanResponse, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,21 +258,23 @@ class TokensMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
val addedCurrencies = store.state.walletState.walletsStores.map { walletStore ->
|
||||
walletStore.walletsData.map { walletData -> walletData.currency }
|
||||
}.flatten().map {
|
||||
when (it) {
|
||||
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
|
||||
it.blockchain,
|
||||
it.derivationPath,
|
||||
)
|
||||
is Currency.Token -> DomainWrapped.Currency.Token(
|
||||
it.token,
|
||||
it.blockchain,
|
||||
it.derivationPath,
|
||||
)
|
||||
val addedCurrencies = store.state.walletState.walletsStores
|
||||
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
|
||||
.flatten()
|
||||
.map { currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
|
||||
currency.blockchain,
|
||||
currency.derivationPath,
|
||||
)
|
||||
|
||||
is Currency.Token -> DomainWrapped.Currency.Token(
|
||||
currency.token,
|
||||
currency.blockchain,
|
||||
currency.derivationPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package com.tangem.tap.features.tokens.legacy.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.Currency.Token
|
||||
import org.rekotlin.Action
|
||||
|
||||
object TokensReducer {
|
||||
|
|
@ -10,47 +15,41 @@ object TokensReducer {
|
|||
private fun internalReduce(action: Action, state: AppState): TokensState {
|
||||
if (action !is TokensAction) return state.tokensState
|
||||
|
||||
val tokensState = state.tokensState
|
||||
return when (action) {
|
||||
is TokensAction.ResetState -> TokensState()
|
||||
is TokensAction.LoadCurrencies -> {
|
||||
val loadingState = if (tokensState.currencies.isEmpty()) {
|
||||
LoadCoinsState.LOADING
|
||||
} else {
|
||||
LoadCoinsState.LOADED
|
||||
}
|
||||
tokensState.copy(
|
||||
scanResponse = action.scanResponse,
|
||||
loadCoinsState = loadingState,
|
||||
)
|
||||
}
|
||||
is TokensAction.LoadCurrencies.Success -> {
|
||||
tokensState.copy(
|
||||
currencies = tokensState.currencies + action.currencies,
|
||||
needToLoadMore = action.loadMore,
|
||||
pageToLoad = tokensState.pageToLoad + 1,
|
||||
loadCoinsState = LoadCoinsState.LOADED,
|
||||
)
|
||||
}
|
||||
is TokensAction.SetAddedCurrencies -> {
|
||||
tokensState.copy(
|
||||
is TokensAction.SetArgs.ManageAccess -> {
|
||||
state.tokensState.copy(
|
||||
isManageAccess = true,
|
||||
addedWallets = action.wallets,
|
||||
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
|
||||
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
|
||||
addedWallets = action.wallets,
|
||||
)
|
||||
}
|
||||
is TokensAction.AllowToAddTokens -> {
|
||||
tokensState.copy(allowToAdd = action.allow)
|
||||
|
||||
is TokensAction.SetArgs.ReadAccess -> {
|
||||
state.tokensState.copy(isManageAccess = false)
|
||||
}
|
||||
is TokensAction.SetSearchInput -> {
|
||||
tokensState.copy(
|
||||
searchInput = action.searchInput.ifBlank { null },
|
||||
needToLoadMore = true,
|
||||
currencies = emptyList(),
|
||||
pageToLoad = 0,
|
||||
loadCoinsState = LoadCoinsState.LOADING,
|
||||
)
|
||||
}
|
||||
else -> tokensState
|
||||
|
||||
else -> state.tokensState
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
||||
return 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 mapNotNull { walletDataModel ->
|
||||
if (walletDataModel.currency !is Token) return@mapNotNull null
|
||||
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
||||
|
||||
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
|
||||
}.distinct()
|
||||
}
|
||||
|
|
@ -1,76 +1,16 @@
|
|||
package com.tangem.tap.features.tokens.legacy.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import org.rekotlin.StateType
|
||||
import com.tangem.tap.features.wallet.models.Currency.Token as CurrencyToken
|
||||
|
||||
data class TokensState(
|
||||
val isManageAccess: Boolean = false,
|
||||
val addedWallets: List<WalletDataModel> = emptyList(),
|
||||
val addedTokens: List<TokenWithBlockchain> = emptyList(),
|
||||
val addedBlockchains: List<Blockchain> = emptyList(),
|
||||
val currencies: List<Currency> = emptyList(),
|
||||
val searchInput: String? = null,
|
||||
val allowToAdd: Boolean = true,
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val needToLoadMore: Boolean = true,
|
||||
val pageToLoad: Int = 0,
|
||||
val loadCoinsState: LoadCoinsState = LoadCoinsState.LOADING,
|
||||
) : StateType {
|
||||
) : StateType
|
||||
|
||||
fun canHandleToken(token: TokenWithBlockchain): Boolean {
|
||||
return scanResponse?.card?.canHandleToken(token.blockchain) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
typealias ContractAddress = String
|
||||
|
||||
fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
|
||||
derivationStyle: DerivationStyle?,
|
||||
): List<TokenWithBlockchain> {
|
||||
return mapNotNull {
|
||||
if (it.currency !is CurrencyToken) return@mapNotNull null
|
||||
if (it.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
||||
TokenWithBlockchain(it.currency.token, it.currency.blockchain)
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
||||
return mapNotNull {
|
||||
if (it.currency.isCustomCurrency(derivationStyle)) {
|
||||
null
|
||||
} else {
|
||||
(it.currency as? com.tangem.tap.features.wallet.models.Currency.Blockchain)?.blockchain
|
||||
}
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
data class TokenWithBlockchain(
|
||||
val token: Token,
|
||||
val blockchain: Blockchain,
|
||||
)
|
||||
|
||||
fun List<Currency>.filter(supportedBlockchains: Set<Blockchain>?): List<Currency> {
|
||||
if (supportedBlockchains == null) return this
|
||||
|
||||
return map { currency ->
|
||||
currency.copy(
|
||||
contracts = currency.contracts.filter { contract ->
|
||||
supportedBlockchains.contains(contract.blockchain) &&
|
||||
(contract.blockchain.canHandleTokens() || contract.address == null)
|
||||
},
|
||||
)
|
||||
}.filterNot { currency ->
|
||||
currency.contracts.isEmpty() && !supportedBlockchains.contains(Blockchain.fromNetworkId(currency.id))
|
||||
}
|
||||
}
|
||||
|
||||
enum class LoadCoinsState {
|
||||
LOADING, LOADED,
|
||||
}
|
||||
// TODO: [REDACTED_TASK_KEY] Remove this class
|
||||
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.tap.features.tokens.legacy.ui.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.compose.extensions.addAndNotify
|
||||
import com.tangem.tap.common.compose.extensions.removeAndNotify
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.LoadCoinsState
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensState
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun CurrenciesScreen(
|
||||
tokensState: MutableState<TokensState>,
|
||||
onSaveChanges: (List<TokenWithBlockchain>, List<Blockchain>) -> Unit,
|
||||
onNetworkItemClick: (ContractAddress) -> Unit,
|
||||
onLoadMore: () -> Unit,
|
||||
) {
|
||||
val tokensAddedOnMainScreen = remember { tokensState.value.addedTokens }
|
||||
val blockchainsAddedOnMainScreen = remember { tokensState.value.addedBlockchains }
|
||||
|
||||
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
|
||||
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
|
||||
|
||||
val keyboardState by keyboardAsState()
|
||||
|
||||
val onAddCurrencyToggleClick = { currency: Currency, token: TokenWithBlockchain? ->
|
||||
val blockchain = Blockchain.fromNetworkId(currency.id)
|
||||
if (blockchain != null && token == null) {
|
||||
toggleBlockchain(
|
||||
blockchain,
|
||||
blockchainsAddedOnMainScreen,
|
||||
addedBlockchainsState,
|
||||
addedTokensState.value,
|
||||
)
|
||||
} else if (token != null) {
|
||||
toggleToken(
|
||||
token,
|
||||
tokensAddedOnMainScreen,
|
||||
addedTokensState,
|
||||
tokensState.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val statusBarColor = colorResource(id = R.color.backgroundLightGray)
|
||||
SystemBarsEffect {
|
||||
setSystemBarsColor(color = statusBarColor)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
if (tokensState.value.allowToAdd) {
|
||||
SaveChangesButton(keyboardState) {
|
||||
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = Color(0xFF1ACE80))
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADED,
|
||||
enter = fadeIn(animationSpec = tween(1000)),
|
||||
exit = fadeOut(animationSpec = tween(1000)),
|
||||
) {
|
||||
Column {
|
||||
val showHeader = tokensState.value.scanResponse?.card?.useOldStyleDerivation == true
|
||||
ListOfCurrencies(
|
||||
header = { if (showHeader) CurrenciesWarning() },
|
||||
currencies = tokensState.value.currencies,
|
||||
addedTokens = addedTokensState.value,
|
||||
addedBlockchains = addedBlockchainsState.value,
|
||||
allowToAdd = tokensState.value.allowToAdd,
|
||||
onAddCurrencyToggle = { currency, token ->
|
||||
onAddCurrencyToggleClick(currency, token)
|
||||
},
|
||||
onNetworkItemClick = onNetworkItemClick,
|
||||
onLoadMore = onLoadMore,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBlockchain(
|
||||
blockchain: Blockchain,
|
||||
blockchainsAddedOnMainScreen: List<Blockchain>,
|
||||
addedBlockchainsState: MutableState<List<Blockchain>>,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
) {
|
||||
val isTryingToRemove = addedBlockchainsState.value.contains(blockchain)
|
||||
val isAddedOnMainScreen = blockchainsAddedOnMainScreen.contains(blockchain)
|
||||
val isTokenWithSameBlockchainFound = addedTokens.any { it.blockchain == blockchain }
|
||||
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Blockchain(blockchain)
|
||||
|
||||
if (isTryingToRemove) {
|
||||
if (isTokenWithSameBlockchainFound) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.TokensAreLinkedDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
currencySymbol = blockchain.currency,
|
||||
),
|
||||
)
|
||||
} else if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
onOk = {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOff()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
}
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
addedBlockchainsState.addAndNotify(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleToken(
|
||||
token: TokenWithBlockchain,
|
||||
tokensAddedOnMainScreen: List<TokenWithBlockchain>,
|
||||
addedTokensState: MutableState<List<TokenWithBlockchain>>,
|
||||
tokensState: TokensState,
|
||||
) {
|
||||
val isTryingToRemove = addedTokensState.value.contains(token)
|
||||
val isAddedOnMainScreen = tokensAddedOnMainScreen.contains(token)
|
||||
val isUnsupportedToken = !tokensState.canHandleToken(token)
|
||||
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Token(token.token)
|
||||
|
||||
if (isTryingToRemove) {
|
||||
if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = token.token.name,
|
||||
onOk = {
|
||||
analyticsCurrencyTypeParam.sendOff()
|
||||
addedTokensState.removeAndNotify(token)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOff()
|
||||
addedTokensState.removeAndNotify(token)
|
||||
}
|
||||
} else {
|
||||
if (isUnsupportedToken) {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.alert_manage_tokens_unsupported_message,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
addedTokensState.addAndNotify(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnalyticsParam.CurrencyType.sendOn() {
|
||||
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.On))
|
||||
}
|
||||
|
||||
private fun AnalyticsParam.CurrencyType.sendOff() {
|
||||
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.Off))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = keyboardState.height)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_save_changes),
|
||||
onClick = onSaveChanges,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun CurrenciesWarning() {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.background(
|
||||
color = Color(0xFFF2F2F2),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
),
|
||||
) {
|
||||
val warning = stringResource(id = R.string.alert_manage_tokens_addresses_message)
|
||||
val end = warning.indexOf(" ")
|
||||
val spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(fontWeight = FontWeight.Bold),
|
||||
start = 0,
|
||||
end = end,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = AnnotatedString(text = warning, spanStyles = spanStyles),
|
||||
textAlign = TextAlign.Start,
|
||||
color = Color(0xFF848488),
|
||||
modifier = Modifier.padding(
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun CurrencyPlaceholderIcon(id: String) {
|
||||
val letterColor: Color = Color.White
|
||||
val circleColor: Color = Color.Black
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.background(circleColor, shape = CircleShape),
|
||||
) {
|
||||
Text(
|
||||
text = id.firstOrNull()?.titlecase() ?: "",
|
||||
textAlign = TextAlign.Center,
|
||||
color = letterColor,
|
||||
modifier = Modifier.padding(4.dp),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.common.compose.extensions.HideKeyboardOnScroll
|
||||
import com.tangem.tap.common.compose.extensions.OnBottomReached
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem.CurrencyItem
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun ListOfCurrencies(
|
||||
header: @Composable () -> Unit,
|
||||
currencies: List<Currency>,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
allowToAdd: Boolean,
|
||||
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClick: (ContractAddress) -> Unit,
|
||||
onLoadMore: () -> Unit,
|
||||
) {
|
||||
val expandedCurrencies = remember { mutableStateOf(listOf("")) }
|
||||
|
||||
val onCurrencyClick = { currencyId: String ->
|
||||
val mutableList = expandedCurrencies.value.toMutableList()
|
||||
if (mutableList.contains(currencyId)) {
|
||||
mutableList.remove(currencyId)
|
||||
} else {
|
||||
mutableList.add(currencyId)
|
||||
}
|
||||
expandedCurrencies.value = mutableList
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
listState.HideKeyboardOnScroll()
|
||||
listState.OnBottomReached(loadMoreThreshold = 40) { onLoadMore() }
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 90.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item { header() }
|
||||
itemsIndexed(currencies) { _, currency ->
|
||||
CurrencyItem(
|
||||
currency = currency,
|
||||
addedTokens = addedTokens,
|
||||
addedBlockchains = addedBlockchains,
|
||||
allowToAdd = allowToAdd,
|
||||
isExpanded = expandedCurrencies.value.contains(currency.id),
|
||||
onCurrencyClick = { onCurrencyClick(currency.id) },
|
||||
onAddCurrencyToggle = onAddCurrencyToggle,
|
||||
onNetworkItemClick = onNetworkItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Currency.fullName: String
|
||||
get() = "${this.name} (${this.symbol})"
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.domain.tokens.Contract
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun CurrencyExpandedContent(
|
||||
currency: Currency,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
allowToAdd: Boolean,
|
||||
isExpanded: Boolean,
|
||||
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClick: (ContractAddress) -> Unit,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
val blockchains = currency.contracts.map { it.blockchain }
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
blockchains.mapIndexed { index, blockchain ->
|
||||
val currencyContract = currency.contracts
|
||||
.firstOrNull { it.blockchain == blockchain }
|
||||
?: return@mapIndexed
|
||||
|
||||
val added = if (currencyContract.address != null) {
|
||||
addedTokens.any(currencyContract::isInclude)
|
||||
} else {
|
||||
addedBlockchains.contains(blockchain)
|
||||
}
|
||||
NetworkItem(
|
||||
currency = currency,
|
||||
contract = currencyContract,
|
||||
blockchain = blockchain,
|
||||
allowToAdd = allowToAdd,
|
||||
added = added,
|
||||
index = index,
|
||||
size = blockchains.size,
|
||||
onAddCurrencyToggle = onAddCurrencyToggle,
|
||||
onNetworkItemClick = onNetworkItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
|
||||
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun CurrencyItem(
|
||||
currency: Currency,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
allowToAdd: Boolean,
|
||||
isExpanded: Boolean,
|
||||
onCurrencyClick: () -> Unit,
|
||||
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClick: (ContractAddress) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
CurrencyItemHeader(
|
||||
currency = currency,
|
||||
addedTokens = addedTokens,
|
||||
addedBlockchains = addedBlockchains,
|
||||
isExpanded = isExpanded,
|
||||
onCurrencyClick = onCurrencyClick,
|
||||
)
|
||||
CurrencyExpandedContent(
|
||||
currency = currency,
|
||||
addedTokens = addedTokens,
|
||||
addedBlockchains = addedBlockchains,
|
||||
allowToAdd = allowToAdd,
|
||||
isExpanded = isExpanded,
|
||||
onAddCurrencyToggle = onAddCurrencyToggle,
|
||||
onNetworkItemClick = onNetworkItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||
import com.tangem.tap.domain.tokens.Contract
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrencyPlaceholderIcon
|
||||
import com.tangem.tap.features.tokens.legacy.ui.compose.fullName
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun CurrencyItemHeader(
|
||||
currency: Currency,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
isExpanded: Boolean,
|
||||
onCurrencyClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 62.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onCurrencyClick,
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.clip(RoundedCornerShape(6.dp)),
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.size(46.dp),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(currency.iconUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = currency.id,
|
||||
loading = { CurrencyPlaceholderIcon(currency.id) },
|
||||
error = { CurrencyPlaceholderIcon(currency.id) },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = currency.fullName,
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF1C1C1E),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(6.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(20.dp),
|
||||
) {
|
||||
if (isExpanded) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
text = stringResource(id = R.string.currency_subtitle_expanded),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF8E8E93),
|
||||
)
|
||||
} else {
|
||||
NetworksRow(
|
||||
currency = currency,
|
||||
addedTokens = addedTokens,
|
||||
addedBlockchains = addedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val expandCollapseArrow = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed
|
||||
Image(
|
||||
painter = painterResource(id = expandCollapseArrow),
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.padding(20.dp)
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworksRow(
|
||||
currency: Currency,
|
||||
addedTokens: List<TokenWithBlockchain>,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
) {
|
||||
Row {
|
||||
if (currency.contracts.isNotEmpty()) {
|
||||
currency.contracts.map { contract ->
|
||||
if (contract.address == null) {
|
||||
BlockchainNetworkItem(
|
||||
blockchain = contract.blockchain,
|
||||
isMainNetwork = true,
|
||||
addedBlockchains = addedBlockchains,
|
||||
)
|
||||
} else {
|
||||
val isAdded = addedTokens.any(contract::isInclude)
|
||||
val iconResId = if (isAdded) {
|
||||
getActiveIconRes(contract.blockchain.id)
|
||||
} else {
|
||||
contract.blockchain.getGreyedOutIconRes()
|
||||
}
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(20.dp),
|
||||
painter = painterResource(id = iconResId),
|
||||
contentDescription = null,
|
||||
)
|
||||
Spacer(Modifier.size(5.dp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BlockchainNetworkItem(
|
||||
blockchain = Blockchain.fromNetworkId(currency.id),
|
||||
isMainNetwork = false,
|
||||
addedBlockchains = addedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun BlockchainNetworkItem(
|
||||
blockchain: Blockchain?,
|
||||
isMainNetwork: Boolean,
|
||||
addedBlockchains: List<Blockchain>,
|
||||
) {
|
||||
val added = addedBlockchains.contains(blockchain)
|
||||
val icon = if (added) blockchain?.let { getActiveIconRes(it.id) } else blockchain?.getGreyedOutIconRes()
|
||||
if (icon != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(20.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
Modifier
|
||||
.size(20.dp),
|
||||
)
|
||||
if (isMainNetwork) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(7.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF1ACE80)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(5.dp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
|
||||
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.legacy.ui.compose.currencyItem
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.SwitchDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
|
||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||
import com.tangem.tap.common.extensions.getNetworkName
|
||||
import com.tangem.tap.domain.tokens.Contract
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.impl.presentation.ui.NetworkItemArrow
|
||||
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.legacy.ui.compose.CurrencyPlaceholderIcon
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod", "MagicNumber")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NetworkItem(
|
||||
currency: Currency,
|
||||
contract: Contract,
|
||||
blockchain: Blockchain,
|
||||
allowToAdd: Boolean,
|
||||
added: Boolean,
|
||||
index: Int,
|
||||
size: Int,
|
||||
onAddCurrencyToggle: (Currency, TokenWithBlockchain?) -> Unit,
|
||||
onNetworkItemClick: (ContractAddress) -> Unit,
|
||||
) {
|
||||
val rowHeight = 53.dp
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(rowHeight)
|
||||
.combinedClickable(
|
||||
enabled = allowToAdd,
|
||||
onLongClick = {
|
||||
contract.address?.let { onNetworkItemClick(it) }
|
||||
},
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(78.dp)
|
||||
.padding(start = 38.dp),
|
||||
) {
|
||||
NetworkItemArrow(
|
||||
itemHeight = rowHeight,
|
||||
isLastItem = index == size - 1,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = if (added) getActiveIconRes(blockchain.id) else blockchain.getGreyedOutIconRes(),
|
||||
contentDescription = blockchain.fullName,
|
||||
loading = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
error = { CurrencyPlaceholderIcon(blockchain.id) },
|
||||
modifier = Modifier
|
||||
.size(20.dp),
|
||||
)
|
||||
if (contract.address == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(7.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF1ACE80)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically),
|
||||
text = prepareNetworkNameSpannableText(
|
||||
blockchain = blockchain,
|
||||
contractAddress = contract.address,
|
||||
),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
color = if (added) Color.Black else Color(0xFF848488),
|
||||
)
|
||||
|
||||
if (allowToAdd) {
|
||||
val token = if (contract.address != null) {
|
||||
Token(
|
||||
id = currency.id,
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
contractAddress = contract.address,
|
||||
decimals = contract.decimalCount!!,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val tokenWithBlockchain = token?.let { TokenWithBlockchain(it, contract.blockchain) }
|
||||
|
||||
val currencyToSave = if (contract.address == null) {
|
||||
currency.copy(id = contract.networkId)
|
||||
} else {
|
||||
currency
|
||||
}
|
||||
|
||||
Switch(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
checked = added,
|
||||
onCheckedChange = { onAddCurrencyToggle(currencyToSave, tokenWithBlockchain) },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color(0xFF1ACE80),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun prepareNetworkNameSpannableText(blockchain: Blockchain, contractAddress: String?): AnnotatedString {
|
||||
val blockchainName = blockchain.fullNameWithoutTestnet.uppercase()
|
||||
val additionalText =
|
||||
if (contractAddress == null) "MAIN" else blockchain.getNetworkName().uppercase()
|
||||
|
||||
val text = "$blockchainName $additionalText"
|
||||
|
||||
val startOfAdditionalText =
|
||||
if (additionalText.isNotBlank()) text.indexOf(additionalText) else text.length
|
||||
|
||||
val spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (contractAddress != null) Color(0xFF8E8E93) else Color(0xFF1ACE80),
|
||||
),
|
||||
start = startOfAdditionalText,
|
||||
end = text.length,
|
||||
),
|
||||
)
|
||||
return AnnotatedString(text = text, spanStyles = spanStyles)
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package com.tangem.tap.features.wallet.redux.middlewares
|
|||
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
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.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
|
|
@ -14,7 +16,6 @@ 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.persistence.FiatCurrenciesPrefStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
|
|
@ -47,8 +48,10 @@ class AppCurrencyMiddleware(
|
|||
|
||||
scope.launch {
|
||||
runCatching { walletRepository.getCurrencyList() }
|
||||
.onSuccess {
|
||||
val currenciesList = it.currencies
|
||||
.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(
|
||||
|
|
@ -64,7 +67,9 @@ class AppCurrencyMiddleware(
|
|||
|
||||
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
|
||||
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency)))
|
||||
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
|
||||
fiatCurrenciesPrefStorage.saveAppCurrency(
|
||||
with(action.fiatCurrency) { DataSourceFiatCurrency(code, name, symbol) },
|
||||
)
|
||||
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
|
|
@ -77,7 +82,7 @@ class AppCurrencyMiddleware(
|
|||
}
|
||||
}
|
||||
|
||||
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {
|
||||
private fun List<DataSourceCurrency>.mapToUiModel(): List<FiatCurrency> {
|
||||
return this.map {
|
||||
FiatCurrency(
|
||||
code = it.code,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class WarningsMiddleware {
|
|||
is WalletAction.Warnings.CheckIfNeeded -> {
|
||||
showCardWarningsIfNeeded(globalState)
|
||||
val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow()
|
||||
if (readyToShow) addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
|
||||
if (readyToShow) addWarningMessage(warning = WarningMessagesManager.appRatingWarning, autoUpdate = true)
|
||||
}
|
||||
is WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline -> checkHashesCountOnline()
|
||||
is WalletAction.Warnings.CheckHashesCount.SaveCardId -> {
|
||||
|
|
@ -69,7 +69,7 @@ class WarningsMiddleware {
|
|||
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
|
||||
}
|
||||
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
|
||||
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
|
||||
addWarningMessage(WarningMessagesManager.appRatingWarning, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,21 +78,21 @@ class WarningsMiddleware {
|
|||
val card = scanResponse.card
|
||||
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
|
||||
if (card.isTestCard) {
|
||||
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
|
||||
addWarningMessage(WarningMessagesManager.testCardWarning, autoUpdate = true)
|
||||
return@let
|
||||
}
|
||||
|
||||
showWarningLowRemainingSignaturesIfNeeded(card)
|
||||
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
|
||||
addWarningMessage(WarningMessagesManager.devCardWarning())
|
||||
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())
|
||||
addWarningMessage(WarningMessagesManager.onlineVerificationFailed)
|
||||
}
|
||||
if (scanResponse.isDemoCard()) {
|
||||
addWarningMessage(WarningMessagesManager.demoCardWarning())
|
||||
addWarningMessage(WarningMessagesManager.demoCardWarning)
|
||||
}
|
||||
setWarningMessages()
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ class WarningsMiddleware {
|
|||
if (scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
|
||||
val isBackupForbidden = with(scanResponse.card.settings) { !(isBackupAllowed || isHDWalletAllowed) }
|
||||
return if (scanResponse.card.hasSignedHashes() && isBackupForbidden) {
|
||||
WarningMessagesManager.signedHashesMultiWalletWarning()
|
||||
WarningMessagesManager.signedHashesMultiWalletWarning
|
||||
} else {
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
|
||||
null
|
||||
|
|
@ -123,7 +123,7 @@ class WarningsMiddleware {
|
|||
val validator = store.state.walletState.walletManagers.firstOrNull() as? SignatureCountValidator
|
||||
return if (validator == null) {
|
||||
if (scanResponse.card.hasSignedHashes()) {
|
||||
WarningMessagesManager.alreadySignedHashesWarning()
|
||||
WarningMessagesManager.alreadySignedHashesWarning
|
||||
} else {
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
|
||||
null
|
||||
|
|
@ -161,9 +161,9 @@ class WarningsMiddleware {
|
|||
}
|
||||
is SimpleResult.Failure ->
|
||||
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning, true)
|
||||
} else if (signedHashes > 0) {
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
|
||||
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning, true)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.tap.features.wallet.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.ColorRes
|
||||
|
|
@ -27,8 +31,16 @@ 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.extensions.appendIfNotNull
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
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.common.extensions.toQrCode
|
||||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.common.utils.SafeStoreSubscriber
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
|
|
@ -45,7 +57,15 @@ 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.features.wallet.ui.utils.assembleWarnings
|
||||
import com.tangem.tap.features.wallet.ui.utils.getAvailableActions
|
||||
import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount
|
||||
import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount
|
||||
import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy
|
||||
import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell
|
||||
import com.tangem.tap.features.wallet.ui.utils.isAvailableToSwap
|
||||
import com.tangem.tap.features.wallet.ui.utils.mainButton
|
||||
import com.tangem.tap.features.wallet.ui.utils.shouldShowMultipleAddress
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.userWalletsListManagerSafe
|
||||
import com.tangem.tap.walletCurrenciesManager
|
||||
|
|
@ -63,9 +83,7 @@ import javax.inject.Inject
|
|||
*/
|
||||
@Suppress("LargeClass", "MagicNumber")
|
||||
@AndroidEntryPoint
|
||||
class WalletDetailsFragment :
|
||||
Fragment(R.layout.fragment_wallet_details),
|
||||
SafeStoreSubscriber<WalletState> {
|
||||
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeStoreSubscriber<WalletState> {
|
||||
|
||||
@Inject
|
||||
lateinit var swapInteractor: SwapInteractor
|
||||
|
|
@ -157,18 +175,6 @@ class WalletDetailsFragment :
|
|||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.select { it.walletState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
|
@ -180,6 +186,16 @@ class WalletDetailsFragment :
|
|||
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()
|
||||
|
|
@ -198,18 +214,13 @@ class WalletDetailsFragment :
|
|||
rvWarningMessages.addItemDecoration(SpaceItemDecoration.vertical(8f))
|
||||
}
|
||||
|
||||
private fun setupButtons() = with(binding) {
|
||||
rowButtons.onSendClick = {
|
||||
store.dispatch(WalletAction.Send())
|
||||
}
|
||||
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(),
|
||||
)
|
||||
TestActions.initFor(view = view, actions = TestWallet.solanaRentExemptWarning())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,10 +234,8 @@ class WalletDetailsFragment :
|
|||
|
||||
private fun updateViewMeasurements() {
|
||||
val tvFiatAmount = binding.lWalletDetails.lBalance.tvFiatAmount
|
||||
val paddingStart = when (tvFiatAmount.text) {
|
||||
UNKNOWN_AMOUNT_SIGN -> 16f
|
||||
else -> 12f
|
||||
}
|
||||
val paddingStart = if (tvFiatAmount.text == UNKNOWN_AMOUNT_SIGN) 16f else 12f
|
||||
|
||||
tvFiatAmount.setPadding(
|
||||
tvFiatAmount.dpToPx(paddingStart).toInt(),
|
||||
tvFiatAmount.paddingTop,
|
||||
|
|
@ -259,12 +268,11 @@ class WalletDetailsFragment :
|
|||
}
|
||||
binding.srlWalletDetails.isRefreshing = true
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
walletCurrenciesManager.update(selectedUserWallet, walletData.currency)
|
||||
.doOnResult {
|
||||
withMainContext {
|
||||
binding.srlWalletDetails.isRefreshing = false
|
||||
}
|
||||
walletCurrenciesManager.update(selectedUserWallet, walletData.currency).doOnResult {
|
||||
withMainContext {
|
||||
binding.srlWalletDetails.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,14 +280,11 @@ class WalletDetailsFragment :
|
|||
|
||||
private fun setupCopyAndShareButtons(walletAddress: String?) {
|
||||
binding.lWalletDetails.btnCopy.setOnClickListener {
|
||||
if (walletAddress != null) {
|
||||
store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext()))
|
||||
}
|
||||
if (walletAddress != null) store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext()))
|
||||
}
|
||||
|
||||
binding.lWalletDetails.btnShare.setOnClickListener {
|
||||
if (walletAddress != null) {
|
||||
store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext()))
|
||||
}
|
||||
if (walletAddress != null) store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -339,9 +344,7 @@ class WalletDetailsFragment :
|
|||
}
|
||||
|
||||
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
|
||||
val knownTransactions = pendingTransactions.filterNot {
|
||||
it.type == PendingTransactionType.Unknown
|
||||
}
|
||||
val knownTransactions = pendingTransactions.filterNot { it.type == PendingTransactionType.Unknown }
|
||||
pendingTransactionAdapter.submitList(knownTransactions)
|
||||
binding.rvPendingTransaction.show(knownTransactions.isNotEmpty())
|
||||
}
|
||||
|
|
@ -387,8 +390,7 @@ class WalletDetailsFragment :
|
|||
chipGroupAddressType.show()
|
||||
chipGroupAddressType.fitChipsByGroupWidth()
|
||||
|
||||
val checkedId =
|
||||
MultipleAddressUiHelper.typeToId(selectedAddress.type)
|
||||
val checkedId = MultipleAddressUiHelper.typeToId(selectedAddress.type)
|
||||
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
|
||||
|
||||
chipGroupAddressType.setOnCheckedChangeListener { _, checkedId ->
|
||||
|
|
@ -443,11 +445,14 @@ class WalletDetailsFragment :
|
|||
-> {
|
||||
lBalance.tvStatus.setVerifiedBalanceStatus(R.string.wallet_balance_verified)
|
||||
}
|
||||
else -> {
|
||||
|
||||
is WalletDataModel.TransactionInProgress -> {
|
||||
lBalance.tvStatus.setWarningStatus(R.string.wallet_balance_tx_in_progress)
|
||||
showPendingTransactionsIfPresent(status.pendingTransactions)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
showPendingTransactionsIfPresent(status.pendingTransactions)
|
||||
}
|
||||
is WalletDataModel.Unreachable -> {
|
||||
lBalanceError.root.hide()
|
||||
|
|
@ -470,10 +475,12 @@ class WalletDetailsFragment :
|
|||
walletData.currency.currencySymbol,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_remove -> {
|
||||
|
|
@ -483,10 +490,15 @@ class WalletDetailsFragment :
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,14 +105,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
|
||||
observeWalletStoresUpdatesJob = manager.selectedUserWallet
|
||||
.map { it.walletId }
|
||||
.flatMapLatest { selectedUserWalletId ->
|
||||
walletStoresManager.get(selectedUserWalletId)
|
||||
}
|
||||
.flatMapLatest(walletStoresManager::get)
|
||||
.debounce { walletStores ->
|
||||
if (walletStores.isNotEmpty()) WALLET_STORES_DEBOUNCE_TIMEOUT else 0
|
||||
}
|
||||
.onEach { walletStores ->
|
||||
store.dispatch(WalletAction.WalletStoresChanged(walletStores))
|
||||
store.dispatchOnMain(WalletAction.WalletStoresChanged(walletStores))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ object SignedHashesWarningDialog {
|
|||
setPositiveButton(R.string.common_understand) { _, _ ->
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
|
||||
store.dispatch(
|
||||
GlobalAction.HideWarningMessage(
|
||||
WarningMessagesManager.signedHashesMultiWalletWarning(),
|
||||
),
|
||||
GlobalAction.HideWarningMessage(WarningMessagesManager.signedHashesMultiWalletWarning),
|
||||
)
|
||||
}
|
||||
setNegativeButton(R.string.common_cancel) { _, _ -> }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.badoo.mvicore.modelWatcher
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.analytics.events.Portfolio
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.getQuantityString
|
||||
|
|
@ -108,16 +107,13 @@ class MultiWalletView : WalletView() {
|
|||
binding.btnAddToken.setOnClickListener {
|
||||
val card = store.state.globalState.scanResponse!!.card
|
||||
Analytics.send(Portfolio.ButtonManageTokens())
|
||||
store.dispatch(TokensAction.LoadCurrencies(scanResponse = store.state.globalState.scanResponse))
|
||||
store.dispatch(TokensAction.AllowToAddTokens(true))
|
||||
|
||||
store.dispatch(
|
||||
TokensAction.SetAddedCurrencies(
|
||||
TokensAction.SetArgs.ManageAccess(
|
||||
wallets = state.walletsDataFromStores,
|
||||
derivationStyle = card.derivationStyle,
|
||||
),
|
||||
)
|
||||
|
||||
Analytics.send(ManageTokens.ScreenOpened())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
}
|
||||
handleErrorStates(state = state, binding = binding, fragment = fragment)
|
||||
|
|
@ -175,7 +171,8 @@ class MultiWalletView : WalletView() {
|
|||
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle),
|
||||
)
|
||||
}
|
||||
else -> { /* no-op */ }
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ internal sealed interface WalletSelectorAction : Action {
|
|||
) : WalletSelectorAction
|
||||
|
||||
object CloseError : WalletSelectorAction
|
||||
|
||||
object ClearUserWallets : WalletSelectorAction
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.tangem.common.doOnSuccess
|
|||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.map
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.events.MyWallets
|
||||
|
|
@ -81,6 +81,12 @@ internal class WalletSelectorMiddleware {
|
|||
is WalletSelectorAction.ChangeAppCurrency -> {
|
||||
refreshUserWalletsAmounts()
|
||||
}
|
||||
is WalletSelectorAction.ClearUserWallets -> scope.launch {
|
||||
clearUserWallets()
|
||||
.doOnFailure { e ->
|
||||
Timber.e(e, "Unable to clear user wallets")
|
||||
}
|
||||
}
|
||||
is WalletSelectorAction.AddWallet.Success,
|
||||
is WalletSelectorAction.AddWallet.Error,
|
||||
is WalletSelectorAction.SelectedWalletChanged,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ internal object WalletSelectorReducer {
|
|||
is WalletSelectorAction.SelectWallet,
|
||||
is WalletSelectorAction.RemoveWallets,
|
||||
is WalletSelectorAction.RenameWallet,
|
||||
is WalletSelectorAction.ClearUserWallets,
|
||||
-> state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
|
|
@ -46,11 +45,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<WalletSelectorScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
}
|
||||
override fun provideState(): State<WalletSelectorScreenState> = viewModel.state.collectAsState()
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
|
|
|||
|
|
@ -200,13 +200,17 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
onDismiss = this::dismissWarningDialog,
|
||||
)
|
||||
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
|
||||
onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onConfirm = this::clearUserWallets,
|
||||
onDismiss = this::dismissWarningDialog,
|
||||
)
|
||||
else -> currentDialog
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearUserWallets() {
|
||||
store.dispatch(WalletSelectorAction.ClearUserWallets)
|
||||
}
|
||||
|
||||
private fun dismissWarningDialog() {
|
||||
stateInternal.update { prevState ->
|
||||
prevState.copy(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -174,8 +173,8 @@ private fun Footer(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = dimensionResource(id = R.dimen.spacing24),
|
||||
bottom = dimensionResource(id = R.dimen.spacing16),
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ internal sealed interface WelcomeAction : Action {
|
|||
data class HandleIntentIfNeeded(val intent: Intent?) : WelcomeAction
|
||||
|
||||
object CloseError : WelcomeAction
|
||||
|
||||
object ClearUserWallets : WelcomeAction
|
||||
}
|
||||
|
|
@ -3,11 +3,14 @@ package com.tangem.tap.features.welcome.redux
|
|||
import android.content.Intent
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnResult
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
|
|
@ -51,6 +54,9 @@ internal class WelcomeMiddleware {
|
|||
is WelcomeAction.HandleIntentIfNeeded -> {
|
||||
handleInitialIntent(action.intent)
|
||||
}
|
||||
is WelcomeAction.ClearUserWallets -> {
|
||||
clearUserWallets()
|
||||
}
|
||||
is WelcomeAction.ProceedWithBiometrics.Error,
|
||||
is WelcomeAction.ProceedWithCard.Error,
|
||||
is WelcomeAction.ProceedWithBiometrics.Success,
|
||||
|
|
@ -60,22 +66,31 @@ internal class WelcomeMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedWithBiometrics(state: WelcomeState) {
|
||||
scope.launch {
|
||||
userWalletsListManager.unlockIfLockable()
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to unlock user wallets with biometrics")
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error))
|
||||
}
|
||||
.doOnSuccess { selectedUserWallet ->
|
||||
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric))
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
|
||||
store.onUserWalletSelected(userWallet = selectedUserWallet)
|
||||
private fun clearUserWallets() = scope.launch {
|
||||
userWalletsListManager.clear()
|
||||
.flatMap { tangemSdkManager.clearSavedUserCodes() }
|
||||
.doOnFailure { e ->
|
||||
Timber.e(e, "Unable to clear user wallets")
|
||||
}
|
||||
.doOnResult {
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
|
||||
intentHandler.handleWalletConnectLink(state.intent)
|
||||
}
|
||||
}
|
||||
private fun proceedWithBiometrics(state: WelcomeState) = scope.launch {
|
||||
userWalletsListManager.unlockIfLockable()
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to unlock user wallets with biometrics")
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error))
|
||||
}
|
||||
.doOnSuccess { selectedUserWallet ->
|
||||
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric))
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
|
||||
store.onUserWalletSelected(userWallet = selectedUserWallet)
|
||||
|
||||
intentHandler.handleWalletConnectLink(state.intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithCard(state: WelcomeState) = scope.launch {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
|
|||
onDismiss = this::dismissWarning,
|
||||
)
|
||||
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
|
||||
onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onConfirm = this::clearUserWallets,
|
||||
onDismiss = this::dismissWarning,
|
||||
)
|
||||
else -> null
|
||||
|
|
@ -88,6 +88,10 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
|
|||
closeError()
|
||||
}
|
||||
|
||||
private fun clearUserWallets() {
|
||||
store.dispatch(WelcomeAction.ClearUserWallets)
|
||||
}
|
||||
|
||||
private fun subscribeToStoreChanges() {
|
||||
store.subscribe(this) { appState ->
|
||||
appState.skip { old, new -> old.welcomeState == new.welcomeState }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
|
|||
* Temporary wrapper for the buy services. Service switches based on selected product type.
|
||||
* Just now - UtorgService used only for the SaltPay cards
|
||||
*/
|
||||
class BuyExchangeService(
|
||||
internal class BuyExchangeService(
|
||||
private val productTypeProvider: () -> ProductType?,
|
||||
private val mercuryoService: MercuryoService,
|
||||
private val utorgService: UtorgExchangeService,
|
||||
|
|
|
|||
|
|
@ -11,57 +11,21 @@ import com.tangem.tap.features.wallet.models.Currency
|
|||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class MercuryoService(
|
||||
private val environment: MercuryoEnvironment,
|
||||
) : ExchangeService {
|
||||
internal class MercuryoService(private val environment: MercuryoEnvironment) : ExchangeService {
|
||||
|
||||
private val api: MercuryoApi = environment.mercuryoApi
|
||||
|
||||
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
|
||||
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
|
||||
private val blockchainsAvailableToBuy = CopyOnWriteArrayList<Blockchain>()
|
||||
private val tokensAvailableToBuy = ConcurrentHashMap<String, List<Blockchain>>()
|
||||
|
||||
override fun featureIsSwitchedOn(): Boolean = true
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override suspend fun update() {
|
||||
when (val result = performRequest { api.currencies(environment.apiVersion) }) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.status == RESPONSE_SUCCESS_STATUS_CODE) {
|
||||
// all currencies which can be bought
|
||||
val currenciesAvailableToBy = response.data.crypto
|
||||
// tokens which can be bought only from specific blockchain network
|
||||
val supportedTokensWithNetwork = response.data.config.base
|
||||
|
||||
currenciesAvailableToBy.forEach { currencyName ->
|
||||
val blockchain = blockchainFromCurrencyName(currencyName)
|
||||
if (blockchain == null) {
|
||||
// suppose its a token
|
||||
supportedTokensWithNetwork[currencyName]?.let {
|
||||
blockchainFromCurrencyName(it)
|
||||
}?.let { blockchainNetwork ->
|
||||
val supportedInBlockchainsNetwork = tokensAvailableToBy[currencyName]
|
||||
?: mutableListOf()
|
||||
supportedInBlockchainsNetwork.add(blockchainNetwork)
|
||||
tokensAvailableToBy[currencyName] = supportedInBlockchainsNetwork
|
||||
}
|
||||
} else {
|
||||
blockchainsAvailableToBuy.add(blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
blockchainsAvailableToBuy.clear()
|
||||
tokensAvailableToBy.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isBuyAllowed(): Boolean = true
|
||||
|
||||
override fun isSellAllowed(): Boolean = false
|
||||
|
|
@ -83,20 +47,32 @@ class MercuryoService(
|
|||
when {
|
||||
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
|
||||
unsupportedBlockchains.contains(blockchain) -> false
|
||||
else -> {
|
||||
blockchainsAvailableToBuy.contains(currency.blockchain)
|
||||
}
|
||||
else -> blockchainsAvailableToBuy.contains(blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
is Currency.Token -> {
|
||||
val supportedInBlockchains = tokensAvailableToBy[currency.currencySymbol] ?: return false
|
||||
supportedInBlockchains.contains(currency.blockchain)
|
||||
val supportedInBlockchains = tokensAvailableToBuy[currency.currencySymbol] ?: return false
|
||||
supportedInBlockchains.contains(blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun availableForSell(currency: Currency): Boolean = false
|
||||
|
||||
override suspend fun update() {
|
||||
val result = performRequest { api.currencies(environment.apiVersion) }
|
||||
when {
|
||||
result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> {
|
||||
handleSuccessfullyUpdatedData(data = result.data.data)
|
||||
}
|
||||
result is Result.Failure -> {
|
||||
blockchainsAvailableToBuy.clear()
|
||||
tokensAvailableToBuy.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUrl(
|
||||
action: CurrencyExchangeManager.Action,
|
||||
blockchain: Blockchain,
|
||||
|
|
@ -117,24 +93,40 @@ class MercuryoService(
|
|||
.appendQueryParameter("fix_currency", "true")
|
||||
.appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL)
|
||||
|
||||
val url = builder.build().toString()
|
||||
return url
|
||||
}
|
||||
|
||||
private fun signature(address: String): String {
|
||||
return (address + environment.secret).calculateSha512().toHexString().lowercase()
|
||||
return builder.build().toString()
|
||||
}
|
||||
|
||||
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
|
||||
|
||||
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) {
|
||||
"BNB" -> Blockchain.BSC
|
||||
"ETH" -> Blockchain.Ethereum
|
||||
"ADA" -> Blockchain.CardanoShelley
|
||||
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
|
||||
private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) {
|
||||
data.crypto.forEach { currencyName ->
|
||||
val blockchain = blockchainFromCurrencyName(currencyName)
|
||||
if (blockchain == null) {
|
||||
val specificBlockchain = data.config.base[currencyName]?.let(::blockchainFromCurrencyName)
|
||||
if (specificBlockchain != null) {
|
||||
tokensAvailableToBuy.set(
|
||||
key = currencyName,
|
||||
value = tokensAvailableToBuy[currencyName].orEmpty() + specificBlockchain,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
blockchainsAvailableToBuy.add(blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RESPONSE_SUCCESS_STATUS_CODE = 200
|
||||
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? {
|
||||
return when (currencyName) {
|
||||
"BNB" -> Blockchain.BSC
|
||||
"ETH" -> Blockchain.Ethereum
|
||||
"ADA" -> Blockchain.CardanoShelley
|
||||
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase()
|
||||
|
||||
private companion object {
|
||||
const val RESPONSE_SUCCESS_STATUS_CODE = 200
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
||||
class CardBalanceStateAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(src: AnalyticsParam.CardBalanceState): String = src.value
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): AnalyticsParam.CardBalanceState {
|
||||
return when (json) {
|
||||
AnalyticsParam.CardBalanceState.Empty.value -> AnalyticsParam.CardBalanceState.Empty
|
||||
AnalyticsParam.CardBalanceState.Full.value -> AnalyticsParam.CardBalanceState.Full
|
||||
else -> error("CardBalanceState not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DisclaimerPrefStorage(
|
||||
private val preferences: SharedPreferences,
|
||||
) {
|
||||
|
||||
fun accept(disclaimerKey: String) {
|
||||
preferences.edit { putBoolean(disclaimerKey, true) }
|
||||
}
|
||||
|
||||
fun isAccepted(disclaimerKey: String): Boolean {
|
||||
return preferences.getBoolean(disclaimerKey, false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FiatCurrenciesPrefStorage(
|
||||
private val preferences: SharedPreferences,
|
||||
private val converter: MoshiJsonConverter,
|
||||
) {
|
||||
fun migrate() {
|
||||
preferences.edit(true) {
|
||||
remove(FIAT_CURRENCIES_KEY_OLD)
|
||||
remove(APP_CURRENCY_KEY_OLD)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppCurrency(): FiatCurrency {
|
||||
val json = preferences.getString(APP_CURRENCY_KEY, "")
|
||||
if (json.isNullOrBlank()) return FiatCurrency.Default
|
||||
|
||||
return converter.fromJson(json) ?: FiatCurrency.Default
|
||||
}
|
||||
|
||||
fun saveAppCurrency(fiatCurrency: FiatCurrency) {
|
||||
val json = converter.toJson(fiatCurrency)
|
||||
preferences.edit { putString(APP_CURRENCY_KEY, json) }
|
||||
}
|
||||
|
||||
fun save(currencies: List<CurrenciesResponse.Currency>) {
|
||||
val json: String = converter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
}
|
||||
|
||||
fun restore(): List<CurrenciesResponse.Currency> {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
val type = converter.typedList(CurrenciesResponse.Currency::class.java)
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
|
||||
return converter.fromJson(json, type) ?: emptyList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FIAT_CURRENCIES_KEY_OLD = "fiatCurrencies"
|
||||
private const val APP_CURRENCY_KEY_OLD = "appCurrency"
|
||||
|
||||
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies_v2"
|
||||
private const val APP_CURRENCY_KEY = "appCurrency_v2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.common.BigDecimalAdapter
|
||||
import java.util.*
|
||||
|
||||
class PreferencesStorage(applicationContext: Application) {
|
||||
|
||||
val appRatingLaunchObserver: AppRatingLaunchObserver
|
||||
val usedCardsPrefStorage: UsedCardsPrefStorage
|
||||
val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage
|
||||
val disclaimerPrefStorage: DisclaimerPrefStorage
|
||||
val toppedUpWalletStorage: ToppedUpWalletStorage
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private val moshiConverter = MoshiJsonConverter(
|
||||
adapters = listOf(BigDecimalAdapter(), CardBalanceStateAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
|
||||
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
|
||||
)
|
||||
|
||||
init {
|
||||
incrementLaunchCounter()
|
||||
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
|
||||
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
|
||||
usedCardsPrefStorage.migrate()
|
||||
fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, moshiConverter)
|
||||
fiatCurrenciesPrefStorage.migrate()
|
||||
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
|
||||
toppedUpWalletStorage = ToppedUpWalletStorage(preferences, moshiConverter)
|
||||
}
|
||||
|
||||
var zendeskFirstLaunchTime: Long?
|
||||
get() = preferences.getLong(ZENDESK_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
|
||||
set(value) = preferences.edit { putLong(ZENDESK_FIRST_LAUNCH_KEY, value ?: 0) }
|
||||
|
||||
var sprinklrFirstLaunchTime: Long?
|
||||
get() = preferences.getLong(SPRINKLR_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
|
||||
set(value) = preferences.edit { putLong(SPRINKLR_FIRST_LAUNCH_KEY, value ?: 0) }
|
||||
|
||||
var shouldShowSaveUserWalletScreen: Boolean
|
||||
get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value)
|
||||
}
|
||||
|
||||
var shouldSaveUserWallets: Boolean
|
||||
get() = preferences.getBoolean(SAVE_USER_WALLETS_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(SAVE_USER_WALLETS_KEY, value)
|
||||
}
|
||||
|
||||
var shouldSaveAccessCodes: Boolean
|
||||
get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(SAVE_ACCESS_CODES_KEY, value)
|
||||
}
|
||||
|
||||
var wasApplicationStopped: Boolean
|
||||
get() = preferences.getBoolean(APPLICATION_STOPPED_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(APPLICATION_STOPPED_KEY, value)
|
||||
}
|
||||
|
||||
var shouldOpenWelcomeScreenOnResume: Boolean
|
||||
get() = preferences.getBoolean(OPEN_WELCOME_ON_RESUME_KEY, false)
|
||||
set(value) = preferences.edit {
|
||||
putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value)
|
||||
}
|
||||
|
||||
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
|
||||
|
||||
fun saveTwinsOnboardingShown() {
|
||||
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
|
||||
}
|
||||
|
||||
fun wasTwinsOnboardingShown(): Boolean {
|
||||
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
|
||||
}
|
||||
|
||||
private fun incrementLaunchCounter() {
|
||||
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
|
||||
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES_NAME = "tapPrefs"
|
||||
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
|
||||
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
|
||||
private const val ZENDESK_FIRST_LAUNCH_KEY = "chatFirstLaunchKey"
|
||||
private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch"
|
||||
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
|
||||
private const val SAVE_USER_WALLETS_KEY = "saveUserWallets"
|
||||
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"
|
||||
private const val APPLICATION_STOPPED_KEY = "applicationStopped"
|
||||
private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume"
|
||||
}
|
||||
}
|
||||
|
||||
class AppRatingLaunchObserver(
|
||||
private val preferences: SharedPreferences,
|
||||
private val launchCounts: Int,
|
||||
) {
|
||||
|
||||
private val deferShowing = 20
|
||||
private val firstShowing = 3
|
||||
private var fundsFoundDate: Calendar? = null
|
||||
|
||||
init {
|
||||
val msWhenFundsWasFound = preferences.getLong(K_FUNDS_FOUND_DATE, FUNDS_FOUND_DATE_UNDEFINED)
|
||||
if (msWhenFundsWasFound != FUNDS_FOUND_DATE_UNDEFINED) {
|
||||
fundsFoundDate = Calendar.getInstance().apply { timeInMillis = msWhenFundsWasFound }
|
||||
}
|
||||
}
|
||||
|
||||
fun foundWalletWithFunds() {
|
||||
if (fundsFoundDate != null) return
|
||||
|
||||
fundsFoundDate = Calendar.getInstance()
|
||||
preferences.edit(true) {
|
||||
putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
|
||||
putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, launchCounts + firstShowing)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun isReadyToShow(): Boolean {
|
||||
val fundsDate = fundsFoundDate ?: return false
|
||||
|
||||
if (!userWasInteractWithRating()) {
|
||||
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
|
||||
val diffInDays = diff / (1000 * 60 * 60 * 24)
|
||||
return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing
|
||||
}
|
||||
|
||||
val nextShowing = getCounterOfNextShowing()
|
||||
return launchCounts >= nextShowing
|
||||
}
|
||||
|
||||
fun applyDelayedShowing() {
|
||||
updateNextShowing(launchCounts + deferShowing)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun setNeverToShow() {
|
||||
updateNextShowing(999999999)
|
||||
}
|
||||
|
||||
private fun updateNextShowing(at: Int) {
|
||||
val editor = preferences.edit()
|
||||
editor.putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at)
|
||||
editor.putBoolean(K_USER_WAS_INTERACT_WITH_RATING, true)
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
private fun userWasInteractWithRating(): Boolean = preferences.getBoolean(K_USER_WAS_INTERACT_WITH_RATING, false)
|
||||
|
||||
private fun getCounterOfNextShowing(): Int = preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing)
|
||||
|
||||
companion object {
|
||||
private const val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
|
||||
private const val K_FUNDS_FOUND_DATE = "fundsFoundDate"
|
||||
private const val K_USER_WAS_INTERACT_WITH_RATING = "userWasInteractWithRating"
|
||||
private const val FUNDS_FOUND_DATE_UNDEFINED = -1L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ToppedUpWalletStorage(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) {
|
||||
|
||||
private val walletList: MutableSet<TopupInfo> = mutableSetOf()
|
||||
|
||||
init {
|
||||
walletList.addAll(restore())
|
||||
}
|
||||
|
||||
fun save(userWalletInfo: TopupInfo): Boolean {
|
||||
walletList.removeAll { it.walletId == userWalletInfo.walletId }
|
||||
walletList.add(userWalletInfo)
|
||||
return save(walletList)
|
||||
}
|
||||
|
||||
fun restore(walletId: String): TopupInfo? {
|
||||
return walletList.firstOrNull { it.walletId == walletId }
|
||||
}
|
||||
|
||||
private fun save(userWallets: MutableSet<TopupInfo>): Boolean {
|
||||
return try {
|
||||
val json = jsonConverter.toJson(userWallets)
|
||||
preferences.edit(true) { putString(KEY, json) }
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun restore(): MutableSet<TopupInfo> {
|
||||
val json = preferences.getString(KEY, null) ?: return mutableSetOf()
|
||||
return try {
|
||||
val typedList = jsonConverter.typedList(TopupInfo::class.java)
|
||||
val listData = jsonConverter.fromJson<List<TopupInfo>>(json, typedList)!!
|
||||
listData.toMutableSet()
|
||||
} catch (ex: Exception) {
|
||||
preferences.edit(true) { remove(KEY) }
|
||||
mutableSetOf()
|
||||
}
|
||||
}
|
||||
|
||||
data class TopupInfo(
|
||||
val walletId: String,
|
||||
val cardBalanceState: AnalyticsParam.CardBalanceState,
|
||||
) {
|
||||
val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY = "userWalletsInfo"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
package com.tangem.tap.persistence
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UsedCardsPrefStorage(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) {
|
||||
|
||||
private val migrationList = mutableListOf(
|
||||
UserCardInfoToV2(this),
|
||||
)
|
||||
|
||||
internal fun migrate() {
|
||||
migrationList.forEach { it.migrate() }
|
||||
migrationList.clear()
|
||||
}
|
||||
|
||||
fun scanned(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
|
||||
?: UsedCardInfo(cardId, true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
||||
fun wasScanned(cardId: String): Boolean {
|
||||
return findCardInfo(cardId)?.isScanned ?: false
|
||||
}
|
||||
|
||||
fun activationStarted(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
||||
fun activationFinished(cardId: String) {
|
||||
val restoredList = restore()
|
||||
var foundItem = findCardInfo(cardId, restoredList) ?: UsedCardInfo(cardId)
|
||||
foundItem = foundItem.copy(
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
||||
fun isActivationStarted(cardId: String): Boolean {
|
||||
return findCardInfo(cardId)?.isActivationStarted ?: false
|
||||
}
|
||||
|
||||
fun isActivationFinished(cardId: String): Boolean {
|
||||
return findCardInfo(cardId)?.isActivationFinished ?: false
|
||||
}
|
||||
|
||||
fun isActivationInProgress(cardId: String): Boolean {
|
||||
val cardInfo = findCardInfo(cardId) ?: return false
|
||||
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
|
||||
}
|
||||
|
||||
private fun findCardInfo(cardId: String, list: MutableList<UsedCardInfo>? = null): UsedCardInfo? {
|
||||
val findInList = list ?: restore()
|
||||
return findInList.firstOrNull { it.cardId == cardId }
|
||||
}
|
||||
|
||||
private fun save(usedCardInfo: UsedCardInfo?, usedCardsInfo: MutableList<UsedCardInfo>) {
|
||||
val info = usedCardInfo ?: return
|
||||
|
||||
usedCardsInfo.replaceByOrAdd(info) { it.cardId == info.cardId }
|
||||
save(usedCardsInfo)
|
||||
}
|
||||
|
||||
private fun save(list: MutableList<UsedCardInfo>): Boolean {
|
||||
return try {
|
||||
val json = jsonConverter.toJson(list)
|
||||
preferences.edit { putString(USED_CARDS_INFO_V2, json) }
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun restore(): MutableList<UsedCardInfo> {
|
||||
val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf()
|
||||
return try {
|
||||
jsonConverter.fromJson(json, jsonConverter.typedList(UsedCardInfo::class.java))!!
|
||||
} catch (ex: Exception) {
|
||||
preferences.edit(true) { remove(USED_CARDS_INFO_V2) }
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2"
|
||||
private const val USED_CARDS_INFO = "usedCardsInfo"
|
||||
}
|
||||
|
||||
private class UserCardInfoToV2(
|
||||
private val storage: UsedCardsPrefStorage,
|
||||
) : Migration {
|
||||
override fun migrate() {
|
||||
val restoredCardsInfo = restore()
|
||||
if (restoredCardsInfo.isEmpty()) return
|
||||
|
||||
val newCardsInfo = restoredCardsInfo.map { cardInfo ->
|
||||
UsedCardInfo(
|
||||
cardId = cardInfo.cardId,
|
||||
isScanned = cardInfo.isScanned,
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = !cardInfo.isActivationStarted,
|
||||
)
|
||||
}.toMutableList()
|
||||
storage.save(newCardsInfo)
|
||||
}
|
||||
|
||||
private fun restore(): MutableList<UsedCardInfoOld> {
|
||||
val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
|
||||
return try {
|
||||
storage.jsonConverter.fromJson(json, storage.jsonConverter.typedList(UsedCardInfoOld::class.java))!!
|
||||
} catch (ex: Exception) {
|
||||
mutableListOf()
|
||||
} finally {
|
||||
storage.preferences.edit(true) { remove(USED_CARDS_INFO) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class UsedCardInfo(
|
||||
val cardId: String,
|
||||
val isScanned: Boolean = false,
|
||||
val isActivationStarted: Boolean = false,
|
||||
val isActivationFinished: Boolean = false,
|
||||
)
|
||||
|
||||
private data class UsedCardInfoOld(
|
||||
val cardId: String,
|
||||
val isScanned: Boolean = false,
|
||||
val isActivationStarted: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
||||
private interface Migration {
|
||||
fun migrate()
|
||||
}
|
||||
|
|
@ -107,8 +107,8 @@ class DerivationManagerImpl(
|
|||
val selectedUserWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync
|
||||
|
||||
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
|
||||
scanResponse.card.cardId,
|
||||
derivations,
|
||||
cardId = null, // always ignore cardId in derive task
|
||||
derivations = derivations,
|
||||
)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface DaggerGraphAction : Action {
|
||||
|
||||
data class SetApplicationDependencies(
|
||||
val assetReader: AssetReader,
|
||||
val networkConnectionManager: NetworkConnectionManager,
|
||||
val tokensListFeatureToggles: TokensListFeatureToggles,
|
||||
val customTokenFeatureToggles: CustomTokenFeatureToggles,
|
||||
) : DaggerGraphAction
|
||||
|
||||
data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction
|
||||
}
|
||||
|
|
@ -12,12 +12,6 @@ object DaggerGraphReducer {
|
|||
|
||||
private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState {
|
||||
return when (action) {
|
||||
is DaggerGraphAction.SetApplicationDependencies -> state.daggerGraphState.copy(
|
||||
assetReader = action.assetReader,
|
||||
networkConnectionManager = action.networkConnectionManager,
|
||||
tokensListFeatureToggles = action.tokensListFeatureToggles,
|
||||
customTokenFeatureToggles = action.customTokenFeatureToggles,
|
||||
)
|
||||
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
|
||||
testerRouter = action.testerRouter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import com.tangem.datasource.asset.AssetReader
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DaggerGraphState(
|
||||
val assetReader: AssetReader? = null,
|
||||
val testerRouter: TesterRouter? = null,
|
||||
val networkConnectionManager: NetworkConnectionManager? = null,
|
||||
val tokensListFeatureToggles: TokensListFeatureToggles? = null,
|
||||
val customTokenFeatureToggles: CustomTokenFeatureToggles? = null,
|
||||
) : StateType {
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue