Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-05 17:18:04 +06:00
commit 0f606f723c
31 changed files with 165 additions and 62 deletions

View file

@ -32,6 +32,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -174,6 +175,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var cardRepository: CardRepository
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
// endregion Injected
override fun onCreate() {
@ -261,6 +265,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
),
),
)

View file

@ -87,8 +87,6 @@ sealed class Onboarding(
class SetupFinished : Twins("Twin Setup Finished")
}
class ButtonChat : Onboarding("Onboarding", "Button - Chat")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",

View file

@ -0,0 +1,12 @@
package com.tangem.tap.common.feedback
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
internal class DefaultFeedbackManagerFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : FeedbackManagerFeatureToggles {
override val isLocalLogsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "LOCAL_USER_LOGS_ENABLED")
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.tap.common.feedback.DefaultFeedbackManagerFeatureToggles
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 FeedbackManagerFeatureTogglesModule {
@Provides
@Singleton
fun provideFeedbackManagerFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): FeedbackManagerFeatureToggles {
return DefaultFeedbackManagerFeatureToggles(featureTogglesManager)
}
}

View file

@ -32,6 +32,7 @@ internal object CardSDKLoggerModule {
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
)
return TangemCardSDKLogger(

View file

@ -8,11 +8,15 @@ internal class DelegatedKeystoreManager(
private val keystoreManagerProvider: Provider<KeystoreManager>,
) : KeystoreManager {
override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? {
return keystoreManagerProvider().authenticateAndGetKey(keyAlias)
override suspend fun get(keyAlias: String): SecretKey? {
return keystoreManagerProvider().get(keyAlias)
}
override suspend fun storeKey(keyAlias: String, key: SecretKey) {
keystoreManagerProvider().storeKey(keyAlias, key)
override suspend fun get(keyAliases: Collection<String>): Map<String, SecretKey> {
return keystoreManagerProvider().get(keyAliases)
}
override suspend fun store(keyAlias: String, key: SecretKey) {
return keystoreManagerProvider().store(keyAlias, key)
}
}

View file

@ -81,41 +81,38 @@ internal class BiometricUserWalletsKeysRepository(
}
private suspend fun getAllInternal(): CompletionResult<List<UserWalletEncryptionKey>> {
return getUserWalletsIds()
.map { userWalletId ->
getEncryptionKey(userWalletId)
.doOnFailure { error ->
when (error) {
is TangemSdkError.UserCanceledAuthentication -> {
// If the user cancels biometric authentication, then cancel operation with error
return CompletionResult.Failure(error)
}
is TangemSdkError.KeystoreInvalidated -> {
// If the biometric cryptography key was invalidated,
// then delete all user wallets encryption keys and cancel operation with error
getUserWalletsIds().forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
return catching {
val userWalletIds = getUserWalletsIds()
return CompletionResult.Failure(error)
}
getEncryptionKeys(userWalletIds)
}
.doOnFailure { error ->
when (error) {
is TangemSdkError.KeystoreInvalidated -> {
// If the biometric cryptography key was invalidated, then delete all encryption keys
getUserWalletsIds().forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
}
}
.fold(listOf()) { acc, data ->
if (data != null) acc + data else acc
else -> Unit
}
}
}
private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult<UserWalletEncryptionKey?> {
return catching { authenticatedStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) }
.map { it.decodeToKey() }
private suspend fun getEncryptionKeys(userWalletsIds: List<UserWalletId>): List<UserWalletEncryptionKey> {
val keys = userWalletsIds.map { userWalletId ->
StorageKey.UserWalletEncryptionKey(userWalletId).name
}
return authenticatedStorage.get(keys).mapNotNull { (_, encodedData) ->
encodedData.decodeToKey()
}
}
private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return catching {
authenticatedStorage.store(
key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}

View file

@ -5,7 +5,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
@ -21,7 +21,7 @@ class OnboardingMenuProvider : MenuProvider {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Onboarding.ButtonChat())
Analytics.send(Basic.ButtonSupport())
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
true

View file

@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
@ -501,6 +502,7 @@ class OnboardingWalletFragment :
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::legacyOnBackHandler,
onOpenChat = {
Analytics.send(Basic.ButtonSupport())
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
},

View file

@ -3,6 +3,8 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
@ -19,6 +21,7 @@ object WalletActivationErrorDialog {
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport())
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
}
setOnDismissListener { store.dispatchDialogHide() }

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.send.redux.SendAction
@ -19,6 +21,7 @@ object RequestFeeErrorDialog {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport())
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }

View file

@ -5,6 +5,8 @@ import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@ -31,6 +33,7 @@ object SendTransactionFailsDialog {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport())
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }

View file

@ -29,7 +29,7 @@ internal class DefaultTokensListRepository(
private val testnetTokensStorage: TestnetTokensStorage,
) : TokensListRepository {
override fun getAvailableTokens(searchText: String?): Flow<PagingData<Token>> {
override fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow<PagingData<Token>> {
return Pager(
config = PagingConfig(
pageSize = 100,
@ -42,6 +42,7 @@ internal class DefaultTokensListRepository(
dispatchers = dispatchers,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
searchText = searchText,
needFilterExcluded = needFilterExcluded,
)
getSelectedWalletSyncUseCase().fold(

View file

@ -27,8 +27,10 @@ internal class TangemApiTokensPagingSource(
private val dispatchers: CoroutineDispatcherProvider,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val searchText: String?,
needFilterExcluded: Boolean = false,
) : PagingSource<Int, Token>() {
private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded)
override fun getRefreshKey(state: PagingState<Int, Token>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
@ -41,7 +43,7 @@ internal class TangemApiTokensPagingSource(
return runCatching(dispatchers.io) {
val supportedBlockchains = getSelectedWalletSyncUseCase().fold(
ifLeft = { Blockchain.values().toList() },
ifLeft = { Blockchain.entries },
ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) },
)
@ -55,7 +57,7 @@ internal class TangemApiTokensPagingSource(
}.fold(
onSuccess = { response ->
LoadResult.Page(
data = CoinsResponseConverter.convert(response),
data = coinsResponseConverter.convert(response),
prevKey = if (page == 0) null else page.minus(other = 1),
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
)

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.tokens.impl.data.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.isSupportedInApp
import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.utils.converter.Converter
@ -11,9 +12,7 @@ import com.tangem.utils.converter.Converter
*
[REDACTED_AUTHOR]
*/
internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
private const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
internal class CoinsResponseConverter(val needFilterExcluded: Boolean) : Converter<CoinsResponse, List<Token>> {
override fun convert(value: CoinsResponse): List<Token> {
return value.coins.map { token ->
@ -25,6 +24,10 @@ internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
networks = token.networks.mapNotNull { network ->
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null
if (needFilterExcluded && !blockchain.isSupportedInApp()) {
return@mapNotNull null
}
// filter tokens, if contractAddress != null, assume that it is a token
if (network.contractAddress != null &&
!blockchain.canHandleTokens()
@ -47,4 +50,8 @@ internal object CoinsResponseConverter : Converter<CoinsResponse, List<Token>> {
fun getIconUrl(id: String, imageHost: String? = null): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}
private companion object {
const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
}
}

View file

@ -13,13 +13,14 @@ import com.tangem.utils.converter.Converter
*/
internal object TestnetTokensConfigConverter : Converter<TestnetTokensConfig, List<Token>> {
private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded = false)
override fun convert(value: TestnetTokensConfig): List<Token> {
return value.tokens.map { token ->
Token(
id = token.id,
name = token.name,
symbol = token.symbol,
iconUrl = CoinsResponseConverter.getIconUrl(token.id),
iconUrl = coinsResponseConverter.getIconUrl(token.id),
networks = token.networks?.mapNotNull { network ->
val blockchain = Blockchain.fromNetworkId(network.id) ?: return@mapNotNull null
@ -27,7 +28,7 @@ internal object TestnetTokensConfigConverter : Converter<TestnetTokensConfig, Li
id = network.id,
blockchain = blockchain,
address = network.address,
iconUrl = CoinsResponseConverter.getIconUrl(network.id),
iconUrl = coinsResponseConverter.getIconUrl(network.id),
decimalCount = network.decimalCount,
)
}.orEmpty(),

View file

@ -11,7 +11,10 @@ import kotlinx.coroutines.flow.Flow
*/
internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
override fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow<PagingData<Token>> {
return repository.getAvailableTokens(
searchText = searchText.ifBlank(defaultValue = { null }),
needFilterExcluded = needFilterExcluded,
)
}
}

View file

@ -12,5 +12,5 @@ import kotlinx.coroutines.flow.Flow
internal interface TokensListInteractor {
/** Get tokens list using filter by text [searchText] */
fun getTokensList(searchText: String): Flow<PagingData<Token>>
fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow<PagingData<Token>>
}

View file

@ -15,8 +15,9 @@ internal interface TokensListRepository {
* Get available tokens list
*
* @param searchText search text
* @param needFilterExcluded flag that determines if excluded tokens should be filtered
*
* @throws com.tangem.datasource.api.common.response.ApiResponseError
*/
fun getAvailableTokens(searchText: String?): Flow<PagingData<Token>>
fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow<PagingData<Token>>
}

View file

@ -250,7 +250,7 @@ internal class TokensListViewModel @Inject constructor(
}
private fun getTokensListBySearchText(text: String): Flow<PagingData<TokenItemState>> {
return interactor.getTokensList(searchText = text)
return interactor.getTokensList(searchText = text, needFilterExcluded = !isManageAccess)
.mapToTokenItemState()
.cachedIn(viewModelScope)
}

View file

@ -3,10 +3,12 @@ package com.tangem.tap.proxy
import androidx.core.text.isDigitsOnly
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
@ -138,6 +140,8 @@ class TransactionManagerImpl(
Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) }
Blockchain.Cosmos -> CosmosTransactionExtras(memo)
Blockchain.TON -> TonTransactionExtras(memo)
Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo)
Blockchain.Algorand -> AlgorandTransactionExtras(memo)
else -> null
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -65,4 +66,5 @@ data class DaggerGraphState(
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val cardRepository: CardRepository? = null,
val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null,
) : StateType