Updated on 2026-08-14
This commit is contained in:
commit
d4b0b11320
105 changed files with 2579 additions and 1197 deletions
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -17,6 +17,7 @@
|
|||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(project(":core:utils"))
|
||||
implementation(project(":libs:crypto"))
|
||||
implementation(project(":libs:auth"))
|
||||
implementation(project(":data:source:preferences"))
|
||||
|
||||
/** Features */
|
||||
implementation(project(":features:onboarding"))
|
||||
|
|
|
|||
|
|
@ -484,6 +484,17 @@
|
|||
"networkId": "ravencoin/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cosmos",
|
||||
"symbol": "ATOM",
|
||||
"name": "Cosmos Hub",
|
||||
"networks":
|
||||
[
|
||||
{
|
||||
"networkId": "cosmos/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.datasource.config.models.Config
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
import com.tangem.tap.common.analytics.AnalyticsFactory
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
|
|
@ -51,9 +52,8 @@ import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementa
|
|||
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 +66,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
|
||||
|
|
@ -134,6 +134,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var preferencesDataSource: PreferencesDataSource
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
@ -142,7 +145,14 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
appReducer(action, state, appStateHolder)
|
||||
},
|
||||
middleware = AppState.getMiddleware(),
|
||||
state = AppState(),
|
||||
state = AppState(
|
||||
daggerGraphState = DaggerGraphState(
|
||||
assetReader = assetReader,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
tokensListFeatureToggles = tokensListFeatureToggles,
|
||||
customTokenFeatureToggles = customTokenFeatureToggles,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
|
@ -154,7 +164,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 +188,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 +249,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
|
||||
|
|
@ -45,24 +46,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 +84,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 {
|
||||
|
|
@ -119,6 +154,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
)
|
||||
if (featureToggles.isRedesignedScreenEnabled) TokensListFragment() else AddTokensFragment()
|
||||
}
|
||||
|
||||
AppScreen.AddCustomToken -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::customTokenFeatureToggles,
|
||||
|
|
@ -129,6 +165,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AddCustomTokenFragment()
|
||||
}
|
||||
}
|
||||
|
||||
AppScreen.WalletDetails -> WalletDetailsFragment()
|
||||
AppScreen.WalletConnectSessions -> WalletConnectFragment()
|
||||
AppScreen.QrScan -> QrScanFragment()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -174,12 +174,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,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]
|
||||
|
|
|
|||
|
|
@ -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,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ 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.model.WalletDataModel
|
||||
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -316,21 +317,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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) { _, _ -> }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
39
app/src/main/res/drawable/ic_cosmos_no_color.xml
Normal file
39
app/src/main/res/drawable/ic_cosmos_no_color.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#EBEBEB"/>
|
||||
<path
|
||||
android:pathData="M11.173,1.882C11.085,1.801 11.04,1.8 11.033,1.8C11.025,1.8 10.98,1.801 10.893,1.882C10.803,1.964 10.696,2.106 10.58,2.326C10.35,2.765 10.129,3.428 9.939,4.283C9.725,5.245 9.556,6.421 9.449,7.738C9.964,7.987 10.494,8.258 11.033,8.549C11.571,8.258 12.101,7.987 12.616,7.737C12.51,6.421 12.341,5.245 12.127,4.283C11.936,3.428 11.716,2.765 11.485,2.326C11.37,2.106 11.262,1.964 11.173,1.882ZM10.582,8.795C10.186,8.585 9.796,8.386 9.415,8.2C9.386,8.623 9.363,9.06 9.348,9.508C9.549,9.387 9.754,9.266 9.962,9.147C10.169,9.027 10.376,8.91 10.582,8.795ZM9.034,7.54C9.343,3.93 10.121,1.369 11.033,1.369C11.944,1.369 12.723,3.93 13.032,7.54C16.314,6.002 18.923,5.396 19.378,6.185C19.834,6.973 18.004,8.928 15.031,11C18.004,13.072 19.834,15.027 19.378,15.816C18.923,16.604 16.314,15.998 13.032,14.46C12.723,18.07 11.944,20.631 11.033,20.631C10.121,20.631 9.343,18.07 9.034,14.46C5.751,15.998 3.143,16.604 2.687,15.816C2.231,15.027 4.061,13.072 7.034,11C4.061,8.928 2.231,6.973 2.687,6.185C3.143,5.396 5.751,6.002 9.034,7.54ZM7.414,10.739C6.326,9.989 5.392,9.254 4.665,8.588C4.019,7.996 3.555,7.473 3.29,7.054C3.157,6.845 3.087,6.681 3.061,6.562C3.035,6.446 3.056,6.406 3.06,6.4C3.063,6.394 3.087,6.355 3.2,6.32C3.317,6.283 3.494,6.262 3.742,6.272C4.238,6.291 4.923,6.432 5.759,6.695C6.7,6.991 7.803,7.433 8.997,7.998C8.956,8.569 8.926,9.163 8.908,9.774C8.387,10.095 7.887,10.418 7.414,10.739ZM7.414,11.261C6.326,12.011 5.392,12.746 4.665,13.412C4.019,14.004 3.555,14.527 3.29,14.946C3.157,15.156 3.087,15.319 3.061,15.438C3.035,15.554 3.056,15.594 3.06,15.6C3.063,15.606 3.087,15.645 3.2,15.68C3.317,15.717 3.494,15.738 3.742,15.728C4.238,15.709 4.923,15.568 5.759,15.305C6.7,15.009 7.803,14.567 8.997,14.002C8.956,13.431 8.926,12.837 8.908,12.226C8.387,11.905 7.887,11.582 7.414,11.261ZM8.897,11.712C8.517,11.475 8.15,11.237 7.797,11C8.15,10.763 8.517,10.525 8.897,10.288C8.893,10.523 8.891,10.76 8.891,11C8.891,11.24 8.893,11.477 8.897,11.712ZM9.333,11.981C9.326,11.658 9.322,11.331 9.322,11C9.322,10.669 9.326,10.342 9.333,10.019C9.608,9.852 9.89,9.685 10.177,9.519C10.464,9.354 10.75,9.193 11.033,9.039C11.316,9.193 11.601,9.354 11.888,9.519C12.175,9.685 12.457,9.852 12.733,10.019C12.74,10.342 12.744,10.669 12.744,11C12.744,11.331 12.74,11.658 12.733,11.981C12.457,12.148 12.175,12.315 11.888,12.481C11.601,12.646 11.316,12.807 11.033,12.962C10.75,12.807 10.464,12.646 10.177,12.481C9.89,12.315 9.608,12.148 9.333,11.981ZM9.348,12.492C9.363,12.94 9.386,13.377 9.415,13.8C9.796,13.614 10.186,13.415 10.582,13.205C10.376,13.09 10.169,12.973 9.962,12.854C9.754,12.734 9.549,12.613 9.348,12.492ZM11.033,13.451C10.494,13.742 9.964,14.013 9.449,14.262C9.556,15.579 9.725,16.755 9.939,17.717C10.129,18.572 10.35,19.235 10.58,19.674C10.696,19.894 10.803,20.036 10.893,20.118C10.98,20.199 11.025,20.201 11.033,20.201C11.04,20.201 11.085,20.199 11.173,20.118C11.262,20.036 11.37,19.894 11.485,19.674C11.716,19.235 11.936,18.572 12.127,17.717C12.341,16.755 12.51,15.579 12.616,14.263C12.101,14.013 11.571,13.742 11.033,13.451ZM13.068,14.002C14.262,14.568 15.366,15.009 16.306,15.305C17.142,15.568 17.827,15.709 18.323,15.728C18.571,15.738 18.748,15.717 18.865,15.68C18.978,15.645 19.002,15.606 19.006,15.6C19.009,15.594 19.031,15.554 19.005,15.438C18.978,15.319 18.908,15.156 18.775,14.946C18.511,14.527 18.046,14.004 17.4,13.412C16.674,12.746 15.739,12.011 14.651,11.261C14.178,11.582 13.679,11.905 13.157,12.226C13.14,12.837 13.11,13.431 13.068,14.002ZM14.651,10.739C15.739,9.989 16.674,9.254 17.4,8.588C18.046,7.996 18.511,7.473 18.775,7.054C18.908,6.845 18.978,6.681 19.005,6.562C19.031,6.446 19.009,6.406 19.006,6.4C19.002,6.394 18.978,6.355 18.865,6.32C18.748,6.283 18.571,6.262 18.323,6.272C17.827,6.291 17.142,6.432 16.306,6.695C15.366,6.991 14.262,7.433 13.068,7.998C13.11,8.569 13.14,9.163 13.157,9.774C13.679,10.095 14.178,10.418 14.651,10.739ZM13.168,10.288C13.549,10.525 13.916,10.763 14.268,11C13.916,11.237 13.549,11.475 13.168,11.712C13.172,11.477 13.174,11.24 13.174,11C13.174,10.76 13.172,10.523 13.168,10.288ZM12.718,9.508C12.702,9.06 12.68,8.623 12.651,8.199C12.269,8.386 11.879,8.585 11.483,8.795C11.689,8.91 11.896,9.027 12.103,9.147C12.311,9.266 12.516,9.387 12.718,9.508ZM12.651,13.801C12.269,13.614 11.879,13.415 11.483,13.205C11.689,13.09 11.896,12.973 12.103,12.854C12.311,12.734 12.516,12.613 12.718,12.492C12.702,12.94 12.68,13.377 12.651,13.801Z"
|
||||
android:strokeWidth="0.3"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#B0B0B0"/>
|
||||
<path
|
||||
android:pathData="M4.877,8.303C5.251,8.303 5.554,8.606 5.554,8.981C5.554,9.355 5.251,9.658 4.877,9.658C4.503,9.658 4.2,9.355 4.2,8.981C4.2,8.606 4.503,8.303 4.877,8.303Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#B0B0B0"/>
|
||||
<path
|
||||
android:pathData="M15.641,5.983C16.015,5.983 16.318,6.286 16.318,6.66C16.318,7.034 16.015,7.338 15.641,7.338C15.267,7.338 14.963,7.034 14.963,6.66C14.963,6.286 15.267,5.983 15.641,5.983Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#B0B0B0"/>
|
||||
<path
|
||||
android:pathData="M9.668,16.618C10.042,16.618 10.346,16.921 10.346,17.295C10.346,17.669 10.042,17.972 9.668,17.972C9.294,17.972 8.99,17.669 8.99,17.295C8.99,16.921 9.294,16.618 9.668,16.618Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#B0B0B0"/>
|
||||
<path
|
||||
android:pathData="M10.995,9.848C11.629,9.848 12.142,10.361 12.142,10.995C12.142,11.629 11.629,12.142 10.995,12.142C10.361,12.142 9.848,11.629 9.848,10.995C9.848,10.361 10.361,9.848 10.995,9.848Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#B0B0B0"/>
|
||||
</vector>
|
||||
|
|
@ -19,8 +19,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/text_primary_1"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
|
|
@ -67,8 +67,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
android:paddingStart="12dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
android:paddingStart="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/iv_currency"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
/**
|
||||
* Rectangle shimmer item with rounded shape from DS
|
||||
*/
|
||||
@Composable
|
||||
fun ShimmerRectangle(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shimmer()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius6),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Circle shimmer item
|
||||
* Size should be set in modifier
|
||||
*/
|
||||
@Composable
|
||||
fun CircleShimmer(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.shimmer()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Composable
|
||||
private fun ShimmersPreview() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
|
||||
) {
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size72,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Shimmers_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
ShimmersPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Shimmers_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
ShimmersPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion preview
|
||||
|
|
@ -36,6 +36,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"The-Open-Network", "The-Open-Network/test" -> R.drawable.img_ton_22
|
||||
"KAVA", "KAVA/test" -> R.drawable.img_kava_22
|
||||
"ravencoin", "ravencoin/test" -> R.drawable.img_ravencoin_22
|
||||
"cosmos", "cosmos/test" -> R.drawable.img_cosmos_22
|
||||
else -> R.drawable.ic_alert_24
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ data class TangemDimens internal constructor(
|
|||
val size50: Dp = 50.dp,
|
||||
val size56: Dp = 56.dp,
|
||||
val size62: Dp = 62.dp,
|
||||
val size68: Dp = 68.dp,
|
||||
val size72: Dp = 72.dp,
|
||||
val size80: Dp = 80.dp,
|
||||
val size84: Dp = 84.dp,
|
||||
|
|
@ -65,7 +66,9 @@ data class TangemDimens internal constructor(
|
|||
val size93: Dp = 93.dp,
|
||||
val size96: Dp = 96.dp,
|
||||
val size102: Dp = 102.dp,
|
||||
val size108: Dp = 108.dp,
|
||||
val size116: Dp = 116.dp,
|
||||
val size120: Dp = 120.dp,
|
||||
val size142: Dp = 142.dp,
|
||||
val size164: Dp = 164.dp,
|
||||
val size200: Dp = 200.dp,
|
||||
|
|
@ -95,5 +98,6 @@ data class TangemDimens internal constructor(
|
|||
val spacing54: Dp = 54.dp,
|
||||
val spacing56: Dp = 56.dp,
|
||||
val spacing92: Dp = 92.dp,
|
||||
val spacing154: Dp = 154.dp,
|
||||
// endregion Spacing
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ data class TangemShapes internal constructor(
|
|||
val roundedCornersSmall: Shape,
|
||||
val roundedCornersSmall2: Shape,
|
||||
val roundedCornersMedium: Shape,
|
||||
val roundedCornersXMedium: Shape,
|
||||
val roundedCornersLarge: Shape,
|
||||
val bottomSheet: Shape,
|
||||
) {
|
||||
|
|
@ -16,6 +17,7 @@ data class TangemShapes internal constructor(
|
|||
roundedCornersSmall = RoundedCornerShape(size = dimens.radius2),
|
||||
roundedCornersSmall2 = RoundedCornerShape(size = dimens.radius4),
|
||||
roundedCornersMedium = RoundedCornerShape(size = dimens.radius12),
|
||||
roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16),
|
||||
roundedCornersLarge = RoundedCornerShape(size = dimens.radius28),
|
||||
bottomSheet = RoundedCornerShape(
|
||||
topStart = dimens.radius16,
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_drag_24.xml
Normal file
10
core/ui/src/main/res/drawable/ic_drag_24.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,8H5V10H19V8ZM19,14H5V16H19V14Z"
|
||||
android:fillColor="#B0B0B0"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_eye_off_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_eye_off_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27ZM12,9C12.796,9 13.559,9.316 14.121,9.879C14.684,10.441 15,11.204 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9ZM12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5ZM3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/img_arrow_down_8.xml
Normal file
9
core/ui/src/main/res/drawable/img_arrow_down_8.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="9dp"
|
||||
android:height="8dp"
|
||||
android:viewportWidth="9"
|
||||
android:viewportHeight="8">
|
||||
<path
|
||||
android:pathData="M4.6114,6.8903C4.8128,7.1793 5.2419,7.175 5.4375,6.8821L8.9151,1.6731C9.1369,1.3409 8.8988,0.8955 8.4993,0.8955L1.3921,0.8955C0.9881,0.8955 0.7509,1.3499 0.9819,1.6814L4.6114,6.8903Z"
|
||||
android:fillColor="#FF3333"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/img_arrow_up_8.xml
Normal file
9
core/ui/src/main/res/drawable/img_arrow_up_8.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="9dp"
|
||||
android:height="8dp"
|
||||
android:viewportWidth="9"
|
||||
android:viewportHeight="8">
|
||||
<path
|
||||
android:pathData="M4.6114,1.1097C4.8128,0.8207 5.2419,0.825 5.4375,1.1179L8.9151,6.3269C9.1369,6.6591 8.8988,7.1045 8.4993,7.1045L1.3921,7.1045C0.9881,7.1045 0.7509,6.6501 0.9819,6.3186L4.6114,1.1097Z"
|
||||
android:fillColor="#1ACE80"/>
|
||||
</vector>
|
||||
39
core/ui/src/main/res/drawable/img_cosmos_22.xml
Normal file
39
core/ui/src/main/res/drawable/img_cosmos_22.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
|
||||
android:fillColor="#171717"/>
|
||||
<path
|
||||
android:pathData="M11.173,1.882C11.085,1.801 11.04,1.8 11.033,1.8C11.025,1.8 10.98,1.801 10.893,1.882C10.803,1.964 10.696,2.106 10.58,2.326C10.35,2.765 10.129,3.428 9.939,4.283C9.725,5.245 9.556,6.421 9.449,7.738C9.964,7.987 10.494,8.258 11.033,8.549C11.571,8.258 12.101,7.987 12.616,7.737C12.51,6.421 12.341,5.245 12.127,4.283C11.936,3.428 11.716,2.765 11.485,2.326C11.37,2.106 11.262,1.964 11.173,1.882ZM10.582,8.795C10.186,8.585 9.796,8.386 9.415,8.2C9.386,8.623 9.363,9.06 9.348,9.508C9.549,9.387 9.754,9.266 9.962,9.147C10.169,9.027 10.376,8.91 10.582,8.795ZM9.034,7.54C9.343,3.93 10.121,1.369 11.033,1.369C11.944,1.369 12.723,3.93 13.032,7.54C16.314,6.002 18.923,5.396 19.378,6.185C19.834,6.973 18.004,8.928 15.031,11C18.004,13.072 19.834,15.027 19.378,15.816C18.923,16.604 16.314,15.998 13.032,14.46C12.723,18.07 11.944,20.631 11.033,20.631C10.121,20.631 9.343,18.07 9.034,14.46C5.751,15.998 3.143,16.604 2.687,15.816C2.231,15.027 4.061,13.072 7.034,11C4.061,8.928 2.231,6.973 2.687,6.185C3.143,5.396 5.751,6.002 9.034,7.54ZM7.414,10.739C6.326,9.989 5.392,9.254 4.665,8.588C4.019,7.996 3.555,7.473 3.29,7.054C3.157,6.845 3.087,6.681 3.061,6.562C3.035,6.446 3.056,6.406 3.06,6.4C3.063,6.394 3.087,6.355 3.2,6.32C3.317,6.283 3.494,6.262 3.742,6.272C4.238,6.291 4.923,6.432 5.759,6.695C6.7,6.991 7.803,7.433 8.997,7.998C8.956,8.569 8.926,9.163 8.908,9.774C8.387,10.095 7.887,10.418 7.414,10.739ZM7.414,11.261C6.326,12.011 5.392,12.746 4.665,13.412C4.019,14.004 3.555,14.527 3.29,14.946C3.157,15.156 3.087,15.319 3.061,15.438C3.035,15.554 3.056,15.594 3.06,15.6C3.063,15.606 3.087,15.645 3.2,15.68C3.317,15.717 3.494,15.738 3.742,15.728C4.238,15.709 4.923,15.568 5.759,15.305C6.7,15.009 7.803,14.567 8.997,14.002C8.956,13.431 8.926,12.837 8.908,12.226C8.387,11.905 7.887,11.582 7.414,11.261ZM8.897,11.712C8.517,11.475 8.15,11.237 7.797,11C8.15,10.763 8.517,10.525 8.897,10.288C8.893,10.523 8.891,10.76 8.891,11C8.891,11.24 8.893,11.477 8.897,11.712ZM9.333,11.981C9.326,11.658 9.322,11.331 9.322,11C9.322,10.669 9.326,10.342 9.333,10.019C9.608,9.852 9.89,9.685 10.177,9.519C10.464,9.354 10.75,9.193 11.033,9.039C11.316,9.193 11.601,9.354 11.888,9.519C12.175,9.685 12.457,9.852 12.733,10.019C12.74,10.342 12.744,10.669 12.744,11C12.744,11.331 12.74,11.658 12.733,11.981C12.457,12.148 12.175,12.315 11.888,12.481C11.601,12.646 11.316,12.807 11.033,12.962C10.75,12.807 10.464,12.646 10.177,12.481C9.89,12.315 9.608,12.148 9.333,11.981ZM9.348,12.492C9.363,12.94 9.386,13.377 9.415,13.8C9.796,13.614 10.186,13.415 10.582,13.205C10.376,13.09 10.169,12.973 9.962,12.854C9.754,12.734 9.549,12.613 9.348,12.492ZM11.033,13.451C10.494,13.742 9.964,14.013 9.449,14.262C9.556,15.579 9.725,16.755 9.939,17.717C10.129,18.572 10.35,19.235 10.58,19.674C10.696,19.894 10.803,20.036 10.893,20.118C10.98,20.199 11.025,20.201 11.033,20.201C11.04,20.201 11.085,20.199 11.173,20.118C11.262,20.036 11.37,19.894 11.485,19.674C11.716,19.235 11.936,18.572 12.127,17.717C12.341,16.755 12.51,15.579 12.616,14.263C12.101,14.013 11.571,13.742 11.033,13.451ZM13.068,14.002C14.262,14.568 15.366,15.009 16.306,15.305C17.142,15.568 17.827,15.709 18.323,15.728C18.571,15.738 18.748,15.717 18.865,15.68C18.978,15.645 19.002,15.606 19.006,15.6C19.009,15.594 19.031,15.554 19.005,15.438C18.978,15.319 18.908,15.156 18.775,14.946C18.511,14.527 18.046,14.004 17.4,13.412C16.674,12.746 15.739,12.011 14.651,11.261C14.178,11.582 13.679,11.905 13.157,12.226C13.14,12.837 13.11,13.431 13.068,14.002ZM14.651,10.739C15.739,9.989 16.674,9.254 17.4,8.588C18.046,7.996 18.511,7.473 18.775,7.054C18.908,6.845 18.978,6.681 19.005,6.562C19.031,6.446 19.009,6.406 19.006,6.4C19.002,6.394 18.978,6.355 18.865,6.32C18.748,6.283 18.571,6.262 18.323,6.272C17.827,6.291 17.142,6.432 16.306,6.695C15.366,6.991 14.262,7.433 13.068,7.998C13.11,8.569 13.14,9.163 13.157,9.774C13.679,10.095 14.178,10.418 14.651,10.739ZM13.168,10.288C13.549,10.525 13.916,10.763 14.268,11C13.916,11.237 13.549,11.475 13.168,11.712C13.172,11.477 13.174,11.24 13.174,11C13.174,10.76 13.172,10.523 13.168,10.288ZM12.718,9.508C12.702,9.06 12.68,8.623 12.651,8.199C12.269,8.386 11.879,8.585 11.483,8.795C11.689,8.91 11.896,9.027 12.103,9.147C12.311,9.266 12.516,9.387 12.718,9.508ZM12.651,13.801C12.269,13.614 11.879,13.415 11.483,13.205C11.689,13.09 11.896,12.973 12.103,12.854C12.311,12.734 12.516,12.613 12.718,12.492C12.702,12.94 12.68,13.377 12.651,13.801Z"
|
||||
android:strokeAlpha="0.67"
|
||||
android:strokeWidth="0.3"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#ffffff"
|
||||
android:fillAlpha="0.67"/>
|
||||
<path
|
||||
android:pathData="M4.877,8.303C5.251,8.303 5.554,8.606 5.554,8.981C5.554,9.355 5.251,9.658 4.877,9.658C4.503,9.658 4.2,9.355 4.2,8.981C4.2,8.606 4.503,8.303 4.877,8.303Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M15.641,5.983C16.015,5.983 16.318,6.286 16.318,6.66C16.318,7.034 16.015,7.338 15.641,7.338C15.267,7.338 14.963,7.034 14.963,6.66C14.963,6.286 15.267,5.983 15.641,5.983Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M9.668,16.618C10.042,16.618 10.346,16.921 10.346,17.295C10.346,17.669 10.042,17.972 9.668,17.972C9.294,17.972 8.99,17.669 8.99,17.295C8.99,16.921 9.294,16.618 9.668,16.618Z"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M10.995,9.848C11.629,9.848 12.142,10.361 12.142,10.995C12.142,11.629 11.629,12.142 10.995,12.142C10.361,12.142 9.848,11.629 9.848,10.995C9.848,10.361 10.361,9.848 10.995,9.848Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
19
core/ui/src/main/res/drawable/img_loader_15.xml
Normal file
19
core/ui/src/main/res/drawable/img_loader_15.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="15dp"
|
||||
android:height="4dp"
|
||||
android:viewportWidth="15"
|
||||
android:viewportHeight="4">
|
||||
<path
|
||||
android:pathData="M13.5,3.5C12.672,3.5 12,2.828 12,2C12,1.172 12.672,0.5 13.5,0.5C14.328,0.5 15,1.172 15,2C15,2.828 14.328,3.5 13.5,3.5Z"
|
||||
android:fillColor="#FFB71B"/>
|
||||
<path
|
||||
android:pathData="M7.5,3.5C6.672,3.5 6,2.828 6,2C6,1.172 6.672,0.5 7.5,0.5C8.328,0.5 9,1.172 9,2C9,2.828 8.328,3.5 7.5,3.5Z"
|
||||
android:strokeAlpha="0.55"
|
||||
android:fillColor="#FFB71B"
|
||||
android:fillAlpha="0.55"/>
|
||||
<path
|
||||
android:pathData="M1.5,3.5C0.672,3.5 0,2.828 0,2C0,1.172 0.672,0.5 1.5,0.5C2.328,0.5 3,1.172 3,2C3,2.828 2.328,3.5 1.5,3.5Z"
|
||||
android:strokeAlpha="0.25"
|
||||
android:fillColor="#FFB71B"
|
||||
android:fillAlpha="0.25"/>
|
||||
</vector>
|
||||
1
data/source/preferences/.gitignore
vendored
Normal file
1
data/source/preferences/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
17
data/source/preferences/build.gradle.kts
Normal file
17
data/source/preferences/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.android.library)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
// For MoshiJsonConverter
|
||||
implementation(deps.tangem.card.core)
|
||||
}
|
||||
2
data/source/preferences/src/main/AndroidManifest.xml
Normal file
2
data/source/preferences/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.data.source.preferences" />
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.source.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("Create repository instead")
|
||||
class AppRatingLaunchObserver internal constructor(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun isReadyToShow(): Boolean {
|
||||
val fundsDate = fundsFoundDate ?: return false
|
||||
|
||||
if (!userWasInteractWithRating()) {
|
||||
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
|
||||
val diffInDays = diff / DAY_MILLIS
|
||||
return launchCounts >= getCounterOfNextShowing() && diffInDays >= firstShowing
|
||||
}
|
||||
|
||||
val nextShowing = getCounterOfNextShowing()
|
||||
return launchCounts >= nextShowing
|
||||
}
|
||||
|
||||
fun applyDelayedShowing() {
|
||||
updateNextShowing(launchCounts + deferShowing)
|
||||
}
|
||||
|
||||
fun setNeverToShow() {
|
||||
updateNextShowing(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
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
|
||||
private const val DAY_MILLIS = 1000 * 60 * 60 * 24
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
package com.tangem.tap.persistence
|
||||
package com.tangem.data.source.preferences
|
||||
|
||||
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.*
|
||||
import com.tangem.data.source.preferences.adapters.BigDecimalAdapter
|
||||
import com.tangem.data.source.preferences.adapters.CardBalanceStateAdapter
|
||||
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.FiatCurrenciesPrefStorage
|
||||
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
|
||||
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
|
||||
import javax.inject.Inject
|
||||
|
||||
class PreferencesStorage(applicationContext: Application) {
|
||||
// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories
|
||||
// TODO: Replace shared preferences with DataStore
|
||||
@Deprecated("Create repository instead")
|
||||
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
|
||||
|
||||
val appRatingLaunchObserver: AppRatingLaunchObserver
|
||||
val usedCardsPrefStorage: UsedCardsPrefStorage
|
||||
|
|
@ -73,8 +80,6 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
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) }
|
||||
}
|
||||
|
|
@ -83,6 +88,8 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
|
||||
}
|
||||
|
||||
private fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
|
||||
|
||||
private fun incrementLaunchCounter() {
|
||||
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
|
||||
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
|
||||
|
|
@ -100,72 +107,4 @@ class PreferencesStorage(applicationContext: Application) {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.source.preferences.adapters
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BigDecimalAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String) = BigDecimal(value)
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: BigDecimal) = value.toString()
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.data.source.preferences.adapters
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
|
||||
|
||||
class CardBalanceStateAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(src: DataSourceTopupInfo.CardBalanceState): String = src.serializedName
|
||||
|
||||
@FromJson
|
||||
fun fromJson(json: String): DataSourceTopupInfo.CardBalanceState {
|
||||
return when (json) {
|
||||
DataSourceTopupInfo.CardBalanceState.Empty.serializedName -> DataSourceTopupInfo.CardBalanceState.Empty
|
||||
DataSourceTopupInfo.CardBalanceState.Full.serializedName -> DataSourceTopupInfo.CardBalanceState.Full
|
||||
else -> error("CardBalanceState not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.source.preferences.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object PreferencesStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePreferencesStore(@ApplicationContext context: Context) = PreferencesDataSource(context)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceCurrency(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val name: String,
|
||||
val rateBTC: String,
|
||||
val unit: String,
|
||||
val type: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceFiatCurrency(
|
||||
val code: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
data class DataSourceTopupInfo(
|
||||
val walletId: String,
|
||||
val cardBalanceState: CardBalanceState,
|
||||
) {
|
||||
enum class CardBalanceState(val serializedName: String) {
|
||||
Empty(serializedName = "Empty"),
|
||||
Full(serializedName = "Full"),
|
||||
CustomToken(serializedName = "Custom token"),
|
||||
BlockchainError(serializedName = "Blockchain error"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
internal data class DataSourceUsedCardInfo(
|
||||
val cardId: String,
|
||||
val isScanned: Boolean = false,
|
||||
val isActivationStarted: Boolean = false,
|
||||
val isActivationFinished: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.data.source.preferences.model
|
||||
|
||||
internal data class DataSourceUsedCardInfoOld(
|
||||
val cardId: String,
|
||||
val isScanned: Boolean = false,
|
||||
val isActivationStarted: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.persistence
|
||||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
|
|
@ -6,7 +6,8 @@ import androidx.core.content.edit
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DisclaimerPrefStorage(
|
||||
@Deprecated("Create repository instead")
|
||||
class DisclaimerPrefStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
) {
|
||||
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.tangem.tap.persistence
|
||||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
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
|
||||
import com.tangem.data.source.preferences.model.DataSourceCurrency
|
||||
import com.tangem.data.source.preferences.model.DataSourceFiatCurrency
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FiatCurrenciesPrefStorage(
|
||||
@Deprecated("Create repository instead")
|
||||
class FiatCurrenciesPrefStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val converter: MoshiJsonConverter,
|
||||
) {
|
||||
|
|
@ -20,26 +21,26 @@ class FiatCurrenciesPrefStorage(
|
|||
}
|
||||
}
|
||||
|
||||
fun getAppCurrency(): FiatCurrency {
|
||||
fun getAppCurrency(): DataSourceFiatCurrency? {
|
||||
val json = preferences.getString(APP_CURRENCY_KEY, "")
|
||||
if (json.isNullOrBlank()) return FiatCurrency.Default
|
||||
if (json.isNullOrBlank()) return null
|
||||
|
||||
return converter.fromJson(json) ?: FiatCurrency.Default
|
||||
return converter.fromJson(json)
|
||||
}
|
||||
|
||||
fun saveAppCurrency(fiatCurrency: FiatCurrency) {
|
||||
fun saveAppCurrency(fiatCurrency: DataSourceFiatCurrency) {
|
||||
val json = converter.toJson(fiatCurrency)
|
||||
preferences.edit { putString(APP_CURRENCY_KEY, json) }
|
||||
}
|
||||
|
||||
fun save(currencies: List<CurrenciesResponse.Currency>) {
|
||||
fun save(currencies: List<DataSourceCurrency>) {
|
||||
val json: String = converter.toJson(currencies)
|
||||
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
|
||||
}
|
||||
|
||||
fun restore(): List<CurrenciesResponse.Currency> {
|
||||
fun restore(): List<DataSourceCurrency> {
|
||||
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
|
||||
val type = converter.typedList(CurrenciesResponse.Currency::class.java)
|
||||
val type = converter.typedList(DataSourceCurrency::class.java)
|
||||
if (json.isNullOrBlank()) return emptyList()
|
||||
|
||||
return converter.fromJson(json, type) ?: emptyList()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
internal interface Migration {
|
||||
fun migrate()
|
||||
}
|
||||
|
|
@ -1,51 +1,50 @@
|
|||
package com.tangem.tap.persistence
|
||||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
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
|
||||
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ToppedUpWalletStorage(
|
||||
@Deprecated("Create repository instead")
|
||||
class ToppedUpWalletStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) {
|
||||
|
||||
private val walletList: MutableSet<TopupInfo> = mutableSetOf()
|
||||
private val walletList: MutableSet<DataSourceTopupInfo> = mutableSetOf()
|
||||
|
||||
init {
|
||||
walletList.addAll(restore())
|
||||
}
|
||||
|
||||
fun save(userWalletInfo: TopupInfo): Boolean {
|
||||
fun save(userWalletInfo: DataSourceTopupInfo): Boolean {
|
||||
walletList.removeAll { it.walletId == userWalletInfo.walletId }
|
||||
walletList.add(userWalletInfo)
|
||||
return save(walletList)
|
||||
}
|
||||
|
||||
fun restore(walletId: String): TopupInfo? {
|
||||
fun restore(walletId: String): DataSourceTopupInfo? {
|
||||
return walletList.firstOrNull { it.walletId == walletId }
|
||||
}
|
||||
|
||||
private fun save(userWallets: MutableSet<TopupInfo>): Boolean {
|
||||
private fun save(userWallets: MutableSet<DataSourceTopupInfo>): 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> {
|
||||
private fun restore(): MutableSet<DataSourceTopupInfo> {
|
||||
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)!!
|
||||
val typedList = jsonConverter.typedList(DataSourceTopupInfo::class.java)
|
||||
val listData = jsonConverter.fromJson<List<DataSourceTopupInfo>>(json, typedList)!!
|
||||
listData.toMutableSet()
|
||||
} catch (ex: Exception) {
|
||||
preferences.edit(true) { remove(KEY) }
|
||||
|
|
@ -53,13 +52,6 @@ class ToppedUpWalletStorage(
|
|||
}
|
||||
}
|
||||
|
||||
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,15 +1,16 @@
|
|||
package com.tangem.tap.persistence
|
||||
package com.tangem.data.source.preferences.storage
|
||||
|
||||
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
|
||||
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfo
|
||||
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfoOld
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UsedCardsPrefStorage(
|
||||
@Deprecated("Create repository instead")
|
||||
class UsedCardsPrefStorage internal constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) {
|
||||
|
|
@ -26,7 +27,7 @@ class UsedCardsPrefStorage(
|
|||
fun scanned(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
|
||||
?: UsedCardInfo(cardId, true)
|
||||
?: DataSourceUsedCardInfo(cardId, true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
|
@ -38,14 +39,14 @@ class UsedCardsPrefStorage(
|
|||
fun activationStarted(cardId: String) {
|
||||
val restoredList = restore()
|
||||
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
|
||||
?: UsedCardInfo(cardId, isActivationStarted = true)
|
||||
?: DataSourceUsedCardInfo(cardId, isActivationStarted = true)
|
||||
|
||||
save(foundItem, restoredList)
|
||||
}
|
||||
|
||||
fun activationFinished(cardId: String) {
|
||||
val restoredList = restore()
|
||||
var foundItem = findCardInfo(cardId, restoredList) ?: UsedCardInfo(cardId)
|
||||
var foundItem = findCardInfo(cardId, restoredList) ?: DataSourceUsedCardInfo(cardId)
|
||||
foundItem = foundItem.copy(
|
||||
isActivationStarted = true,
|
||||
isActivationFinished = true,
|
||||
|
|
@ -67,33 +68,38 @@ class UsedCardsPrefStorage(
|
|||
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
|
||||
}
|
||||
|
||||
private fun findCardInfo(cardId: String, list: MutableList<UsedCardInfo>? = null): UsedCardInfo? {
|
||||
private fun findCardInfo(
|
||||
cardId: String,
|
||||
list: MutableList<DataSourceUsedCardInfo>? = null,
|
||||
): DataSourceUsedCardInfo? {
|
||||
val findInList = list ?: restore()
|
||||
return findInList.firstOrNull { it.cardId == cardId }
|
||||
}
|
||||
|
||||
private fun save(usedCardInfo: UsedCardInfo?, usedCardsInfo: MutableList<UsedCardInfo>) {
|
||||
private fun save(usedCardInfo: DataSourceUsedCardInfo?, usedCardsInfo: MutableList<DataSourceUsedCardInfo>) {
|
||||
val info = usedCardInfo ?: return
|
||||
|
||||
usedCardsInfo.replaceByOrAdd(info) { it.cardId == info.cardId }
|
||||
with(usedCardsInfo) {
|
||||
val index = indexOfFirst { it.cardId == info.cardId }
|
||||
if (index == -1) {
|
||||
add(info)
|
||||
} else {
|
||||
set(index, info)
|
||||
}
|
||||
}
|
||||
|
||||
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 save(list: MutableList<DataSourceUsedCardInfo>) {
|
||||
val json = jsonConverter.toJson(list)
|
||||
preferences.edit { putString(USED_CARDS_INFO_V2, json) }
|
||||
}
|
||||
|
||||
private fun restore(): MutableList<UsedCardInfo> {
|
||||
private fun restore(): MutableList<DataSourceUsedCardInfo> {
|
||||
val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf()
|
||||
return try {
|
||||
jsonConverter.fromJson(json, jsonConverter.typedList(UsedCardInfo::class.java))!!
|
||||
jsonConverter.fromJson(json, jsonConverter.typedList(DataSourceUsedCardInfo::class.java))!!
|
||||
} catch (ex: Exception) {
|
||||
preferences.edit(true) { remove(USED_CARDS_INFO_V2) }
|
||||
mutableListOf()
|
||||
|
|
@ -113,7 +119,7 @@ class UsedCardsPrefStorage(
|
|||
if (restoredCardsInfo.isEmpty()) return
|
||||
|
||||
val newCardsInfo = restoredCardsInfo.map { cardInfo ->
|
||||
UsedCardInfo(
|
||||
DataSourceUsedCardInfo(
|
||||
cardId = cardInfo.cardId,
|
||||
isScanned = cardInfo.isScanned,
|
||||
isActivationStarted = true,
|
||||
|
|
@ -123,10 +129,13 @@ class UsedCardsPrefStorage(
|
|||
storage.save(newCardsInfo)
|
||||
}
|
||||
|
||||
private fun restore(): MutableList<UsedCardInfoOld> {
|
||||
private fun restore(): MutableList<DataSourceUsedCardInfoOld> {
|
||||
val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
|
||||
return try {
|
||||
storage.jsonConverter.fromJson(json, storage.jsonConverter.typedList(UsedCardInfoOld::class.java))!!
|
||||
storage.jsonConverter.fromJson(
|
||||
json,
|
||||
storage.jsonConverter.typedList(DataSourceUsedCardInfoOld::class.java),
|
||||
)!!
|
||||
} catch (ex: Exception) {
|
||||
mutableListOf()
|
||||
} finally {
|
||||
|
|
@ -134,21 +143,4 @@ class UsedCardsPrefStorage(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Provides a temporary copies of the app module classes, data structures, etc.
|
||||
*/
|
||||
// TODO: refactoring: : after refactoring they should be unwrapped and moved
|
||||
// to appropriate parts of module
|
||||
@Deprecated("After refactoring they should be unwrapped and moved to appropriate parts of module")
|
||||
sealed interface DomainWrapped {
|
||||
|
||||
// Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency
|
||||
|
|
@ -30,10 +29,5 @@ sealed interface DomainWrapped {
|
|||
) : Currency {
|
||||
override val currencySymbol: String = blockchain.currency
|
||||
}
|
||||
|
||||
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
|
||||
if (derivationPath == null || derivationStyle == null) return false
|
||||
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
|
|||
"kava/test" -> Blockchain.KavaTestnet
|
||||
"ravencoin" -> Blockchain.Ravencoin
|
||||
"ravencoin/test" -> Blockchain.RavencoinTestnet
|
||||
"cosmos" -> Blockchain.Cosmos
|
||||
"cosmos/test" -> Blockchain.CosmosTestnet
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +118,8 @@ fun Blockchain.toNetworkId(): String {
|
|||
Blockchain.KavaTestnet -> "kava/test"
|
||||
Blockchain.Ravencoin -> "ravencoin"
|
||||
Blockchain.RavencoinTestnet -> "ravencoin/test"
|
||||
Blockchain.Cosmos -> "cosmos"
|
||||
Blockchain.CosmosTestnet -> "cosmos/test"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +158,7 @@ fun Blockchain.toCoinId(): String {
|
|||
Blockchain.Unknown -> "unknown"
|
||||
Blockchain.Kava, Blockchain.KavaTestnet -> "kava"
|
||||
Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin"
|
||||
Blockchain.Cosmos, Blockchain.CosmosTestnet -> "cosmos"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -251,11 +251,9 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
when (state.getCustomTokenType()) {
|
||||
CustomTokenType.Blockchain -> {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
if (alreadyAdded) {
|
||||
warningsAdd.add(TokenAlreadyAdded)
|
||||
} else {
|
||||
warningsRemove.add(TokenAlreadyAdded)
|
||||
}
|
||||
|
||||
if (alreadyAdded) warningsAdd.add(TokenAlreadyAdded) else warningsRemove.add(TokenAlreadyAdded)
|
||||
|
||||
if (state.derivationPathIsSelected()) {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
} else {
|
||||
|
|
@ -266,12 +264,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
if (tokenIsSupported) {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
} else {
|
||||
val error = ContractAddress.validateValue(ContractAddress.getFieldValue())
|
||||
when (error) {
|
||||
AddCustomTokenError.FieldIsEmpty -> warningsRemove.add(UnsupportedSolanaToken)
|
||||
else -> {
|
||||
warningsAdd.add(UnsupportedSolanaToken)
|
||||
}
|
||||
val validationResult = ContractAddress.validateValue(ContractAddress.getFieldValue())
|
||||
if (validationResult == AddCustomTokenError.FieldIsEmpty) {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
} else {
|
||||
warningsAdd.add(UnsupportedSolanaToken)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,12 +326,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
// blockchain
|
||||
else -> {
|
||||
if (state.networkIsSelected()) {
|
||||
val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList()
|
||||
if (alreadyAdded) {
|
||||
disableAddButton()
|
||||
} else {
|
||||
enableAddButton()
|
||||
}
|
||||
if (isBlockchainPersistIntoAppSavedTokensList()) disableAddButton() else enableAddButton()
|
||||
} else {
|
||||
disableAddButton()
|
||||
}
|
||||
|
|
@ -369,18 +361,18 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList()
|
||||
}
|
||||
|
||||
private fun isTokenPersistIntoAppSavedTokensList(
|
||||
tokenId: String? = hubState.foundToken?.id,
|
||||
tokenContractAddress: String = ContractAddress.getFieldValue(),
|
||||
tokenNetworkId: String = Network.getFieldValue<Blockchain>().toNetworkId(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
|
||||
): Boolean {
|
||||
private fun isTokenPersistIntoAppSavedTokensList(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
|
||||
val tokenId = hubState.foundToken?.id
|
||||
val tokenContractAddress = ContractAddress.getFieldValue<String>()
|
||||
val tokenNetworkId = Network.getFieldValue<Blockchain>().toNetworkId()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {}
|
||||
is DomainWrapped.Currency.Blockchain -> Unit
|
||||
is DomainWrapped.Currency.Token -> {
|
||||
val sameId = tokenId == wrappedCurrency.token.id
|
||||
val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress
|
||||
|
|
@ -396,14 +388,12 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
return false
|
||||
}
|
||||
|
||||
private fun isBlockchainPersistIntoAppSavedTokensList(
|
||||
selectedNetwork: Blockchain = Network.getFieldValue(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue(),
|
||||
): Boolean {
|
||||
val state = hubState
|
||||
val savedCurrencies = state.appSavedCurrencies ?: return false
|
||||
|
||||
private fun isBlockchainPersistIntoAppSavedTokensList(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
val selectedNetwork = Network.getFieldValue<Blockchain>()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {
|
||||
|
|
@ -411,7 +401,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
|
||||
if (isSameBlockchain && isSameDerivationPath) return true
|
||||
}
|
||||
is DomainWrapped.Currency.Token -> {}
|
||||
|
||||
is DomainWrapped.Currency.Token -> Unit
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
1
features/wallet/api/.gitignore
vendored
Normal file
1
features/wallet/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
5
features/wallet/api/build.gradle.kts
Normal file
5
features/wallet/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
2
features/wallet/api/src/main/AndroidManifest.xml
Normal file
2
features/wallet/api/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.features.wallet.api" />
|
||||
1
features/wallet/impl/.gitignore
vendored
Normal file
1
features/wallet/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
36
features/wallet/impl/build.gradle.kts
Normal file
36
features/wallet/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(project(":core:featuretoggles"))
|
||||
implementation(project(":core:ui"))
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(project(":features:wallet:api"))
|
||||
|
||||
}
|
||||
4
features/wallet/impl/src/main/AndroidManifest.xml
Normal file
4
features/wallet/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.tangem.feature.wallet.impl">
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
package com.tangem.feature.wallet.presentation.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Surface
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemTypography
|
||||
import com.tangem.feature.wallet.presentation.ui.config.PriceChange
|
||||
import com.tangem.feature.wallet.presentation.ui.config.PriceChangeType
|
||||
import com.tangem.feature.wallet.presentation.ui.config.TokenV2Config
|
||||
import com.tangem.feature.wallet.presentation.ui.state.TokenOptionsUIState
|
||||
import com.tangem.feature.wallet.presentation.ui.state.TokenUIState
|
||||
|
||||
private const val DOTS = "•••"
|
||||
|
||||
@Composable
|
||||
fun TokenItemV2(config: TokenV2Config) {
|
||||
when (config.tokenUIState) {
|
||||
TokenUIState.LOADED -> LoadedTokenState(config)
|
||||
TokenUIState.LOADING -> LoadingTokenState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadedTokenState(config: TokenV2Config) {
|
||||
BaseSurface {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
) {
|
||||
val (iconItem, tokenNameItem, fiatItem) = createRefs()
|
||||
TokenIcon(
|
||||
modifier = Modifier.constrainAs(iconItem) {
|
||||
centerVerticallyTo(parent)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
tokenIconUrl = config.iconUrl,
|
||||
icon = config.icon,
|
||||
networkIconRes = config.networkIcon,
|
||||
)
|
||||
TokenTitleAmountBlock(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.constrainAs(tokenNameItem) {
|
||||
centerVerticallyTo(parent)
|
||||
start.linkTo(iconItem.end)
|
||||
end.linkTo(fiatItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
title = config.name,
|
||||
amount = config.amount,
|
||||
hasPending = config.hasPending,
|
||||
)
|
||||
TokenOptionsBlock(
|
||||
config = config,
|
||||
modifier = Modifier.constrainAs(fiatItem) {
|
||||
centerVerticallyTo(parent)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block for end part of token item
|
||||
* shows status is reachable, is drag, hidden or show balance
|
||||
*/
|
||||
@Composable
|
||||
private fun TokenOptionsBlock(config: TokenV2Config, modifier: Modifier = Modifier) {
|
||||
when (config.tokenOptionsUIState) {
|
||||
TokenOptionsUIState.VISIBLE -> TokenFiatPercentageBlock(
|
||||
modifier = modifier,
|
||||
fiatAmount = config.fiatAmount,
|
||||
priceChange = config.priceChange,
|
||||
)
|
||||
TokenOptionsUIState.UNREACHABLE -> Text(
|
||||
modifier = modifier,
|
||||
text = "Unreachable", // TODO (conform this text)
|
||||
style = TangemTypography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
TokenOptionsUIState.HIDDEN -> TokenFiatPercentageBlock(
|
||||
modifier = modifier,
|
||||
fiatAmount = DOTS,
|
||||
priceChange = config.priceChange,
|
||||
)
|
||||
TokenOptionsUIState.DRAG -> Icon(
|
||||
modifier = modifier,
|
||||
painter = painterResource(id = R.drawable.ic_drag_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingTokenState() {
|
||||
BaseSurface {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42))
|
||||
SpacerW12()
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size72,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size50,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size40,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size40,
|
||||
height = TangemTheme.dimens.size12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun BaseSurface(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
|
||||
Surface(
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size68),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitleAmountBlock(title: String, amount: String, hasPending: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTypography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (hasPending) {
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
painter = painterResource(id = R.drawable.img_loader_15),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = amount,
|
||||
style = TangemTypography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenFiatPercentageBlock(fiatAmount: String, priceChange: PriceChange, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.requiredWidth(IntrinsicSize.Max)) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
text = fiatAmount,
|
||||
style = TangemTypography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH2()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
val iconChangeArrow: Int
|
||||
val changeTextColor: Color
|
||||
if (priceChange.type == PriceChangeType.UP) {
|
||||
iconChangeArrow = R.drawable.img_arrow_up_8
|
||||
changeTextColor = TangemTheme.colors.text.accent
|
||||
} else {
|
||||
iconChangeArrow = R.drawable.img_arrow_down_8
|
||||
changeTextColor = TangemTheme.colors.text.warning
|
||||
}
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
painter = painterResource(id = iconChangeArrow),
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = priceChange.valuePercent,
|
||||
style = TangemTypography.body2,
|
||||
color = changeTextColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenIcon(
|
||||
tokenIconUrl: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@DrawableRes networkIconRes: Int? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(end = TangemTheme.dimens.spacing16)
|
||||
.size(TangemTheme.dimens.size42),
|
||||
) {
|
||||
val tokenImageModifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.size(TangemTheme.dimens.size36)
|
||||
|
||||
val data = if (tokenIconUrl.isNullOrEmpty()) {
|
||||
icon
|
||||
} else {
|
||||
tokenIconUrl
|
||||
}
|
||||
SubcomposeAsyncImage(
|
||||
modifier = tokenImageModifier,
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(data)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = { CircleShimmer(modifier = tokenImageModifier) },
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
if (networkIconRes != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(TangemTheme.dimens.size18)
|
||||
.background(color = Color.White, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.padding(all = 0.5.dp),
|
||||
painter = painterResource(id = networkIconRes),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Composable
|
||||
private fun TokensPreview() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
|
||||
) {
|
||||
val config = TokenV2Config(
|
||||
name = "Polygon",
|
||||
amount = "5,412 MATIC",
|
||||
fiatAmount = "321 $",
|
||||
priceChange = PriceChange(
|
||||
valuePercent = "2%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
iconUrl = null,
|
||||
icon = R.drawable.img_polygon_22,
|
||||
networkIcon = R.drawable.img_polygon_22,
|
||||
tokenUIState = TokenUIState.LOADED,
|
||||
tokenOptionsUIState = TokenOptionsUIState.VISIBLE,
|
||||
hasPending = true,
|
||||
)
|
||||
// Loaded item
|
||||
TokenItemV2(config)
|
||||
// Unreachable item
|
||||
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.UNREACHABLE))
|
||||
// Drag item
|
||||
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.DRAG))
|
||||
// Hidden item
|
||||
TokenItemV2(config.copy(tokenOptionsUIState = TokenOptionsUIState.UNREACHABLE))
|
||||
// Loading item
|
||||
TokenItemV2(
|
||||
config.copy(
|
||||
tokenUIState = TokenUIState.LOADING,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_Tokens_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
TokensPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_Tokens_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
TokensPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion preview
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package com.tangem.feature.wallet.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.ui.config.WalletHeaderConfig
|
||||
import com.tangem.feature.wallet.presentation.ui.state.BalanceUIState
|
||||
|
||||
private const val DOTS = "•••"
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun WalletHeaderCard(config: WalletHeaderConfig, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier.defaultMinSize(minHeight = TangemTheme.dimens.size108),
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
onClick = config.onClick ?: {},
|
||||
enabled = config.onClick != null,
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val (balanceBlock, imageItem) = createRefs()
|
||||
Column(
|
||||
modifier = Modifier.constrainAs(balanceBlock) {
|
||||
centerVerticallyTo(parent)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(imageItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
HeaderWalletName(
|
||||
walletName = config.walletName,
|
||||
balanceState = config.balanceState,
|
||||
)
|
||||
HeaderBalanceTitle(
|
||||
balance = config.balance,
|
||||
balanceState = config.balanceState,
|
||||
)
|
||||
Text(
|
||||
text = config.additionalInfo,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
val imageWidth = TangemTheme.dimens.size120
|
||||
Image(
|
||||
modifier = Modifier.constrainAs(imageItem) {
|
||||
centerVerticallyTo(parent)
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
height = Dimension.fillToConstraints
|
||||
width = Dimension.value(imageWidth)
|
||||
},
|
||||
painter = config.cardImage,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeaderWalletName(walletName: String, balanceState: BalanceUIState) {
|
||||
if (balanceState == BalanceUIState.HIDDEN) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
Text(
|
||||
text = walletName,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
painter = painterResource(id = R.drawable.ic_eye_off_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = walletName,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeaderBalanceTitle(balance: String, balanceState: BalanceUIState) {
|
||||
when (balanceState) {
|
||||
BalanceUIState.VISIBLE -> {
|
||||
ResizableText(
|
||||
text = balance,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
|
||||
)
|
||||
}
|
||||
BalanceUIState.LOADING -> {
|
||||
ShimmerRectangle(
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.size102,
|
||||
height = TangemTheme.dimens.size24,
|
||||
),
|
||||
)
|
||||
}
|
||||
BalanceUIState.HIDDEN -> {
|
||||
Text(
|
||||
text = DOTS,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
BalanceUIState.NONE -> {
|
||||
Text(
|
||||
text = "—",
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Composable
|
||||
private fun WarningsPreview() {
|
||||
val walletHeaderConfig = WalletHeaderConfig(
|
||||
walletName = "Wallet 1",
|
||||
balance = "8923,05 $",
|
||||
additionalInfo = "3 cards • Seed enabled",
|
||||
balanceState = BalanceUIState.VISIBLE,
|
||||
cardImage = painterResource(id = R.drawable.ill_businessman_3d),
|
||||
onClick = {},
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
WalletHeaderCard(
|
||||
config = walletHeaderConfig,
|
||||
)
|
||||
SpacerH32()
|
||||
WalletHeaderCard(
|
||||
config = walletHeaderConfig.copy(balanceState = BalanceUIState.HIDDEN),
|
||||
)
|
||||
SpacerH32()
|
||||
WalletHeaderCard(
|
||||
config = walletHeaderConfig.copy(balanceState = BalanceUIState.LOADING),
|
||||
)
|
||||
SpacerH32()
|
||||
WalletHeaderCard(
|
||||
config = walletHeaderConfig.copy(balanceState = BalanceUIState.NONE),
|
||||
)
|
||||
SpacerH32()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_Warning_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
WarningsPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_Warning_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
WarningsPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.feature.wallet.presentation.ui.config
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.feature.wallet.presentation.ui.state.TokenOptionsUIState
|
||||
import com.tangem.feature.wallet.presentation.ui.state.TokenUIState
|
||||
|
||||
/**
|
||||
* Token config
|
||||
*
|
||||
* @property name of token/coin
|
||||
* @property amount of token
|
||||
* @property fiatAmount amount in fiat
|
||||
* @property priceChange value of price changing
|
||||
* @property iconUrl
|
||||
* @property icon token/coin icon
|
||||
* @property networkIcon
|
||||
* @property tokenUIState token state [TokenUIState] loading etc.
|
||||
* @property tokenOptionsUIState state for token options like 'unreachable', 'hidden' etc
|
||||
* @property hasPending pending tx in blockchain
|
||||
*/
|
||||
data class TokenV2Config(
|
||||
val name: String,
|
||||
val amount: String,
|
||||
val fiatAmount: String,
|
||||
val priceChange: PriceChange,
|
||||
val iconUrl: String?,
|
||||
@DrawableRes val icon: Int?,
|
||||
@DrawableRes val networkIcon: Int,
|
||||
val tokenUIState: TokenUIState,
|
||||
val tokenOptionsUIState: TokenOptionsUIState,
|
||||
val hasPending: Boolean,
|
||||
)
|
||||
|
||||
data class PriceChange(
|
||||
val valuePercent: String,
|
||||
val type: PriceChangeType,
|
||||
)
|
||||
|
||||
enum class PriceChangeType {
|
||||
UP, DOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.ui.config
|
||||
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import com.tangem.feature.wallet.presentation.ui.state.BalanceUIState
|
||||
|
||||
data class WalletHeaderConfig(
|
||||
val walletName: String,
|
||||
val balance: String,
|
||||
val additionalInfo: String,
|
||||
val balanceState: BalanceUIState,
|
||||
val cardImage: Painter,
|
||||
val onClick: (() -> Unit)?,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue