Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-05 15:47:51 +03:00
commit 6a342ab06b
16 changed files with 217 additions and 78 deletions

View file

@ -10,6 +10,7 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -159,6 +160,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var testerFeatureToggles: TesterFeatureToggles
@Inject
lateinit var blockchainWalletManagerFactory: WalletManagerFactory
// endregion Injected
override fun onCreate() {
@ -236,6 +240,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
sendFeatureToggles = sendFeatureToggles,
derivationsRepository = derivationsRepository,
testerFeatureToggles = testerFeatureToggles,
blockchainWalletManagerFactory = blockchainWalletManagerFactory,
),
),
)

View file

@ -1,8 +1,8 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
@ -24,7 +24,7 @@ internal object WalletManagersFacadeModule {
fun provideWalletManagersFacade(
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainWalletManagerFactory: WalletManagerFactory,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
@ -32,7 +32,7 @@ internal object WalletManagersFacadeModule {
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
blockchainWalletManagerFactory = blockchainWalletManagerFactory,
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),

View file

@ -1,23 +1,23 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -30,18 +30,15 @@ class TapWalletManager(
private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(),
) {
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private var loadUserWalletDataJob: Job? = null
set(value) {
field?.cancel()
field = value
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val walletManagerFactory: WalletManagerFactory by lazy {
store.state.daggerGraphState.get(DaggerGraphState::blockchainWalletManagerFactory)
}
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.proxy.redux
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -57,6 +58,7 @@ data class DaggerGraphState(
val currenciesRepository: CurrenciesRepository? = null,
val derivationsRepository: DerivationsRepository? = null,
val testerFeatureToggles: TesterFeatureToggles? = null,
val blockchainWalletManagerFactory: WalletManagerFactory? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -0,0 +1,28 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object BlockchainWalletManagerFactoryModule {
@Provides
@Singleton
fun provideBlockchainWalletManagerFactory(
configManager: ConfigManager,
appPreferencesStore: AppPreferencesStore,
): WalletManagerFactory {
return WalletManagerFactory(
config = configManager.config.blockchainSdkConfig,
blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore),
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.blockchain
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
/**
* [BlockchainDataStorage] implementation
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainDataStorage(
private val appPreferencesStore: AppPreferencesStore,
) : BlockchainDataStorage {
override suspend fun getOrNull(key: String): String? {
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
}
override suspend fun store(key: String, value: String) {
appPreferencesStore.edit {
it[stringPreferencesKey(key)] = value
}
}
}

View file

@ -19,7 +19,6 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.hasDerivation
@ -39,6 +38,7 @@ import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import java.math.BigDecimal
import java.util.EnumSet
import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory
@Suppress("LargeClass", "TooManyFunctions")
// FIXME: Move to its own module and make internal
@ -46,7 +46,7 @@ import java.util.EnumSet
class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainWalletManagerFactory: BlockchainWalletManagerFactory,
mnemonic: Mnemonic,
assetReader: AssetReader,
moshi: Moshi,
@ -54,7 +54,7 @@ class DefaultWalletManagersFacade(
private val demoConfig by lazy { DemoConfig() }
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
private val walletManagerFactory by lazy { WalletManagerFactory(configManager) }
private val walletManagerFactory by lazy { WalletManagerFactory(blockchainWalletManagerFactory) }
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }

View file

@ -3,23 +3,18 @@ package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.makeWalletManagerForApp
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import timber.log.Timber
import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory
internal class WalletManagerFactory(
private val configManager: ConfigManager,
private val sdkWalletManagerFactory: BlockchainWalletManagerFactory,
) {
private val sdkWalletManagerFactory by lazy {
WalletManagerFactory(configManager.config.blockchainSdkConfig)
}
fun createWalletManager(
scanResponse: ScanResponse,
blockchain: Blockchain,

View file

@ -15,12 +15,16 @@ internal class StateRouter(
fragmentManager.get()?.popBackStack()
}
fun onBackClick() {
when (currentState.value) {
SendUiStateType.Recipient -> popBackStack()
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Send -> showFee()
fun onBackClick(isSuccess: Boolean = false) {
if (isSuccess) {
popBackStack()
} else {
when (currentState.value) {
SendUiStateType.Recipient -> popBackStack()
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Fee -> showAmount()
SendUiStateType.Send -> showFee()
}
}
}
@ -54,7 +58,7 @@ internal class StateRouter(
currentState.update { SendUiStateType.Fee }
}
private fun showSend() {
fun showSend() {
currentState.update { SendUiStateType.Send }
}
}

View file

@ -70,7 +70,7 @@ internal class FeeNotificationFactory(
val customAmount = customFee.firstOrNull() ?: return
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) {
add(SendFeeNotification.Informational.TooLow)
add(SendFeeNotification.Warning.TooLow)
}
}

View file

@ -8,22 +8,6 @@ import com.tangem.features.send.impl.R
sealed class SendFeeNotification(val config: NotificationConfig) {
sealed class Informational(
val title: TextReference,
val subtitle: TextReference,
) : SendFeeNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_circle_24,
),
) {
object TooLow : Informational(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
}
sealed class Warning(
val title: TextReference,
val subtitle: TextReference,
@ -36,6 +20,11 @@ sealed class SendFeeNotification(val config: NotificationConfig) {
buttonsState = buttonsState,
),
) {
object TooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
data class TooHigh(
val value: String,
) : Warning(

View file

@ -15,14 +15,15 @@ import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
if (state == null) return
@ -36,46 +37,92 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
horizontal = TangemTheme.dimens.spacing16,
),
) {
item(
key = FEE_SELECTOR_KEY,
) {
SendSpeedSelector(
state = state,
clickIntents = clickIntents,
modifier = Modifier.animateItemPlacement(),
)
}
feeSelector(state, clickIntents)
topNotifications(notifications)
customFee(feeSendState)
notifications(notifications)
subtractButton(
receivedAmount = state.receivedAmount,
isSubtract = state.isSubtract,
isSubtractAvailable = state.isSubtractAvailable,
clickIntents = clickIntents,
)
middleNotifications(notifications)
subtractButton(state, clickIntents)
bottomNotifications(notifications)
}
}
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotification>, modifier: Modifier = Modifier) {
private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: SendClickIntents) {
item(
key = FEE_SELECTOR_KEY,
) {
SendSpeedSelector(
state = state,
clickIntents = clickIntents,
modifier = Modifier.animateItemPlacement(),
)
}
}
private fun LazyListScope.topNotifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
) {
notifications(
configs = configs.filter {
it is SendFeeNotification.Error.ExceedsBalance ||
it is SendFeeNotification.Warning.NetworkFeeUnreachable
}.toImmutableList(),
modifier = modifier,
)
}
private fun LazyListScope.middleNotifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
) {
notifications(
configs = configs.filter {
it is SendFeeNotification.Warning.TooLow ||
it is SendFeeNotification.Warning.TooHigh
}.toImmutableList(),
modifier = modifier,
)
}
private fun LazyListScope.bottomNotifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
) {
notifications(
configs = configs.filterIsInstance<SendFeeNotification.Warning.NetworkCoverage>().toImmutableList(),
isLast = true,
modifier = modifier,
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.notifications(
configs: ImmutableList<SendFeeNotification>,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
items(
items = configs,
key = { it::class.java },
contentType = { it::class.java },
itemContent = {
val bottomPadding = if (isLast) TangemTheme.dimens.spacing12 else TangemTheme.dimens.spacing0
Notification(
config = it.config,
modifier = modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(
top = TangemTheme.dimens.spacing12,
bottom = bottomPadding,
)
.animateItemPlacement(),
containerColor = when (it) {
is SendFeeNotification.Error.ExceedsBalance,
is SendFeeNotification.Warning.NetworkFeeUnreachable,
-> TangemTheme.colors.background.primary
-> TangemTheme.colors.background.action
else -> TangemTheme.colors.button.disabled
},
iconTint = when (it) {
is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent
is SendFeeNotification.Error.ExceedsBalance -> {
if (it.config.buttonsState == null) {
TangemTheme.colors.icon.warning
@ -116,20 +163,30 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.subtractButton(
receivedAmount: String,
isSubtract: Boolean,
isSubtractAvailable: Boolean,
state: SendStates.FeeState,
clickIntents: SendClickIntents,
modifier: Modifier = Modifier,
) {
val receivedAmount = state.receivedAmount
val isSubtract = state.isSubtract
val isSubtractAvailable = state.isSubtractAvailable
val feeSendState = state.feeSelectorState
if (isSubtractAvailable) {
item {
val feeStateContent = feeSendState as? FeeSelectorState.Content
val isCustomAvailable = feeStateContent?.customValues.isNullOrEmpty().not()
val isCustomSelected = feeStateContent?.selectedFee == FeeType.CUSTOM
val topPadding = if (isCustomSelected && isCustomAvailable) {
TangemTheme.dimens.spacing12
} else {
TangemTheme.dimens.spacing20
}
SendSpeedSubtract(
receivingAmount = receivedAmount,
isSubtract = isSubtract,
onSelectClick = clickIntents::onSubtractSelect,
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing12)
.padding(top = topPadding)
.animateItemPlacement(),
)
}

View file

@ -4,6 +4,9 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -39,6 +42,7 @@ import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import java.math.BigDecimal
@ -112,12 +116,17 @@ internal fun SendSpeedSelector(
visible = fees.normal is Fee.Ethereum,
label = "Custom fee appearance animation",
) {
val showWarning = state.notifications.any {
it is SendFeeNotification.Warning.TooHigh ||
it is SendFeeNotification.Warning.TooLow
}
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_custom,
iconRes = R.drawable.ic_edit_24,
isSelected = isSelected == FeeType.CUSTOM,
onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) },
showDivider = fees.normal !is Fee.Ethereum,
showWarning = showWarning,
)
}
}
@ -224,6 +233,7 @@ private fun SendSpeedSelectorItem(
symbolLength: Int? = null,
isSelected: Boolean = false,
showDivider: Boolean = true,
showWarning: Boolean = false,
) {
val iconTint by animateColorAsState(
targetValue = if (isSelected) {
@ -259,7 +269,10 @@ private fun SendSpeedSelectorItem(
symbolLength = symbolLength,
textStyle = textStyle,
)
} else {
SpacerWMax()
}
WarningIcon(showWarning = showWarning)
}
if (showDivider) {
Box(
@ -340,6 +353,26 @@ private fun RowScope.SelectorValueContent(
)
}
@Composable
private fun WarningIcon(showWarning: Boolean = false) {
AnimatedVisibility(
visible = showWarning,
label = "Custom fee warning indicator",
enter = fadeIn(),
exit = fadeOut(),
) {
Image(
painter = painterResource(R.drawable.ic_alert_triangle_20),
contentDescription = null,
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing12,
horizontal = TangemTheme.dimens.spacing14,
),
)
}
}
//region preview
@Preview
@Composable

View file

@ -67,16 +67,16 @@ internal fun SendContent(uiState: SendUiState) {
walletBalance = amountState.walletBalance.resolveReference(),
)
}
AmountBlock(
amountState = amountState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showAmount,
)
RecipientBlock(
recipientState = recipientState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showRecipient,
)
AmountBlock(
amountState = amountState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showAmount,
)
FeeBlock(
feeState = feeState,
isSuccess = isSuccess,

View file

@ -352,7 +352,7 @@ internal class SendViewModel @Inject constructor(
// region screen state navigation
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick()
override fun onBackClick() = stateRouter.onBackClick(uiState.sendState.isSuccess)
override fun onNextClick() = stateRouter.onNextClick()
override fun onPrevClick() = stateRouter.onPrevClick()

View file

@ -85,9 +85,9 @@ spr-client = "3.6.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.6-472"
tangemBlockchainSdk = "develop-475"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.6-325"
tangemCardSdk = "develop-324"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem