Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-12 17:05:23 +03:00
commit 13183498ba
17 changed files with 94 additions and 60 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlinx.coroutines.suspendCancellableCoroutine
@ -23,12 +24,12 @@ class TangemSigner(
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
return suspendCancellableCoroutine { continuation ->
val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val task = SignHashesTask(hashes, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = task,
cardId = cardId,
cardId = card.cardId.takeIf { isCardNotBackedUp },
initialMessage = initialMessage,
accessCode = accessCode,
) { result ->

View file

@ -14,6 +14,7 @@ import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
@ -36,7 +37,10 @@ class TapWalletManager {
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
// do nothing if its the same wallet
if (store.state.globalState.scanResponse == userWallet.scanResponse) return
store.state.globalState.scanResponse?.let {
val userWalletId = UserWalletIdBuilder.scanResponse(it).build()
if (userWalletId == userWallet.walletId) return
}
Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {

View file

@ -17,6 +17,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.ByteArrayKeyAdapter
import com.tangem.tap.domain.userWalletList.utils.json.CardBackupStatusAdapter
import com.tangem.tap.domain.userWalletList.utils.json.DerivationPathAdapterWithMigration
import com.tangem.tap.domain.userWalletList.utils.json.ExtendedPublicKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.ScanResponseDerivedKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.WalletDerivedKeysMapAdapter
@ -33,8 +34,8 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())

View file

@ -54,16 +54,6 @@ internal class BiometricUserWalletsListManager(
get() = keysRepository.hasSavedEncryptionKeys()
override suspend fun unlock(): CompletionResult<UserWallet> {
// Occurs when user has locked user wallets after unlocking with biometry
// e.g. after biometric storage master key invalidation
if (state.value.hasLockedUserWalletsAfterUnlock) {
return CompletionResult.Failure(
error = UserWalletsListError.UnableToUnlockUserWallets(
cause = IllegalStateException("Permanently locked"),
),
)
}
return unlockWithBiometryInternal()
.mapFailure { error ->
if (error is UserWalletsListError) {
@ -216,10 +206,7 @@ internal class BiometricUserWalletsListManager(
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
prevState.copy(
isLocked = hasLockedUserWallets,
hasLockedUserWalletsAfterUnlock = hasLockedUserWallets,
)
prevState.copy(isLocked = hasLockedUserWallets)
}
}
}
@ -262,15 +249,6 @@ internal class BiometricUserWalletsListManager(
}
}
}
.doOnFailure { error ->
if (error is UserWalletsListError.EncryptionKeyInvalidated) {
state.update { prevState ->
prevState.copy(
hasLockedUserWalletsAfterUnlock = true,
)
}
}
}
}
private suspend fun getSavedUserWallets(): CompletionResult<List<UserWallet>> {
@ -334,6 +312,5 @@ internal class BiometricUserWalletsListManager(
val userWallets: List<UserWallet> = emptyList(),
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = true,
val hasLockedUserWalletsAfterUnlock: Boolean = false,
)
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.domain.userWalletList.utils.json
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import com.tangem.common.extensions.guard
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.crypto.hdWallet.DerivationNode
import com.tangem.crypto.hdWallet.DerivationPath
import timber.log.Timber
class DerivationPathAdapterWithMigration {
@ToJson
fun toJson(src: DerivationPath): String = src.rawPath
@FromJson
fun fromJson(json: String): DerivationPath {
val jsonMap = MoshiJsonConverter.default().toMap(json)
return if (jsonMap.isEmpty()) {
DerivationPath(json)
} else {
fromLegacyScheme(jsonMap)
}
}
@Suppress("UNCHECKED_CAST")
private fun fromLegacyScheme(jsonMap: Map<String, Any>): DerivationPath {
val rawPath = jsonMap["rawPath"] as String
val nodeIndexes = (jsonMap["nodes"] as? List<Number>).guard {
Timber.e("Unable to convert derivation path nodes from JSON")
return DerivationPath(rawPath)
}
val nodes = nodeIndexes.map { DerivationNode.fromIndex(it.toLong()) }
return DerivationPath(rawPath, nodes)
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
@ -31,14 +32,6 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -84,6 +77,8 @@ class DetailsMiddleware {
allowsRequestAccessCodeFromRepository = true,
)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)

View file

@ -160,7 +160,10 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
@Composable
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
LazyRow(modifier = Modifier.padding(start = 8.dp, end = 8.dp)) {
LazyRow(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
items(links) {
Icon(
painter = painterResource(id = it.network.iconRes),

View file

@ -55,25 +55,28 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github)
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube)
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin)
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
}
object TangemSocialAccounts {
val accountsEn: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/TangemCards"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
)
val accountsRu: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_ru"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.util.cardTypesResolver
@ -16,6 +17,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
import kotlin.properties.ReadOnlyProperty
data class WalletState(
@ -103,6 +105,9 @@ data class WalletState(
it.derivationPath?.rawPath == currency.derivationPath
}
}
fun getBlockchainAmount(currency: Currency): BigDecimal =
getWalletManager(currency)?.wallet?.amounts?.get(AmountType.Coin)?.value ?: BigDecimal.ZERO
}
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
@ -114,7 +119,6 @@ enum class ErrorType {
sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class SendButton(enabled: Boolean) : WalletMainButton(enabled)
class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled)
}
data class Artwork(

View file

@ -55,6 +55,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
/**
@ -128,7 +129,8 @@ class WalletDetailsFragment :
(WalletState::selectedWalletData or WalletState::isExchangeServiceFeatureOn) { state ->
val selectedWallet = state.selectedWalletData
if (selectedWallet != null) {
setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn)
val blockchainAmount: BigDecimal = state.getBlockchainAmount(selectedWallet.currency)
setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn, blockchainAmount)
}
}
(WalletState::state or WalletState::error) { state ->
@ -281,7 +283,11 @@ class WalletDetailsFragment :
}
}
private fun setupButtonsRow(selectedWallet: WalletDataModel, isExchangeServiceFeatureOn: Boolean) {
private fun setupButtonsRow(
selectedWallet: WalletDataModel,
isExchangeServiceFeatureOn: Boolean,
blockchainAmount: BigDecimal,
) {
val exchangeManager = store.state.globalState.exchangeManager
binding.rowButtons.apply {
onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
@ -310,7 +316,7 @@ class WalletDetailsFragment :
binding.rowButtons.updateButtonsVisibility(
actions = actions,
exchangeServiceFeatureOn = isExchangeServiceFeatureOn,
sendAllowed = selectedWallet.mainButton.enabled,
sendAllowed = selectedWallet.mainButton(blockchainAmount).enabled,
)
}

View file

@ -239,7 +239,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry,
) { store.dispatch(WalletAction.LoadData) }
)
// because was added logic of autoupdate mainscreen data, remove retry
// TODO("remove comment after release 4.6")
// { store.dispatch(WalletAction.LoadData) }
} else {
isNetworkConnectionError.value = false
}

View file

@ -18,10 +18,9 @@ import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import java.math.BigDecimal
internal val WalletDataModel.mainButton: WalletMainButton
get() = WalletMainButton.SendButton(
enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(),
)
internal fun WalletDataModel.mainButton(blockchainAmount: BigDecimal): WalletMainButton = WalletMainButton.SendButton(
enabled = !isEmptyAmount && status.pendingTransactions.isEmpty() && !blockchainAmount.isZero(),
)
internal fun WalletDataModel.getFormattedAmount(): String {
return status.amount.toFormattedCurrencyString(

View file

@ -12,7 +12,6 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
@ -175,15 +174,10 @@ class SingleWalletView : WalletView() {
binding?.rowButtons?.updateButtonsVisibility(
actions = actions,
exchangeServiceFeatureOn = isExchangeServiceFeatureEnabled,
sendAllowed = walletData.mainButton.enabled,
sendAllowed = walletData.mainButton(walletData.status.amount).enabled,
)
rowButtons.onSendClick = {
when (walletData.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send())
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
rowButtons.onSendClick = { store.dispatch(WalletAction.Send()) }
}
private fun setupAddressCard(state: WalletState, binding: FragmentWalletBinding) = with(binding.lAddress) {

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="18dp"
android:viewportWidth="22"
android:viewportHeight="18">
<path
android:pathData="M18.636,1.924C17.212,1.259 15.689,0.775 14.097,0.5C13.902,0.853 13.673,1.327 13.516,1.705C11.824,1.451 10.147,1.451 8.486,1.705C8.328,1.327 8.095,0.853 7.897,0.5C6.304,0.775 4.779,1.26 3.355,1.927C0.483,6.26 -0.296,10.486 0.093,14.651C1.999,16.071 3.845,16.934 5.66,17.498C6.108,16.882 6.508,16.228 6.852,15.538C6.196,15.29 5.568,14.983 4.975,14.626C5.132,14.51 5.286,14.388 5.435,14.263C9.055,15.953 12.988,15.953 16.565,14.263C16.715,14.388 16.869,14.51 17.025,14.626C16.43,14.984 15.8,15.291 15.144,15.54C15.488,16.228 15.887,16.884 16.336,17.5C18.153,16.935 20.001,16.073 21.906,14.651C22.363,9.822 21.126,5.636 18.636,1.924ZM7.345,12.089C6.259,12.089 5.368,11.076 5.368,9.843C5.368,8.61 6.24,7.596 7.345,7.596C8.451,7.596 9.342,8.608 9.323,9.843C9.325,11.076 8.451,12.089 7.345,12.089ZM14.655,12.089C13.568,12.089 12.677,11.076 12.677,9.843C12.677,8.61 13.549,7.596 14.655,7.596C15.76,7.596 16.651,8.608 16.632,9.843C16.632,11.076 15.76,12.089 14.655,12.089Z"
android:fillColor="#B0B0B0"/>
</vector>

View file

@ -9,6 +9,6 @@
},
{
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
"version": "4.5.0"
"version": "undefined"
}
]

View file

@ -45,7 +45,7 @@ object SaltPayWorkaround {
) + attachTestWalletCardIds()
val walletCardIdRanges = listOf(
CardIdRange("AC05000000000003", "AC05000000023997")!!,
CardIdRange("AC05000000000003", "AC05000000015993")!!,
) + attachTestWalletCardIdRanges()
fun tokenFrom(blockchain: Blockchain): Token {

View file

@ -74,7 +74,7 @@ arrow = "1.2.0-RC"
# region Tangem
tangemBlockchainSdk = "develop-210"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-229"
tangemCardSdk = "develop-235"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem