Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-12 16:53:47 +03:00
commit d4b0b11320
105 changed files with 2579 additions and 1197 deletions

View file

@ -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" />

View file

@ -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"))

View file

@ -484,6 +484,17 @@
"networkId": "ravencoin/test"
}
]
},
{
"id": "cosmos",
"symbol": "ATOM",
"name": "Cosmos Hub",
"networks":
[
{
"networkId": "cosmos/test"
}
]
}
]
}

View file

@ -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>,
) {

View file

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

View file

@ -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>,
) {

View file

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

View file

@ -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)

View file

@ -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()

View file

@ -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,
),
)
}

View file

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

View file

@ -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,
),

View file

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

View file

@ -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,

View file

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

View file

@ -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)

View file

@ -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
*

View file

@ -0,0 +1,4 @@
package com.tangem.tap.features.customtoken.impl.presentation.models
/** Custom token type */
enum class CustomTokenType { TOKEN, BLOCKCHAIN }

View file

@ -9,4 +9,7 @@ internal interface CustomTokenRouter {
/** Return to last screen */
fun popBackStack()
/** Open wallet (main) screen */
fun openWalletScreen()
}

View file

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

View file

@ -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,

View file

@ -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())
}
}

View file

@ -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 = {}),
)
}
}

View file

@ -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(),
),
)

View file

@ -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())
}
}

View file

@ -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 = {}),
),
)

View file

@ -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))
},
),
)

View file

@ -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())
}
}

View file

@ -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,
)
}
},
)
}
}

View file

@ -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,
)

View file

@ -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]

View file

@ -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]

View file

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

View file

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

View file

@ -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() {

View file

@ -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(

View file

@ -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))

View file

@ -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,

View file

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

View file

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

View file

@ -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) { _, _ -> }

View file

@ -57,4 +57,6 @@ internal sealed interface WalletSelectorAction : Action {
) : WalletSelectorAction
object CloseError : WalletSelectorAction
object ClearUserWallets : WalletSelectorAction
}

View file

@ -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,

View file

@ -59,6 +59,7 @@ internal object WalletSelectorReducer {
is WalletSelectorAction.SelectWallet,
is WalletSelectorAction.RemoveWallets,
is WalletSelectorAction.RenameWallet,
is WalletSelectorAction.ClearUserWallets,
-> state
}
}

View file

@ -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() }

View file

@ -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(

View file

@ -18,4 +18,6 @@ internal sealed interface WelcomeAction : Action {
data class HandleIntentIfNeeded(val intent: Intent?) : WelcomeAction
object CloseError : WelcomeAction
object ClearUserWallets : WelcomeAction
}

View file

@ -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 {

View file

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

View file

@ -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,

View file

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

View file

@ -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")
}
}
}

View file

@ -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 -> {

View file

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

View file

@ -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,
)

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

View file

@ -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"

View file

@ -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"

View file

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

View file

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

View file

@ -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
)

View file

@ -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,

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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android: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>

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

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

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

View 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
View file

@ -0,0 +1 @@
/build

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

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.data.source.preferences" />

View file

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

View file

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

View file

@ -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()
}

View file

@ -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")
}
}
}

View file

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

View file

@ -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,
)

View file

@ -0,0 +1,7 @@
package com.tangem.data.source.preferences.model
data class DataSourceFiatCurrency(
val code: String,
val name: String,
val symbol: String,
)

View file

@ -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"),
}
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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,
) {

View file

@ -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()

View file

@ -0,0 +1,5 @@
package com.tangem.data.source.preferences.storage
internal interface Migration {
fun migrate()
}

View file

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

View file

@ -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()
}

View file

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

View file

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

View file

@ -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
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,5 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}

View 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
View file

@ -0,0 +1 @@
/build

View 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"))
}

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

View file

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

View file

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

View file

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

View file

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