Updated on 2026-08-14
This commit is contained in:
commit
5c71b53ea6
57 changed files with 586 additions and 432 deletions
|
|
@ -3,11 +3,11 @@ package com.tangem.tap.common.analytics.converters
|
|||
import com.tangem.common.Converter
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
|
|
@ -29,7 +29,9 @@ class BasicSignInEventConverter(
|
|||
currency = cardCurrency,
|
||||
batch = scanResponse.card.batchId,
|
||||
).apply {
|
||||
filterData = scanResponse.card.userWalletId.stringValue
|
||||
filterData = UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
?.stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +45,7 @@ class BasicTopUpEventConverter(
|
|||
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
|
||||
|
||||
val data = BasicTopUpFilter.Data(
|
||||
walletId = scanResponse.card.userWalletId.stringValue,
|
||||
walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue ?: "",
|
||||
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import com.tangem.blockchain.common.WalletManager
|
|||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
|
||||
class AdditionalFeedbackInfo {
|
||||
class EmailWalletInfo(
|
||||
|
|
@ -54,7 +54,7 @@ class AdditionalFeedbackInfo {
|
|||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = formatSignedHashes(data.card.wallets)
|
||||
userWalletId = data.card.userWalletId.stringValue
|
||||
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
|
|
|
|||
|
|
@ -57,11 +57,8 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
|
|||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
Settings.ACTION_BIOMETRIC_ENROLL
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
Settings.ACTION_FINGERPRINT_ENROLL
|
||||
}
|
||||
else -> {
|
||||
Settings.ACTION_SETTINGS
|
||||
Settings.ACTION_SECURITY_SETTINGS
|
||||
}
|
||||
}
|
||||
val intent = Intent(settingsAction).apply {
|
||||
|
|
|
|||
|
|
@ -234,6 +234,11 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
}
|
||||
|
||||
fun useBiometricsForAccessCode(): Boolean {
|
||||
val policy = tangemSdk.config.userCodeRequestPolicy
|
||||
return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode
|
||||
}
|
||||
|
||||
companion object {
|
||||
val config = Config(
|
||||
linkedTerminal = true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.domain.model
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
|
||||
|
|
@ -33,23 +32,4 @@ data class UserWallet(
|
|||
|
||||
val isLocked: Boolean
|
||||
get() = scanResponse.card.wallets.isEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Workaround !!!
|
||||
*
|
||||
* Calculate same [UserWalletId] for twins instead
|
||||
*
|
||||
* TODO: Remove after [REDACTED_JIRA]
|
||||
* */
|
||||
fun UserWallet.isTwinnedWith(other: UserWallet): Boolean {
|
||||
if (!scanResponse.isTangemTwins() || !other.scanResponse.isTangemTwins()) return false
|
||||
if (scanResponse.secondTwinPublicKey == null || other.scanResponse.secondTwinPublicKey == null) return false
|
||||
if (other.scanResponse.secondTwinPublicKey == scanResponse.card.wallets.firstOrNull()?.publicKey?.toHexString()) {
|
||||
return true
|
||||
}
|
||||
if (scanResponse.secondTwinPublicKey == other.scanResponse.card.wallets.firstOrNull()?.publicKey?.toHexString()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.domain.model.builders
|
||||
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.CardDTO
|
||||
|
|
@ -8,7 +10,6 @@ import com.tangem.domain.common.ScanResponse
|
|||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.operations.attestation.TangemApi
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
|
|
@ -40,7 +41,16 @@ class UserWalletBuilder(
|
|||
ProductType.Note -> false
|
||||
ProductType.Twins -> false
|
||||
ProductType.SaltPay -> false
|
||||
ProductType.Wallet -> !card.isStart2Coin
|
||||
ProductType.Wallet -> when {
|
||||
card.isStart2Coin -> false
|
||||
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable -> true
|
||||
else -> {
|
||||
val cardWallets = card.wallets
|
||||
require(cardWallets.isNotEmpty()) { "Card wallets must not be empty" }
|
||||
|
||||
cardWallets.first().curve == EllipticCurve.Secp256k1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun backupCardsIds(backupCardsIds: Set<String>?) = this.apply {
|
||||
|
|
@ -49,16 +59,20 @@ class UserWalletBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun build(): UserWallet {
|
||||
suspend fun build(): UserWallet? {
|
||||
return with(scanResponse) {
|
||||
UserWallet(
|
||||
walletId = card.userWalletId,
|
||||
name = userWalletName,
|
||||
artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
|
||||
cardsInWallet = backupCardsIds.plus(card.cardId),
|
||||
scanResponse = this,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
)
|
||||
UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
?.let {
|
||||
UserWallet(
|
||||
walletId = it,
|
||||
name = userWalletName,
|
||||
artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
|
||||
cardsInWallet = backupCardsIds.plus(card.cardId),
|
||||
scanResponse = this,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.tap.domain.model.builders
|
||||
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.crypto.Secp256k1
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
|
||||
class UserWalletIdBuilder private constructor(
|
||||
private val publicKey: ByteArray?,
|
||||
private val pairTwinPublicKey: ByteArray? = null,
|
||||
) {
|
||||
fun build(): UserWalletId? {
|
||||
val seed = if (publicKey != null) {
|
||||
if (pairTwinPublicKey != null) {
|
||||
Secp256k1.sum(publicKey, pairTwinPublicKey)
|
||||
} else {
|
||||
publicKey
|
||||
}
|
||||
} else null
|
||||
|
||||
return seed?.let {
|
||||
UserWalletId(value = calculateUserWalletId(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateUserWalletId(seed: ByteArray?): ByteArray? {
|
||||
val message = MESSAGE_FOR_WALLET_ID.toByteArray()
|
||||
val keyHash = seed?.calculateSha256()
|
||||
|
||||
return if (keyHash != null) {
|
||||
message.calculateHmacSha256(keyHash)
|
||||
} else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun card(card: CardDTO): UserWalletIdBuilder {
|
||||
require(!card.isTangemTwins) {
|
||||
"For twin cards use scanResponse to ID calculation"
|
||||
}
|
||||
|
||||
return UserWalletIdBuilder(findPublicKey(card.wallets))
|
||||
}
|
||||
|
||||
fun scanResponse(scanResponse: ScanResponse): UserWalletIdBuilder {
|
||||
return UserWalletIdBuilder(
|
||||
publicKey = findPublicKey(scanResponse.card.wallets),
|
||||
pairTwinPublicKey = when (scanResponse.productType) {
|
||||
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
|
||||
ProductType.Note,
|
||||
ProductType.Wallet,
|
||||
ProductType.SaltPay,
|
||||
-> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
|
||||
return wallets.firstOrNull()
|
||||
?.publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechService
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.AndroidFileReader
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.domain.tokens.converters.CurrencyConverter
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
|
|
@ -29,7 +29,7 @@ class UserTokensRepository(
|
|||
|
||||
// TODO("After adding DI") replace with CoroutineDispatcherProvider
|
||||
suspend fun getUserTokens(card: CardDTO): List<Currency> = withContext(dispatchers.io) {
|
||||
val userId = card.userWalletId.stringValue
|
||||
val userId = getUserWalletId(card) ?: return@withContext emptyList()
|
||||
if (DemoHelper.isDemoCardId(card.cardId)) {
|
||||
return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies)
|
||||
}
|
||||
|
|
@ -55,17 +55,15 @@ class UserTokensRepository(
|
|||
|
||||
// TODO("After adding DI") replace with CoroutineDispatcherProvider
|
||||
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) {
|
||||
val userId = card.userWalletId.stringValue
|
||||
val userId = getUserWalletId(card) ?: return@withContext
|
||||
val userTokens = tokens.toUserTokensResponse()
|
||||
tangemTechApi.saveUserTokens(userId, userTokens)
|
||||
storageService.saveUserTokens(userId, userTokens)
|
||||
}
|
||||
|
||||
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) {
|
||||
val blockchainNetworks = loadTokensOffline(
|
||||
card = card,
|
||||
userId = card.userWalletId.stringValue,
|
||||
).toBlockchainNetworks()
|
||||
val userId = getUserWalletId(card) ?: return@withContext emptyList()
|
||||
val blockchainNetworks = loadTokensOffline(card = card, userId = userId).toBlockchainNetworks()
|
||||
|
||||
if (DemoHelper.isDemoCardId(card.cardId)) {
|
||||
return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks)
|
||||
|
|
@ -109,6 +107,11 @@ class UserTokensRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getUserWalletId(card: CardDTO): String? {
|
||||
return UserWalletIdBuilder.card(card).build()
|
||||
?.stringValue
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val GROUP_DEFAULT_VALUE = "none"
|
||||
private const val SORT_DEFAULT_VALUE = "manual"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ interface UserWalletsListManager {
|
|||
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
/**
|
||||
* Save user wallet
|
||||
* Save provided user wallet and set it as selected
|
||||
* @param userWallet [UserWallet] to save
|
||||
* @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an
|
||||
* already saved card
|
||||
|
|
@ -27,6 +27,17 @@ interface UserWalletsListManager {
|
|||
* */
|
||||
suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Same as [save] but not change selected user wallet ID
|
||||
* and not terminate with [UserWalletListError.WalletAlreadySaved] if [UserWallet] already saved
|
||||
*
|
||||
* Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]
|
||||
* @param userWalletId update [UserWallet] with that [UserWalletId]
|
||||
* @param update lambda that receives stored [UserWallet] and returns updated [UserWallet]
|
||||
* @return [CompletionResult] of operation with updated [UserWallet]
|
||||
* */
|
||||
suspend fun update(userWalletId: UserWalletId, update: (UserWallet) -> UserWallet): CompletionResult<UserWallet>
|
||||
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.domain.userWalletList.implementation
|
|||
import com.tangem.common.*
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.isTwinnedWith
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
|
|
@ -11,6 +10,7 @@ import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletReposit
|
|||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -51,7 +51,7 @@ internal class BiometricUserWalletsListManager(
|
|||
get() = state.value.isLocked
|
||||
|
||||
override val hasSavedUserWallets: Boolean
|
||||
get() = publicInformationRepository.isNotEmpty()
|
||||
get() = keysRepository.hasSavedEncryptionKeys()
|
||||
|
||||
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
|
||||
return unlockWithBiometryInternal()
|
||||
|
|
@ -85,22 +85,35 @@ internal class BiometricUserWalletsListManager(
|
|||
|
||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
||||
return if (canOverride) {
|
||||
saveInternal(userWallet)
|
||||
saveInternal(userWallet, changeSelectedUserWallet = true)
|
||||
} else {
|
||||
val isWalletSaved = state.value.userWallets
|
||||
.any {
|
||||
// Workaround, check [UserWallet.isTwinnedWith]
|
||||
it.cardsInWallet.contains(userWallet.cardId) || it.isTwinnedWith(userWallet)
|
||||
it.walletId == userWallet.walletId || it.cardsInWallet.contains(userWallet.cardId)
|
||||
}
|
||||
|
||||
if (isWalletSaved) {
|
||||
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
|
||||
} else {
|
||||
saveInternal(userWallet)
|
||||
saveInternal(userWallet, changeSelectedUserWallet = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
update: (UserWallet) -> UserWallet,
|
||||
): CompletionResult<UserWallet> {
|
||||
return get(userWalletId)
|
||||
.map { storedUserWallet ->
|
||||
update(storedUserWallet)
|
||||
}
|
||||
.flatMap { updatedUserWallet ->
|
||||
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
|
||||
.map { updatedUserWallet }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
if (userWalletIds.isEmpty()) {
|
||||
return CompletionResult.Success(Unit)
|
||||
|
|
@ -113,9 +126,12 @@ internal class BiometricUserWalletsListManager(
|
|||
.flatMap { keysRepository.delete(userWalletIds) }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
val newUserWallets = prevState.userWallets.filter { it.walletId !in userWalletIds }
|
||||
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in userWalletIds },
|
||||
userWallets = prevState.userWallets.filter { it.walletId !in userWalletIds },
|
||||
userWallets = newUserWallets,
|
||||
isLocked = newUserWallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -137,27 +153,23 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
val newEncryptionKeys = state.value.encryptionKeys
|
||||
.plus(UserWalletEncryptionKey(userWallet))
|
||||
.distinctBy { it.walletId }
|
||||
|
||||
return keysRepository.store(newEncryptionKeys)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = newEncryptionKeys,
|
||||
selectedUserWalletId = userWallet.walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
private suspend fun saveInternal(
|
||||
userWallet: UserWallet,
|
||||
changeSelectedUserWallet: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return saveEncryptionKeyIfNotNull(userWallet)
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
|
||||
.flatMap { publicInformationRepository.save(userWallet) }
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet) }
|
||||
.map { selectedUserWalletRepository.set(userWallet.walletId) }
|
||||
.flatMap { loadModels() }
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedUserWalletId = if (changeSelectedUserWallet) {
|
||||
userWallet.walletId
|
||||
} else {
|
||||
prevState.selectedUserWalletId
|
||||
},
|
||||
isLocked = prevState.userWallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
|
|
@ -183,6 +195,27 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
|
||||
val encryptionKey = userWallet.scanResponse.card.encryptionKey
|
||||
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
|
||||
|
||||
return if (encryptionKey != null) {
|
||||
keysRepository.save(encryptionKey)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys
|
||||
.plus(encryptionKey)
|
||||
.distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { encryptionKey.encryptionKey }
|
||||
} else {
|
||||
CompletionResult.Success(data = null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadModels(): CompletionResult<Unit> {
|
||||
return getSavedUserWallets()
|
||||
.map { userWallets ->
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.tap.domain.userWalletList.implementation
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class DummyUserWalletsListManager : UserWalletsListManager {
|
||||
override val userWallets: Flow<List<UserWallet>>
|
||||
get() = flowOf(emptyList())
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = flowOf()
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = null
|
||||
override val isLocked: Flow<Boolean>
|
||||
get() = flowOf(true)
|
||||
override val isLockedSync: Boolean
|
||||
get() = true
|
||||
override val hasSavedUserWallets: Boolean
|
||||
get() = false
|
||||
|
||||
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
|
||||
return CompletionResult.Success(null)
|
||||
}
|
||||
|
||||
override fun lock() {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,12 @@ package com.tangem.tap.domain.userWalletList.model
|
|||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletEncryptionKey(
|
||||
val walletId: UserWalletId,
|
||||
val encryptionKey: ByteArray,
|
||||
) {
|
||||
constructor(userWallet: UserWallet) : this(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is UserWalletEncryptionKey) return false
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ internal interface UserWalletsKeysRepository {
|
|||
suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
|
||||
/**
|
||||
* Store the encryption keys for user wallets. Biometric authentication not required
|
||||
* @param encryptionKeys List of encryption keys for user wallets
|
||||
* Save the encryption key for user wallet. Biometric authentication not required
|
||||
* @param encryptionKey [UserWalletEncryptionKey] to save
|
||||
* @return [CompletionResult] of operation
|
||||
* */
|
||||
suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit>
|
||||
suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Delete encryption keys for user wallets. Biometric authentication not required
|
||||
|
|
@ -32,4 +32,10 @@ internal interface UserWalletsKeysRepository {
|
|||
* @return [CompletionResult] of operation
|
||||
* */
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Determine if the user has saved user wallets
|
||||
* @return [Boolean] true if user has saved wallets
|
||||
* */
|
||||
fun hasSavedEncryptionKeys(): Boolean
|
||||
}
|
||||
|
|
@ -12,6 +12,4 @@ internal interface UserWalletsPublicInformationRepository {
|
|||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
fun isNotEmpty(): Boolean
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
|||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
internal interface UserWalletsSensitiveInformationRepository {
|
||||
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
|
||||
suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit>
|
||||
suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.tap.domain.userWalletList.UserWalletListError
|
|||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class BiometricUserWalletsKeysRepository(
|
||||
|
|
@ -44,10 +45,9 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit> {
|
||||
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
encryptionKeys.map { storeEncryptionKey(it) }
|
||||
.fold()
|
||||
storeEncryptionKey(encryptionKey)
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
|
|
@ -77,6 +77,12 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun hasSavedEncryptionKeys(): Boolean {
|
||||
return runBlocking {
|
||||
getUserWalletsIds().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAllInternal(): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getUserWalletsIds()
|
||||
.map { userWalletId ->
|
||||
|
|
|
|||
|
|
@ -72,10 +72,6 @@ internal class DefaultUserWalletsPublicInformationRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun isNotEmpty(): Boolean {
|
||||
return secureStorage.get(StorageKey.UserWalletPublicInformation.name)?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
@JvmName("saveWithPublicInformation")
|
||||
private suspend fun save(
|
||||
publicInformation: List<UserWalletPublicInformation>,
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import com.tangem.common.catching
|
|||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.common.extensions.filterNotNull
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -38,11 +38,12 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
|
|||
Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding")
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
override suspend fun save(userWallet: UserWallet, encryptionKey: ByteArray?): CompletionResult<Unit> {
|
||||
if (encryptionKey == null) return CompletionResult.Success(Unit) // Encryption key is null, do nothing
|
||||
return catching {
|
||||
val encryptedSensitiveInformation = userWallet.sensitiveInformation
|
||||
.encode()
|
||||
.encryptAndStoreIv(userWallet.walletId.stringValue, userWallet.scanResponse.card.encryptionKey)
|
||||
.encryptAndStoreIv(userWallet.walletId.stringValue, encryptionKey)
|
||||
|
||||
getAllEncrypted().toMutableMap()
|
||||
.apply { set(userWallet.walletId.stringValue, encryptedSensitiveInformation) }
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
package com.tangem.domain.common.util
|
||||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
|
||||
val CardDTO.userWalletId: UserWalletId
|
||||
get() = UserWalletId(findWalletPublicKey(wallets))
|
||||
|
||||
val CardDTO.encryptionKey: ByteArray
|
||||
get() = findWalletPublicKey(wallets)
|
||||
?.let { calculateEncryptionKey(it) }
|
||||
?: error("Wallet ID not found")
|
||||
internal val CardDTO.encryptionKey: ByteArray?
|
||||
get() = findPublicKey(wallets)?.let { calculateEncryptionKey(it) }
|
||||
|
||||
private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray {
|
||||
val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray()
|
||||
val keyHash = publicKey.calculateSha256()
|
||||
|
||||
return message.calculateHmacSha256(keyHash)
|
||||
}
|
||||
|
||||
private fun findWalletPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
|
||||
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
|
||||
return wallets.firstOrNull()
|
||||
?.publicKey
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
|||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletCurrenciesManager(
|
||||
|
|
@ -64,7 +65,15 @@ internal class DefaultWalletCurrenciesManager(
|
|||
saveUserCurrencies(card, newCurrencies)
|
||||
}
|
||||
.flatMap {
|
||||
walletAmountsRepository.updateAmountsForUserWallet(
|
||||
val updatedBlockchains = updatedBlockchainNetworks
|
||||
.map { it.blockchain }
|
||||
val updatedWalletStores = walletStoresRepository.get(userWallet.walletId)
|
||||
.firstOrNull()
|
||||
?.filter { it.blockchain in updatedBlockchains }
|
||||
?: return@flatMap CompletionResult.Success(Unit)
|
||||
|
||||
walletAmountsRepository.updateAmountsForWalletStores(
|
||||
walletStores = updatedWalletStores,
|
||||
userWallet = userWallet,
|
||||
fiatCurrency = appCurrencyProvider(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ interface WalletAmountsRepository {
|
|||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun updateAmountsForWalletStores(
|
||||
walletStores: List<WalletStoreModel>,
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
/**
|
||||
* Fetch wallet amounts and fiat rates then update [com.tangem.tap.domain.walletStores.storage.WalletStoresStorage]
|
||||
* and [com.tangem.tap.domain.walletStores.storage.WalletManagerStorage] with new data
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -62,7 +63,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
else withContext(Dispatchers.Default) {
|
||||
awaitAll(
|
||||
async { fetchAmountsForUserWallets(userWallets) },
|
||||
async { fetchFiatRates(userWallets, fiatCurrency) },
|
||||
async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
|
|
@ -75,46 +76,40 @@ internal class DefaultWalletAmountsRepository(
|
|||
return updateAmountsForUserWallets(listOf(userWallet), fiatCurrency)
|
||||
}
|
||||
|
||||
override suspend fun updateAmountsForWalletStores(
|
||||
walletStores: List<WalletStoreModel>,
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
return if (walletStores.isEmpty()) CompletionResult.Success(Unit)
|
||||
else withContext(Dispatchers.Default) {
|
||||
val userWalletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
awaitAll(
|
||||
async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) },
|
||||
async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAmountsForWalletStore(
|
||||
walletStore: WalletStoreModel,
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
val walletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
awaitAll(
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmountsForWalletStore(walletId, scanResponse, walletStore, walletManager)
|
||||
},
|
||||
async { fetchFiatRates(listOf(userWallet), fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountsForUserWallets(
|
||||
userWallets: List<UserWallet>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
userWallets.map { async { fetchAmountsForUserWallet(it) } }
|
||||
.awaitAll()
|
||||
.fold()
|
||||
): CompletionResult<Unit> {
|
||||
return updateAmountsForWalletStores(listOf(walletStore), userWallet, fiatCurrency)
|
||||
}
|
||||
|
||||
private suspend fun fetchFiatRates(
|
||||
userWallets: List<UserWallet>,
|
||||
walletStores: List<WalletStoreModel>?,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
val walletsIds = userWallets.map { it.walletId }
|
||||
val walletStores = walletsIds
|
||||
.flatMap {
|
||||
walletStoresStorage.getAll()
|
||||
.first()
|
||||
.getOrElse(it) { emptyList() }
|
||||
}
|
||||
val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
|
||||
|
||||
val currencies = walletStores
|
||||
val currencies = walletStoresInternal
|
||||
.asSequence()
|
||||
.flatMap { it.walletsData }
|
||||
.map { it.currency }
|
||||
|
|
@ -124,7 +119,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
return withContext(dispatchers.io) {
|
||||
runCatching { tangemTechApi.getRates(fiatCurrency.code.lowercase(), coinsIds.joinToString(",")) }
|
||||
.onSuccess {
|
||||
updateWalletStoresWithFiatRates(walletStores = walletStores, fiatRates = it.rates)
|
||||
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
|
||||
return@withContext CompletionResult.Success(Unit)
|
||||
}
|
||||
.onFailure {
|
||||
|
|
@ -137,7 +132,6 @@ internal class DefaultWalletAmountsRepository(
|
|||
error,
|
||||
"""
|
||||
Unable to fetch fiat rates
|
||||
|- User wallets ids: $walletsIds
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
|
@ -149,20 +143,34 @@ internal class DefaultWalletAmountsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountsForUserWallets(
|
||||
userWallets: List<UserWallet>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
userWallets.map { async { fetchAmountsForUserWallet(it) } }
|
||||
.awaitAll()
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountsForUserWallet(
|
||||
userWallet: UserWallet,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
val walletId = userWallet.walletId
|
||||
val userWalletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val walletStores = walletStoresStorage.getAll()
|
||||
.first()
|
||||
.getOrElse(walletId) { emptyList() }
|
||||
val walletStores = getWalletStores(listOf(userWallet))
|
||||
|
||||
fetchAmountForWalletStores(userWalletId, scanResponse, walletStores)
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountForWalletStores(
|
||||
userWalletId: UserWalletId,
|
||||
scanResponse: ScanResponse,
|
||||
walletStores: List<WalletStoreModel>,
|
||||
): CompletionResult<Unit> = coroutineScope {
|
||||
walletStores.map { walletStore ->
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmountsForWalletStore(walletId, scanResponse, walletStore, walletManager)
|
||||
fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
|
@ -170,7 +178,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchAmountsForWalletStore(
|
||||
walletId: UserWalletId,
|
||||
userWalletId: UserWalletId,
|
||||
scanResponse: ScanResponse,
|
||||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager?,
|
||||
|
|
@ -180,11 +188,15 @@ internal class DefaultWalletAmountsRepository(
|
|||
}
|
||||
|
||||
return when {
|
||||
hasMissedDerivations -> updateWalletStoreWithMissedDerivation(walletStore)
|
||||
walletManager == null -> updateWalletStoreWithUnreachable(walletStore)
|
||||
hasMissedDerivations -> {
|
||||
updateWalletStoreWithMissedDerivation(walletStore)
|
||||
}
|
||||
walletManager == null -> {
|
||||
updateWalletStoreWithUnreachable(walletStore)
|
||||
}
|
||||
else -> {
|
||||
withInternetConnection { walletManager.update() }
|
||||
.map { updateWalletManagerWithAmounts(walletId, walletManager) }
|
||||
.map { updateWalletManagerWithAmounts(userWalletId, walletManager) }
|
||||
.flatMap {
|
||||
updateWalletStoreWithAmounts(
|
||||
walletStore = walletStore,
|
||||
|
|
@ -242,35 +254,6 @@ internal class DefaultWalletAmountsRepository(
|
|||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult<Unit> {
|
||||
return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val error = WalletStoresError.NoInternetConnection
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
} else withContext(Dispatchers.IO) {
|
||||
catching { block() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWalletManagerWithAmounts(
|
||||
walletId: UserWalletId,
|
||||
walletManager: WalletManager,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[walletId].orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == it.wallet.blockchain
|
||||
}
|
||||
}
|
||||
|
||||
prevManagers.apply {
|
||||
set(walletId, newManagersForUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWalletStoreWithError(
|
||||
walletStore: WalletStoreModel,
|
||||
wallet: Wallet,
|
||||
|
|
@ -419,4 +402,44 @@ internal class DefaultWalletAmountsRepository(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult<Unit> {
|
||||
return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val error = WalletStoresError.NoInternetConnection
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
} else withContext(Dispatchers.IO) {
|
||||
catching { block() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWalletManagerWithAmounts(
|
||||
userWalletId: UserWalletId,
|
||||
walletManager: WalletManager,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == it.wallet.blockchain
|
||||
}
|
||||
}
|
||||
|
||||
prevManagers.apply {
|
||||
set(userWalletId, newManagersForUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWalletStores(userWallets: List<UserWallet>): List<WalletStoreModel> {
|
||||
return userWallets
|
||||
.map { it.walletId }
|
||||
.flatMap { userWalletId ->
|
||||
walletStoresStorage.getAll()
|
||||
.firstOrNull()
|
||||
?.get(userWalletId)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,11 +106,7 @@ internal class DefaultWalletManagersRepository(
|
|||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
userWalletIds.forEach { userWalletId ->
|
||||
remove(userWalletId)
|
||||
}
|
||||
}
|
||||
prevManagers.filterKeys { it !in userWalletIds } as HashMap<UserWalletId, List<WalletManager>>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,9 +62,11 @@ internal fun WalletStoreModel.updateWithSelf(
|
|||
): WalletStoreModel {
|
||||
val oldStore = this
|
||||
return oldStore.copy(
|
||||
walletManager = newWalletStore.walletManager,
|
||||
walletRent = newWalletStore.walletRent,
|
||||
derivationPath = newWalletStore.derivationPath,
|
||||
walletsData = oldStore.walletsData.updateWithSelf(newWalletStore.walletsData),
|
||||
walletRent = newWalletStore.walletRent,
|
||||
blockchainNetwork = newWalletStore.blockchainNetwork,
|
||||
walletManager = newWalletStore.walletManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
|
|
@ -20,6 +18,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.model.builders.UserWalletBuilder
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
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
|
||||
|
|
@ -27,6 +26,7 @@ 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
|
||||
|
|
@ -79,20 +79,16 @@ class DetailsMiddleware {
|
|||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
scope.launch {
|
||||
tangemSdkManager.scanCard(allowRequestAccessCodeFromRepository = true)
|
||||
.doOnSuccess { card ->
|
||||
val isSameWallet = state.scanResponse?.card?.userWalletId
|
||||
?.equals(card.userWalletId)
|
||||
?: false
|
||||
tangemSdkManager.scanProduct(userTokensRepository)
|
||||
.doOnSuccess { scanResponse ->
|
||||
val currentUserWalletId = state.scanResponse
|
||||
?.let { UserWalletIdBuilder.scanResponse(it).build() }
|
||||
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
|
||||
.build()
|
||||
val isSameWallet = currentUserWalletId == scannedUserWalletId
|
||||
|
||||
// !!! Workaround !!!
|
||||
// TODO: Remove after [REDACTED_JIRA]
|
||||
val isTwinned = card.wallets.firstOrNull()?.publicKey?.toHexString()
|
||||
?.equals(state.scanResponse?.secondTwinPublicKey)
|
||||
?: false
|
||||
|
||||
if (isSameWallet || isTwinned) {
|
||||
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
|
||||
if (isSameWallet) {
|
||||
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scanResponse.card))
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
|
|
@ -122,8 +118,10 @@ class DetailsMiddleware {
|
|||
is DetailsAction.ResetToFactory.Proceed -> {
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
scope.launch {
|
||||
val userWalletId = UserWalletIdBuilder.card(card).build()
|
||||
|
||||
tangemSdkManager.resetToFactorySettings(card.cardId)
|
||||
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
|
||||
.flatMap { userWalletsListManager.delete(listOfNotNull(userWalletId)) }
|
||||
.flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) }
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished())
|
||||
|
|
@ -275,12 +273,9 @@ class DetailsMiddleware {
|
|||
|
||||
private suspend fun saveCurrentWallet(state: DetailsState) {
|
||||
val scanResponse = state.scanResponse ?: return
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
val userWallet = UserWalletBuilder(scanResponse).build() ?: return
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Wallet saving failed")
|
||||
}
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
|
||||
|
||||
|
|
@ -296,6 +291,9 @@ class DetailsMiddleware {
|
|||
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSavedWallets() {
|
||||
|
|
@ -315,6 +313,9 @@ class DetailsMiddleware {
|
|||
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to delete saved wallets")
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAccessCodes(state: DetailsState) {
|
||||
|
|
@ -353,6 +354,9 @@ class DetailsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to delete saved access codes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import com.tangem.tap.domain.extensions.signedHashesCount
|
|||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import org.rekotlin.Action
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ private fun handlePrepareScreen(
|
|||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appCurrency = store.state.globalState.appCurrency,
|
||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||
saveWallets = userWalletsListManager.hasSavedUserWallets,
|
||||
saveWallets = preferencesStorage.shouldSaveUserWallets,
|
||||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.tap.common.analytics.events.Settings
|
|||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.CardSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Store
|
||||
|
||||
class CardSettingsViewModel(private val store: Store<AppState>) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.material.IconToggleButton
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -37,6 +38,7 @@ fun ResetCardScreen(state: ResetCardScreenState, onBackPressed: () -> Unit) {
|
|||
SettingsScreensScaffold(
|
||||
content = { ResetCardView(state = state) },
|
||||
onBackClick = onBackPressed,
|
||||
backgroundColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ private fun readCard() = scope.launch {
|
|||
onSuccess = { scanResponse ->
|
||||
scope.launch {
|
||||
if (preferencesStorage.shouldSaveUserWallets) {
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@launch
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import com.tangem.tap.common.extensions.copyToClipboard
|
|||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchShare
|
||||
import com.tangem.tap.common.extensions.dispatchToastNotification
|
||||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -62,26 +62,12 @@ class AddressInfoBottomSheetDialog(
|
|||
Analytics.send(Token.Recieve.ButtonShareAddress())
|
||||
store.dispatchShare(data.shareUrl)
|
||||
}
|
||||
tvReceiveMessage.text = getQRReceiveMessage(tvReceiveMessage.context, stateDialog.currency)
|
||||
}
|
||||
}
|
||||
|
||||
fun getQRReceiveMessage(context: Context, currency: Currency): String {
|
||||
return when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
context.getString(
|
||||
R.string.address_qr_code_message_format,
|
||||
currency.blockchain.fullName,
|
||||
currency.currencySymbol,
|
||||
)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
context.getString(
|
||||
R.string.address_qr_code_message_token_format,
|
||||
currency.token.name,
|
||||
currency.currencySymbol,
|
||||
currency.blockchain.fullName,
|
||||
)
|
||||
}
|
||||
val blockchain = stateDialog.currency.blockchain
|
||||
tvReceiveMessage.text = tvReceiveMessage.getString(
|
||||
id = R.string.address_qr_code_message_format,
|
||||
blockchain.fullName,
|
||||
blockchain.currency,
|
||||
blockchain.fullName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,10 @@ class OnboardingHelper {
|
|||
// then open save wallet screen
|
||||
tangemSdkManager.canUseBiometry &&
|
||||
preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch {
|
||||
store.onCardScanned(scanResponse)
|
||||
|
||||
delay(timeMillis = 1_200)
|
||||
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
|
@ -25,6 +24,7 @@ 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.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
|
@ -156,7 +156,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
TwinCardsStep.CreateFirstWallet -> {
|
||||
scope.launch {
|
||||
userWalletsListManager.delete(
|
||||
listOf(getScanResponse().card.userWalletId),
|
||||
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ internal class SaveWalletMiddleware {
|
|||
scope.launch {
|
||||
val userWallet = UserWalletBuilder(scanResponse)
|
||||
.backupCardsIds(state.backupInfo?.backupCardsIds)
|
||||
.build()
|
||||
.build() ?: return@launch
|
||||
|
||||
val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ class TokensMiddleware {
|
|||
&& blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
) {
|
||||
store.dispatchDebugErrorNotification("Nothing to save")
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
@ -281,13 +281,16 @@ class TokensMiddleware {
|
|||
) {
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
val updatedUserWallet = selectedUserWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.save(updatedUserWallet, canOverride = true)
|
||||
.flatMap {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { userWallet ->
|
||||
userWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
},
|
||||
)
|
||||
.flatMap { updatedUserWallet ->
|
||||
walletCurrenciesManager.addCurrencies(
|
||||
userWallet = updatedUserWallet,
|
||||
currenciesToAdd = currencyList,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.tap.common.analytics.events.MainScreen
|
|||
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
|
|
@ -180,21 +181,25 @@ class MultiWalletMiddleware {
|
|||
}
|
||||
|
||||
private fun scanAndUpdateCard(
|
||||
selectedWallet: UserWallet,
|
||||
selectedUserWallet: UserWallet,
|
||||
state: WalletState?,
|
||||
) = scope.launch {
|
||||
) = scope.launch(Dispatchers.Default) {
|
||||
Analytics.send(MainScreen.CardWasScanned())
|
||||
ScanCardProcessor.scan(
|
||||
cardId = selectedWallet.cardId,
|
||||
cardId = selectedUserWallet.cardId,
|
||||
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
|
||||
) { scanResponse ->
|
||||
val userWallet = selectedWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
userWalletsListManager.update(
|
||||
userWalletId = selectedUserWallet.walletId,
|
||||
update = { userWallet ->
|
||||
userWallet.copy(
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
userWalletsListManager.save(userWallet, canOverride = true)
|
||||
.doOnSuccess {
|
||||
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
|
||||
.doOnSuccess { updatedUserWallet ->
|
||||
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
|
||||
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ class WalletMiddleware {
|
|||
val reduxWalletStores = wallStores.mapToReduxModels(state.isMultiwalletAllowed)
|
||||
store.dispatchOnMain(
|
||||
WalletAction.WalletStoresChanged.UpdateWalletStores(
|
||||
reduxWalletStores = reduxWalletStores.toList(),
|
||||
reduxWalletStores = reduxWalletStores,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class MultiWalletReducer {
|
|||
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
|
||||
return when (action) {
|
||||
is WalletAction.MultiWallet.AddBlockchains -> {
|
||||
val walletStores: List<WalletStore> = action.blockchains.mapNotNull { blockchain ->
|
||||
val walletStores: List<WalletStore> = action.blockchains.map { blockchain ->
|
||||
val walletManager = action.walletManagers.firstOrNull {
|
||||
it.wallet.blockchain == blockchain.blockchain &&
|
||||
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
|
||||
|
|
@ -70,10 +70,10 @@ class MultiWalletReducer {
|
|||
)
|
||||
}
|
||||
|
||||
val selectedCurrency = if (!state.isMultiwalletAllowed) {
|
||||
walletStores.firstOrNull()?.walletsData?.firstOrNull()?.currency
|
||||
} else {
|
||||
val selectedCurrency = if (state.isMultiwalletAllowed) {
|
||||
state.selectedCurrency
|
||||
} else {
|
||||
walletStores.firstOrNull()?.walletsData?.firstOrNull()?.currency
|
||||
}
|
||||
state.copy(
|
||||
walletsStores = walletStores,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
|||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.TotalBalance
|
||||
import com.tangem.tap.features.wallet.models.WalletRent
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
|
|
@ -36,7 +35,6 @@ import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData
|
|||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -344,8 +342,9 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
card.settings.isBackupAllowed &&
|
||||
card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
walletCardsCount = card.findCardsCount(),
|
||||
walletsStores = newState.walletsStores,
|
||||
totalBalance = if (isMultiCurrency) {
|
||||
TotalBalance(ProgressState.Loading, BigDecimal.ZERO, store.state.globalState.appCurrency)
|
||||
newState.totalBalance
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
|
@ -362,13 +361,13 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
|
|||
)
|
||||
}
|
||||
is WalletAction.LoadData.Success -> {
|
||||
val selectedCurrency = if (!newState.isMultiwalletAllowed) {
|
||||
val selectedCurrency = if (newState.isMultiwalletAllowed) {
|
||||
newState.selectedCurrency
|
||||
} else {
|
||||
newState.walletsStores.firstOrNull()
|
||||
?.walletsData
|
||||
?.firstOrNull()
|
||||
?.currency
|
||||
} else {
|
||||
newState.selectedWalletData?.currency
|
||||
}
|
||||
|
||||
newState = newState.copy(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import com.tangem.tap.common.extensions.toQrCode
|
|||
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.getQRReceiveMessage
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.PendingTransaction
|
||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||
|
|
@ -132,12 +131,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
walletDataWatcher.clear()
|
||||
walletStateWatcher.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
@ -328,8 +323,21 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
)
|
||||
}
|
||||
ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
|
||||
tvReceiveMessage.text =
|
||||
getQRReceiveMessage(tvReceiveMessage.context, state.currency)
|
||||
|
||||
tvReceiveMessage.text = when (val currency = state.currency) {
|
||||
is Currency.Blockchain -> tvReceiveMessage.getString(
|
||||
id = R.string.address_qr_code_message_format,
|
||||
currency.blockchain.fullName,
|
||||
currency.currencySymbol,
|
||||
currency.blockchain.fullName,
|
||||
)
|
||||
is Currency.Token -> tvReceiveMessage.getString(
|
||||
id = R.string.address_qr_code_message_format,
|
||||
currency.token.name,
|
||||
currency.currencySymbol,
|
||||
currency.blockchain.fullName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.wallet.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
|
@ -9,6 +10,7 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionInflater
|
||||
|
|
@ -16,6 +18,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
|
|||
import coil.load
|
||||
import coil.size.Scale
|
||||
import com.tangem.core.ui.fragments.setStatusBarColor
|
||||
import com.tangem.core.ui.utils.OneTouchClickListener
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.tap.MainActivity
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
|
|
@ -59,6 +62,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
|
||||
private val viewModel by viewModels<WalletViewModel>()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
activity?.lifecycleScope?.launchWhenCreated {
|
||||
viewModel.launch()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
|
@ -81,7 +91,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.slide_right)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
viewModel.launch()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
@ -110,9 +119,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
store.dispatch(WalletAction.ChangeWallet)
|
||||
}
|
||||
binding.toolbar.setNavigationOnClickListener(
|
||||
OneTouchClickListener { store.dispatch(WalletAction.ChangeWallet) },
|
||||
)
|
||||
setupWarningsRecyclerView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
addCustomActionOnCard()
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import com.tangem.tap.store
|
|||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -21,8 +23,10 @@ internal class WalletViewModel : ViewModel() {
|
|||
|
||||
private fun bootstrapSelectedWalletStoresChanges() {
|
||||
userWalletsListManager.selectedUserWallet
|
||||
.flatMapLatest { selectedWallet ->
|
||||
walletStoresManager.get(selectedWallet.walletId)
|
||||
.map { it.walletId }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { selectedUserWalletId ->
|
||||
walletStoresManager.get(selectedUserWalletId)
|
||||
}
|
||||
.onEach { walletStores ->
|
||||
store.dispatch(WalletAction.WalletStoresChanged(walletStores))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
|
||||
import com.tangem.tap.common.extensions.getQuantityString
|
||||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
|
|
@ -65,7 +66,7 @@ class SingleWalletView : WalletView() {
|
|||
|
||||
setupTwinCards(state.twinCardsState, binding)
|
||||
setupButtons(state.primaryWallet, binding, state.isExchangeServiceFeatureOn)
|
||||
setupAddressCard(state.primaryWallet, binding)
|
||||
setupAddressCard(state, binding)
|
||||
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
|
||||
setupBalance(state, state.primaryWallet)
|
||||
}
|
||||
|
|
@ -152,35 +153,50 @@ class SingleWalletView : WalletView() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupAddressCard(state: WalletData, binding: FragmentWalletBinding) = with(binding.lAddress) {
|
||||
if (state.walletAddresses != null && state.currency is Currency.Blockchain) {
|
||||
private fun setupAddressCard(state: WalletState, binding: FragmentWalletBinding) = with(binding.lAddress) {
|
||||
val primaryWallet = state.primaryWallet
|
||||
if (primaryWallet?.walletAddresses != null && primaryWallet.currency is Currency.Blockchain) {
|
||||
binding.lAddress.root.show()
|
||||
if (state.shouldShowMultipleAddress()) {
|
||||
if (primaryWallet.shouldShowMultipleAddress()) {
|
||||
(binding.lAddress.root as? ViewGroup)?.beginDelayedTransition()
|
||||
chipGroupAddressType.show()
|
||||
chipGroupAddressType.fitChipsByGroupWidth()
|
||||
val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
|
||||
val checkedId = MultipleAddressUiHelper.typeToId(primaryWallet.walletAddresses.selectedAddress.type)
|
||||
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
|
||||
|
||||
chipGroupAddressType.setOnCheckedChangeListener { group, checkedId ->
|
||||
if (checkedId == -1) return@setOnCheckedChangeListener
|
||||
val type = MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain)
|
||||
val type = MultipleAddressUiHelper.idToType(checkedId, primaryWallet.currency.blockchain)
|
||||
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
|
||||
}
|
||||
} else {
|
||||
chipGroupAddressType.hide()
|
||||
}
|
||||
tvAddress.text = state.walletAddresses.selectedAddress.address
|
||||
tvAddress.text = primaryWallet.walletAddresses.selectedAddress.address
|
||||
tvExplore.setOnClickListener {
|
||||
store.dispatch(
|
||||
WalletAction.ExploreAddress(
|
||||
state.walletAddresses.selectedAddress.exploreUrl,
|
||||
primaryWallet.walletAddresses.selectedAddress.exploreUrl,
|
||||
fragment!!.requireContext(),
|
||||
),
|
||||
)
|
||||
}
|
||||
setupCardInfo(state)
|
||||
} else {
|
||||
binding.lAddress.root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCardInfo(state: WalletState) {
|
||||
val textView = binding?.lAddress?.tvInfo
|
||||
val blockchain = state.primaryWallet?.currency?.blockchain
|
||||
if (textView != null && blockchain != null) {
|
||||
textView.text = textView.getString(
|
||||
id = R.string.address_qr_code_message_format,
|
||||
blockchain.fullName,
|
||||
blockchain.currency,
|
||||
blockchain.fullName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.walletSelector.redux
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
|
|
@ -19,6 +20,7 @@ import com.tangem.tap.domain.model.UserWallet
|
|||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.model.builders.UserWalletBuilder
|
||||
import com.tangem.tap.domain.scanCard.ScanCardProcessor
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
|
@ -131,21 +133,46 @@ internal class WalletSelectorMiddleware {
|
|||
private fun addWallet() = scope.launch {
|
||||
Analytics.send(MyWallets.Button.ScanNewCard)
|
||||
|
||||
scanCardInternal { scanResponse ->
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
val prevUseBiometricsForAccessCode = tangemSdkManager.useBiometricsForAccessCode()
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
|
||||
}
|
||||
.doOnSuccess {
|
||||
Analytics.send(MyWallets.CardWasScanned)
|
||||
// Update access code policy for access code saving when a card was scanned
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
ScanCardProcessor.scan(
|
||||
onSuccess = { scanResponse ->
|
||||
saveUserWalletAndPopBackToWalletScreen(scanResponse)
|
||||
.doOnFailure { error ->
|
||||
// Rollback policy if card saving was failed
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
// Rollback policy if card scanning was failed
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
|
||||
},
|
||||
onWalletNotCreated = {
|
||||
// No need to rollback policy, continue with the policy set before the card scan
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult<Unit> {
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
|
||||
|
||||
return userWalletsListManager.save(userWallet)
|
||||
.doOnSuccess {
|
||||
Analytics.send(MyWallets.CardWasScanned)
|
||||
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectWallet(userWalletId: UserWalletId) {
|
||||
|
|
@ -212,9 +239,7 @@ internal class WalletSelectorMiddleware {
|
|||
Analytics.send(MyWallets.Button.EditWalletTapped)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.get(userWalletId)
|
||||
.map { it.copy(name = newName) }
|
||||
.flatMap { userWalletsListManager.save(it, canOverride = true) }
|
||||
userWalletsListManager.update(userWalletId) { it.copy(name = newName) }
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
|
||||
}
|
||||
|
|
@ -252,43 +277,32 @@ internal class WalletSelectorMiddleware {
|
|||
.flatMap { walletStoresManager.delete(userWalletsIds) }
|
||||
.flatMap { deleteAccessCodes(userWalletsIds) }
|
||||
.doOnSuccess {
|
||||
val selectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
|
||||
when {
|
||||
selectedWallet == null -> {
|
||||
selectedUserWallet == null -> {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
}
|
||||
currentSelectedWalletId != selectedWallet.walletId -> {
|
||||
store.onUserWalletSelected(selectedWallet)
|
||||
|
||||
currentSelectedWalletId != selectedUserWallet.walletId -> {
|
||||
store.onUserWalletSelected(selectedUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun scanCardInternal(
|
||||
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
|
||||
) {
|
||||
ScanCardProcessor.scan(
|
||||
onSuccess = {
|
||||
onCardScanned(it)
|
||||
},
|
||||
onFailure = { error ->
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
|
||||
},
|
||||
onWalletNotCreated = {
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun deleteAccessCodes(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
val cardsIds = userWalletsListManager.userWallets.firstOrNull().orEmpty()
|
||||
.asSequence()
|
||||
.filter { it.walletId in userWalletsIds }
|
||||
.flatMap { it.cardsInWallet }
|
||||
val cardsIds = userWalletsListManager.userWallets.firstOrNull()
|
||||
?.asSequence()
|
||||
?.filter { it.walletId in userWalletsIds }
|
||||
?.flatMap { it.cardsInWallet }
|
||||
?.toSet()
|
||||
|
||||
return tangemSdkManager.deleteSavedUserCodes(cardsIds.toSet())
|
||||
return if (cardsIds.isNullOrEmpty()) {
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
tangemSdkManager.deleteSavedUserCodes(cardsIds.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance(
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
|
||||
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
|
||||
when {
|
||||
editingUserWalletsIds.isNotEmpty() && isWalletLocked(userWalletId, this) -> Unit
|
||||
editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> {
|
||||
editWallet(userWalletId)
|
||||
}
|
||||
|
|
@ -52,7 +51,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
}
|
||||
|
||||
fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) {
|
||||
if (!isWalletLocked(userWalletId, this) && editingUserWalletsIds.isEmpty()) {
|
||||
if (editingUserWalletsIds.isEmpty()) {
|
||||
editWallet(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +66,15 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
|
||||
fun renameWallet() = with(state.value) {
|
||||
if (editingUserWalletsIds.isNotEmpty() && renameWalletDialog == null) {
|
||||
val editedWalletId = editingUserWalletsIds.first()
|
||||
val editedWallet = (multiCurrencyWallets + singleCurrencyWallets)
|
||||
.find { it.id == editedWalletId }
|
||||
val editedUserWalletId = editingUserWalletsIds.first()
|
||||
val editedUserWallet = (multiCurrencyWallets + singleCurrencyWallets)
|
||||
.find { it.id == editedUserWalletId }
|
||||
|
||||
if (editedWallet != null) {
|
||||
if (editedUserWallet != null) {
|
||||
val dialog = RenameWalletDialog(
|
||||
currentName = editedWallet.name,
|
||||
currentName = editedUserWallet.name,
|
||||
onApply = { newName ->
|
||||
store.dispatch(WalletSelectorAction.RenameWallet(editedWalletId, newName))
|
||||
store.dispatch(WalletSelectorAction.RenameWallet(editedUserWalletId, newName))
|
||||
stateInternal.update { prevState ->
|
||||
prevState.copy(
|
||||
renameWalletDialog = null,
|
||||
|
|
@ -137,11 +136,6 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
}
|
||||
}
|
||||
|
||||
private fun isWalletLocked(userWalletId: UserWalletId, state: WalletSelectorScreenState): Boolean = with(state) {
|
||||
multiCurrencyWallets.find { it.id == userWalletId }?.isLocked
|
||||
?: singleCurrencyWallets.find { it.id == userWalletId }?.isLocked ?: isLocked
|
||||
}
|
||||
|
||||
private fun subscribeToStoreChanges() {
|
||||
store.subscribe(this) { appState ->
|
||||
appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState }
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ internal class WelcomeMiddleware {
|
|||
|
||||
private fun proceedWithCard(state: WelcomeState) = scope.launch {
|
||||
scanCardInternal { scanResponse ->
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal
|
||||
|
||||
userWalletsListManager.save(userWallet, canOverride = true)
|
||||
.doOnFailure { error ->
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ import com.tangem.domain.common.CardDTO
|
|||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import com.tangem.lib.crypto.models.Currency
|
||||
import com.tangem.lib.crypto.models.Currency.NativeToken
|
||||
import com.tangem.lib.crypto.models.Currency.NonNativeToken
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import org.rekotlin.Action
|
||||
|
|
@ -54,7 +54,12 @@ class UserWalletManagerImpl(
|
|||
}
|
||||
|
||||
override fun getWalletId(): String {
|
||||
return appStateHolder.getActualCard()?.userWalletId?.stringValue ?: ""
|
||||
return appStateHolder.getActualCard()?.let {
|
||||
UserWalletIdBuilder.card(it)
|
||||
.build()
|
||||
?.stringValue
|
||||
}
|
||||
?: ""
|
||||
}
|
||||
|
||||
override suspend fun isTokenAdded(currency: Currency): Boolean {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imv_qr_code"
|
||||
tools:text="@string/address_qr_code_message_token_format" />
|
||||
tools:text="Send only Ethereum (ETH) from Ethereum network to this address. Using other tokens and networks may result in loss of funds." />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline3"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,17 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_info"
|
||||
style="@style/TextViewOnboarding.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/spacing16"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_explore"
|
||||
tools:text="Send only Ethereum (ETH) from Ethereum network to this address. Using other tokens and networks may result in loss of funds." />
|
||||
|
||||
<View
|
||||
android:id="@+id/v_payid_divider"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_copy"
|
||||
tools:text="@string/address_qr_code_message_token_format" />
|
||||
tools:text="Send only Ethereum (ETH) from Ethereum network to this address. Using other tokens and networks may result in loss of funds." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ object Versions {
|
|||
const val tangemBlockchainSdk = "develop-142"
|
||||
// const val tangemBlockchainSdk = "0.0.1"
|
||||
|
||||
const val tangemCardSgk = "develop-178"
|
||||
const val tangemCardSgk = "develop-179"
|
||||
// endregion Tangem
|
||||
|
||||
// region Testing
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_3_cards">3 cards</string>
|
||||
<string name="shop_2_cards">2 cards</string>
|
||||
<string name="shop_shipping">Shipping</string>
|
||||
<string name="shop_free">Free</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
|
|
@ -355,8 +353,7 @@
|
|||
<string name="onboarding_done_header">Success!</string>
|
||||
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_token_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="onboarding_twins_interrupt_warning">If the process of creating the wallet gets interrupted in any way, you\'ll have to start over.</string>
|
||||
<string name="onboarding_twin_exit_warning">The twinning process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="warning_button_ok">OK, ich hab\'s!</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_3_cards">3 cards</string>
|
||||
<string name="shop_2_cards">2 cards</string>
|
||||
<string name="shop_shipping">Shipping</string>
|
||||
<string name="shop_free">Free</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
|
|
@ -355,8 +353,7 @@
|
|||
<string name="onboarding_done_header">Success!</string>
|
||||
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_token_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="onboarding_twins_interrupt_warning">If the process of creating the wallet gets interrupted in any way, you\'ll have to start over.</string>
|
||||
<string name="onboarding_twin_exit_warning">The twinning process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="warning_button_ok">Ok, je l\'ai!</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_3_cards">3 cards</string>
|
||||
<string name="shop_2_cards">2 cards</string>
|
||||
<string name="shop_shipping">Shipping</string>
|
||||
<string name="shop_free">Free</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
|
|
@ -355,8 +353,7 @@
|
|||
<string name="onboarding_done_header">Success!</string>
|
||||
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_token_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="onboarding_twins_interrupt_warning">If the process of creating the wallet gets interrupted in any way, you\'ll have to start over.</string>
|
||||
<string name="onboarding_twin_exit_warning">The twinning process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="warning_button_ok">Ok, ho capito!</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_3_cards">3 карты</string>
|
||||
<string name="shop_2_cards">2 карты</string>
|
||||
<string name="shop_shipping">Доставка</string>
|
||||
<string name="shop_free">Бесплатно</string>
|
||||
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
|
||||
|
|
@ -208,8 +206,8 @@
|
|||
<string name="swapping_token_list_your_tokens">Ваши токены</string>
|
||||
<string name="swapping_token_list_other_tokens">Другие токены</string>
|
||||
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Причина: %s. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="referral_error_failed_to_participate">Не удалось обработать вашу заявку на участие. Причина: %s. Пожалуйста, попробуйте позже. Если проблема сохранится, вы можете обратиться в техподдержку.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="referral_error_failed_to_participate">Не удалось обработать вашу заявку на участие. Код ошибки: %s. Пожалуйста, попробуйте позже. Если проблема сохранится, вы можете обратиться в техподдержку.</string>
|
||||
<string name="referral_promo_code_copied">Персональный код скопирован!</string>
|
||||
<string name="referral_share_link">Купи Tangem Wallet со скидкой!\n%s</string>
|
||||
<string name="warning_existential_deposit_message">Сеть %s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %s, он будет деактивирован, а все оставшиеся средства будут уничтожены.</string>
|
||||
|
|
@ -355,8 +353,7 @@
|
|||
<string name="onboarding_done_header">Успешно!</string>
|
||||
<string name="onboarding_done_body">Ваша карта активирована и готова к использованию</string>
|
||||
<string name="onboarding_balance_title">Баланс</string>
|
||||
<string name="address_qr_code_message_format">Отправляйте только %s (%s) на этот адрес. Иначе это может привести к утрате средств.</string>
|
||||
<string name="address_qr_code_message_token_format">Отправляйте только %s (%s) из сети %s на этот адрес. Иначе это может привести к утрате средств.</string>
|
||||
<string name="address_qr_code_message_format">Отправляйте только %s (%s) в сети %s на этот адрес. Использование другой сети может привести к утрате средств.</string>
|
||||
<string name="onboarding_twins_interrupt_warning">Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала.</string>
|
||||
<string name="onboarding_twin_exit_warning">Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас.</string>
|
||||
<string name="warning_button_ok">Понятно!</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_3_cards">3 cards</string>
|
||||
<string name="shop_2_cards">2 cards</string>
|
||||
<string name="shop_shipping">Shipping</string>
|
||||
<string name="shop_free">Free</string>
|
||||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
|
|
@ -33,7 +31,7 @@
|
|||
<string name="search_tokens_title">Search tokens</string>
|
||||
<string name="alert_demo_message">You are currently running in Demo mode. All funds are not real.</string>
|
||||
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
|
||||
<string name="token_details_send_blocked_fee_format">Not enough funds for fee on your %s wallet to send a transaction. Top up your %s wallet first.</string>
|
||||
<string name="token_details_send_blocked_fee_format">Not enough funds for fee in your %s wallet to send a transaction. Top up your %s wallet first.</string>
|
||||
<string name="currency_subtitle_expanded">Available networks</string>
|
||||
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
|
||||
<string name="common_attention">Attention</string>
|
||||
|
|
@ -208,8 +206,8 @@
|
|||
<string name="swapping_token_list_your_tokens">Your tokens</string>
|
||||
<string name="swapping_token_list_other_tokens">Other tokens</string>
|
||||
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Reason: %s. Please try again later.</string>
|
||||
<string name="referral_error_failed_to_participate">Your participation request could not be processed. Reason: %s. Please try again later. If the problem persists — feel free to contact our support.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
|
||||
<string name="referral_error_failed_to_participate">Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support.</string>
|
||||
<string name="referral_promo_code_copied">Personal code copied!</string>
|
||||
<string name="referral_share_link">Buy Tangem Wallet with discount!\n%s</string>
|
||||
<string name="warning_existential_deposit_message">%s network has a concept of Existential Deposit. If your account drops below %s it will be deactivated and any remaining funds will be destroyed.</string>
|
||||
|
|
@ -355,8 +353,7 @@
|
|||
<string name="onboarding_done_header">Success!</string>
|
||||
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_token_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_qr_code_message_format">Send only %s (%s) from %s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="onboarding_twins_interrupt_warning">If the process of creating the wallet gets interrupted in any way, you\'ll have to start over.</string>
|
||||
<string name="onboarding_twin_exit_warning">The twinning process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="warning_button_ok">Ok, Got it!</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* Implementation of click listener for preventing multiple click events
|
||||
*
|
||||
* @property action action that called when a view has been clicked
|
||||
*/
|
||||
class OneTouchClickListener(private val action: () -> Unit) : View.OnClickListener {
|
||||
|
||||
private var lastClickTimeMs: Long = 0L
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
if (SystemClock.elapsedRealtime() - lastClickTimeMs > CLICK_DELAY_MS) {
|
||||
lastClickTimeMs = SystemClock.elapsedRealtime()
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CLICK_DELAY_MS = 500L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
package com.tangem.domain.common.util
|
||||
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.common.extensions.calculateHmacSha256
|
||||
|
||||
class UserWalletId(
|
||||
val stringValue: String,
|
||||
) {
|
||||
val value = stringValue.hexToBytes()
|
||||
|
||||
constructor(walletPublicKey: ByteArray?) : this(
|
||||
stringValue = walletPublicKey?.let { calculateUserWalletId(it).toHexString() } ?: "",
|
||||
constructor(value: ByteArray?) : this(
|
||||
stringValue = value?.toHexString() ?: "",
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
|
@ -32,12 +30,4 @@ class UserWalletId(
|
|||
"UserWalletId(${take(3)}...${takeLast(3)})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateUserWalletId(publicKey: ByteArray): ByteArray {
|
||||
val message = MESSAGE_FOR_WALLET_ID.toByteArray()
|
||||
val keyHash = publicKey.calculateSha256()
|
||||
return message.calculateHmacSha256(keyHash)
|
||||
}
|
||||
|
||||
private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue