Updated on 2026-08-14
This commit is contained in:
commit
c6c1fd3044
42 changed files with 430 additions and 297 deletions
|
|
@ -14,6 +14,11 @@ sealed class MainScreen(
|
|||
class CardWasScanned : MainScreen("Card Was Scanned")
|
||||
class ButtonMyWallets : MainScreen("Button - My Wallets")
|
||||
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
|
||||
event = "Enable Biometric",
|
||||
params = mapOf("State" to state.value),
|
||||
)
|
||||
|
||||
class MainCurrencyChanged(currencyType: AnalyticsParam.CurrencyType) : MainScreen(
|
||||
event = "Main Currency Changed",
|
||||
params = mapOf("Currency Type" to currencyType.value),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
sealed class MyWallets(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("My Wallets", event, params) {
|
||||
|
||||
object MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
|
||||
object CardWasScanned : MyWallets(event = "Card Was Scanned")
|
||||
object WalletUnlockTapped : MyWallets(event = "Wallet Unlock Tapped")
|
||||
|
||||
object Button {
|
||||
object ScanNewCard : MyWallets(event = "Button - Scan New Card")
|
||||
object UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
|
||||
object EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
|
||||
object DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
|
||||
}
|
||||
}
|
||||
|
|
@ -73,4 +73,9 @@ sealed class Onboarding(
|
|||
class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened")
|
||||
class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
|
||||
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
|
||||
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
|
||||
category = "Onboarding / Biometric",
|
||||
event = "Enable Biometric",
|
||||
params = mapOf("State" to state.value),
|
||||
)
|
||||
}
|
||||
|
|
@ -58,16 +58,16 @@ sealed class Settings(
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : Settings("Settings / App Settings", event, params) {
|
||||
|
||||
sealed class FaceIDSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
|
||||
event = "Face ID Switcher Changed",
|
||||
class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
|
||||
event = "Save Wallet Switcher Changed",
|
||||
params = mapOf("State" to state.value),
|
||||
)
|
||||
|
||||
sealed class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
|
||||
class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
|
||||
event = "Save Access Code Switcher Changed",
|
||||
params = mapOf("State" to state.value),
|
||||
)
|
||||
|
||||
class ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication")
|
||||
object ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication")
|
||||
}
|
||||
}
|
||||
|
|
@ -32,10 +32,8 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
|
|||
AppScreen.Home,
|
||||
AppScreen.Welcome,
|
||||
-> {
|
||||
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
|
||||
if (navState?.backStack?.contains(screen) != true) {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
|
||||
}
|
||||
navState?.activity?.get()?.popBackTo(screen, inclusive = true)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
|
||||
}
|
||||
else -> {
|
||||
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
|
|||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.userWalletList.di.USER_WALLETS_BIOMETRIC_KEY_NAME
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
|
|
@ -119,16 +118,6 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
.map { CardDTO(it) }
|
||||
}
|
||||
|
||||
suspend fun unlockBiometricKeys(): CompletionResult<Unit> {
|
||||
return biometricManager.authenticate(
|
||||
mode = BiometricManager.AuthenticationMode.Keys(
|
||||
USER_WALLETS_BIOMETRIC_KEY_NAME,
|
||||
tangemSdk.config.userCodesBiometricKeyName,
|
||||
),
|
||||
)
|
||||
.map { /* no-op */ }
|
||||
}
|
||||
|
||||
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().save(
|
||||
cardIds = cardsIds,
|
||||
|
|
@ -137,20 +126,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
stringValue = accessCode,
|
||||
),
|
||||
)
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().clear()
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
|
|
@ -229,7 +208,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
|
||||
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
|
||||
return context.getString(stringResId, formatArgs)
|
||||
return context.getString(stringResId, *formatArgs)
|
||||
}
|
||||
|
||||
fun setAccessCodeRequestPolicy(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
||||
|
|
@ -12,6 +13,9 @@ import com.tangem.domain.common.util.UserWalletId
|
|||
* @param scanResponse [ScanResponse] of primary user's wallet card.
|
||||
* TODO: Replace with [com.tangem.domain.common.CardDTO]
|
||||
* @property cardId ID of user's wallet primary card
|
||||
* @property hasAccessCode Indicates if the user's wallet primary card has access code
|
||||
* @property isLocked Indicates if this primary card has no currency wallets
|
||||
* @property isSaved Indicates if this user wallet is saved
|
||||
* */
|
||||
data class UserWallet(
|
||||
val name: String,
|
||||
|
|
@ -23,5 +27,27 @@ data class UserWallet(
|
|||
val cardId: String
|
||||
get() = scanResponse.card.cardId
|
||||
|
||||
val hasAccessCode: Boolean
|
||||
get() = scanResponse.card.isAccessCodeSet
|
||||
|
||||
val isLocked: Boolean
|
||||
get() = scanResponse.card.wallets.isEmpty()
|
||||
|
||||
internal var isSaved: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! Workaround !!!
|
||||
*
|
||||
* Calculate same [UserWalletId] for twins instead
|
||||
* */
|
||||
fun UserWallet.isTwinnedWith(other: UserWallet): Boolean {
|
||||
if (!scanResponse.isTangemTwins()) 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
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ class CreateProductWalletTask(
|
|||
private val type: ProductType,
|
||||
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean = false
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean = false
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ScanProductTask(
|
|||
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean
|
||||
override val allowsRequestAccessCodeFromRepository: Boolean
|
||||
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
|
||||
|
||||
override fun run(
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
import com.tangem.operations.wallet.PurgeWalletCommand
|
||||
|
||||
class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
|
||||
class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRunnable<CreateWalletResponse> {
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
|
||||
|
|
@ -18,8 +17,11 @@ class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
|
|||
val card = session.environment.card
|
||||
val publicKey = card?.wallets?.firstOrNull()?.publicKey
|
||||
if (publicKey != null) {
|
||||
if (TwinsHelper.getTwinCardNumber(card.cardId) == TwinCardNumber.Second) {
|
||||
callback(CompletionResult.Failure(WrongTwinCard(TwinCardNumber.First)))
|
||||
val requiredTwinCardNumber = TwinsHelper.getTwinCardNumber(firstCardId)
|
||||
if (requiredTwinCardNumber != TwinsHelper.getTwinCardNumber(card.cardId)) {
|
||||
requiredTwinCardNumber?.let {
|
||||
callback(CompletionResult.Failure(WrongTwinCard(it)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
|
|
@ -15,6 +14,7 @@ import com.tangem.operations.wallet.PurgeWalletCommand
|
|||
|
||||
class CreateSecondTwinWalletTask(
|
||||
private val firstPublicKey: String,
|
||||
private val firstCardId: String,
|
||||
private val issuerKeys: KeyPair,
|
||||
private val preparingMessage: Message,
|
||||
private val creatingWalletMessage: Message,
|
||||
|
|
@ -24,8 +24,11 @@ class CreateSecondTwinWalletTask(
|
|||
val card = session.environment.card
|
||||
val publicKey = card?.wallets?.firstOrNull()?.publicKey
|
||||
if (publicKey != null) {
|
||||
if (TwinsHelper.getTwinCardNumber(card.cardId) == TwinCardNumber.First) {
|
||||
callback(CompletionResult.Failure(WrongTwinCard(TwinCardNumber.Second)))
|
||||
val currentTwinCardNumber = TwinsHelper.getTwinCardNumber(card.cardId)
|
||||
if (TwinsHelper.getTwinCardNumber(firstCardId) == currentTwinCardNumber) {
|
||||
currentTwinCardNumber?.pairNumber()?.let {
|
||||
callback(CompletionResult.Failure(WrongTwinCard(it)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.KeyPair
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.tap.common.AssetReader
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
|
@ -30,7 +30,7 @@ class TwinCardsManager(
|
|||
|
||||
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
|
||||
val response = tangemSdkManager.runTaskAsync(
|
||||
runnable = CreateFirstTwinWalletTask(),
|
||||
runnable = CreateFirstTwinWalletTask(firstCardId),
|
||||
cardId = firstCardId,
|
||||
initialMessage = message,
|
||||
)
|
||||
|
|
@ -48,6 +48,7 @@ class TwinCardsManager(
|
|||
): CompletionResult<CreateWalletResponse> {
|
||||
val task = CreateSecondTwinWalletTask(
|
||||
firstPublicKey = currentCardPublicKey!!,
|
||||
firstCardId = firstCardId,
|
||||
issuerKeys = issuerKeyPair,
|
||||
preparingMessage = preparingMessage,
|
||||
creatingWalletMessage = creatingWalletMessage,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricU
|
|||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.*
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.ByteArrayKeyAdapter
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.CardBackupStatusAdapter
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.ExtendedPublicKeysMapAdapter
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.ScanResponseDerivedKeysMapAdapter
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.WalletDerivedKeysMapAdapter
|
||||
|
||||
const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
|
||||
const val USER_WALLETS_BIOMETRIC_KEY_NAME = "user_wallets"
|
||||
private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
|
||||
|
||||
fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
||||
context: Context,
|
||||
|
|
@ -43,7 +46,6 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
|||
)
|
||||
|
||||
val keysRepository = BiometricUserWalletsKeysRepository(
|
||||
biometricKeyName = USER_WALLETS_BIOMETRIC_KEY_NAME,
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
biometricManager = tangemSdkManager.biometricManager,
|
||||
|
|
@ -61,7 +63,6 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
|||
)
|
||||
|
||||
return BiometricUserWalletsListManager(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
keysRepository = keysRepository,
|
||||
publicInformationRepository = publicInformationRepository,
|
||||
sensitiveInformationRepository = sensitiveInformationRepository,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ package com.tangem.tap.domain.userWalletList.implementation
|
|||
|
||||
import com.tangem.common.*
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
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
|
||||
|
|
@ -20,7 +19,6 @@ import timber.log.Timber
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class BiometricUserWalletsListManager(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val keysRepository: UserWalletsKeysRepository,
|
||||
private val publicInformationRepository: UserWalletsPublicInformationRepository,
|
||||
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
|
||||
|
|
@ -64,10 +62,22 @@ internal class BiometricUserWalletsListManager(
|
|||
|
||||
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
state.update { prevState ->
|
||||
userWallet.isSaved = false
|
||||
// If the previous state contains a saved user wallet with the same ID, it is also saved
|
||||
userWallet.isSaved = prevState.wallets.any {
|
||||
it.walletId == userWallet.walletId && it.isSaved
|
||||
}
|
||||
|
||||
val newEncryptionKeys = prevState.encryptionKeys
|
||||
.plus(UserWalletEncryptionKey(userWallet))
|
||||
.distinctBy { it.walletId }
|
||||
val newUserWallets = prevState.wallets
|
||||
.plus(userWallet)
|
||||
.distinctBy { it.walletId }
|
||||
|
||||
prevState.copy(
|
||||
encryptionKeys = listOf(UserWalletEncryptionKey(userWallet)),
|
||||
wallets = listOf(userWallet),
|
||||
encryptionKeys = newEncryptionKeys,
|
||||
wallets = newUserWallets,
|
||||
selectedWalletId = userWallet.walletId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -75,15 +85,13 @@ internal class BiometricUserWalletsListManager(
|
|||
.map {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedWalletId = userWallet.walletId,
|
||||
isLocked = prevState.wallets.size != 1,
|
||||
isLocked = prevState.wallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun lock() {
|
||||
tangemSdkManager.biometricManager.unauthenticate()
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = emptyList(),
|
||||
|
|
@ -116,20 +124,23 @@ internal class BiometricUserWalletsListManager(
|
|||
): CompletionResult<Unit> = withUnlock {
|
||||
val isWalletSaved = state.value.wallets
|
||||
.filter { it.isSaved }
|
||||
.flatMap(UserWallet::cardsInWallet)
|
||||
.contains(userWallet.cardId)
|
||||
.any {
|
||||
// Workaround, check [UserWallet.isTwinnedWith]
|
||||
it.cardsInWallet.contains(userWallet.cardId) || it.isTwinnedWith(userWallet)
|
||||
}
|
||||
|
||||
if (isWalletSaved && !canOverride) {
|
||||
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
|
||||
} else {
|
||||
keysRepository.save(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
.doOnSuccess { keys ->
|
||||
val newEncryptionKeys = state.value.encryptionKeys
|
||||
.plus(UserWalletEncryptionKey(userWallet))
|
||||
.distinctBy { it.walletId }
|
||||
|
||||
keysRepository.store(newEncryptionKeys)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
|
||||
encryptionKeys = newEncryptionKeys,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -143,28 +154,25 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
if (state.value.isLocked) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val walletIdsToRemove = state.value.wallets
|
||||
.map { it.walletId }
|
||||
.filter { it in walletIds }
|
||||
val remainingEncryptionKeys = state.value.encryptionKeys
|
||||
.filter { it.walletId !in walletIdsToRemove }
|
||||
|
||||
changeSelectedWalletIfNeeded(walletIdsToRemove)
|
||||
|
||||
return sensitiveInformationRepository.delete(walletIdsToRemove)
|
||||
.flatMap { publicInformationRepository.delete(walletIdsToRemove) }
|
||||
.flatMap { keysRepository.delete(walletIdsToRemove) }
|
||||
.map { keys ->
|
||||
.flatMap { keysRepository.store(remainingEncryptionKeys) }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = keys,
|
||||
encryptionKeys = remainingEncryptionKeys,
|
||||
wallets = prevState.wallets.filter { it.walletId !in walletIdsToRemove },
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { loadModels() }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
|
|
@ -175,12 +183,11 @@ internal class BiometricUserWalletsListManager(
|
|||
.flatMap { keysRepository.clear() }
|
||||
.map {
|
||||
selectedUserWalletRepository.set(null)
|
||||
tangemSdkManager.biometricManager.unauthenticate()
|
||||
state.update { State() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> = withUnlock {
|
||||
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
state.value.wallets.first { it.walletId == walletId }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
|
||||
internal interface UserWalletsKeysRepository {
|
||||
suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun save(walletId: UserWalletId, encryptionKey: ByteArray): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -6,24 +6,19 @@ import com.squareup.moshi.Types
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.biometric.BiometricManager
|
||||
import com.tangem.common.biometric.BiometricStorage
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
|
||||
internal class BiometricUserWalletsKeysRepository(
|
||||
biometricKeyName: String,
|
||||
moshi: Moshi,
|
||||
secureStorage: SecureStorage,
|
||||
biometricManager: BiometricManager,
|
||||
) : UserWalletsKeysRepository {
|
||||
private val biometricStorage = BiometricStorage(
|
||||
biometricKeyName = biometricKeyName,
|
||||
biometricManager = biometricManager,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
|
|
@ -41,50 +36,16 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun save(
|
||||
walletId: UserWalletId,
|
||||
encryptionKey: ByteArray,
|
||||
): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.flatMap { keys ->
|
||||
if (keys.any { it.walletId == walletId }) {
|
||||
return@flatMap CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val encodedKeys = keys.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(UserWalletEncryptionKey(walletId, encryptionKey)) {
|
||||
it.walletId == walletId
|
||||
}
|
||||
}
|
||||
.encode()
|
||||
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
override suspend fun store(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<Unit> {
|
||||
return biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encryptionKeys.encode(),
|
||||
)
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.map { keys ->
|
||||
val keysToRemove = keys.filter { it.walletId in walletIds }.toSet()
|
||||
(keys - keysToRemove).encode()
|
||||
}
|
||||
.flatMap { encodedKeys ->
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return biometricStorage.delete(key = StorageKey.WalletEncryptionKeys.name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,14 @@ internal class DefaultWalletStoresManager(
|
|||
.build(),
|
||||
)
|
||||
}
|
||||
.flatMapOnFailure { error ->
|
||||
when (error) {
|
||||
is WalletStoresError.WalletManagerNotCreated,
|
||||
is WalletStoresError.UpdateWalletManagerError,
|
||||
-> CompletionResult.Success(Unit)
|
||||
else -> CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal data class State(
|
||||
|
|
|
|||
|
|
@ -121,8 +121,12 @@ class DetailsMiddleware {
|
|||
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
|
||||
if (selectedUserWallet != null) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(selectedUserWallet)
|
||||
if (userWalletsListManager.isLockedSync) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(selectedUserWallet)
|
||||
}
|
||||
} else {
|
||||
userWalletsListManager.lock()
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
|
|
@ -232,6 +236,7 @@ class DetailsMiddleware {
|
|||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
|
||||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||
}
|
||||
|
||||
|
|
@ -268,14 +273,18 @@ class DetailsMiddleware {
|
|||
Timber.e(error, "Wallet saving failed")
|
||||
}
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
|
||||
|
||||
preferencesStorage.shouldShowSaveUserWalletScreen = false
|
||||
preferencesStorage.shouldSaveUserWallets = true
|
||||
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = true,
|
||||
),
|
||||
)
|
||||
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
|
|
@ -284,22 +293,29 @@ class DetailsMiddleware {
|
|||
userWalletsListManager.clear()
|
||||
.flatMap { walletStoresManager.clear() }
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
|
||||
|
||||
preferencesStorage.shouldSaveUserWallets = false
|
||||
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = false,
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAccessCodes(state: DetailsState) {
|
||||
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
|
||||
|
||||
preferencesStorage.shouldSaveAccessCodes = true
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = state.scanResponse?.card?.isAccessCodeSet == true,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
|
|
@ -311,10 +327,13 @@ class DetailsMiddleware {
|
|||
private suspend fun deleteSavedAccessCodes() {
|
||||
tangemSdkManager.clearSavedUserCodes()
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off))
|
||||
|
||||
preferencesStorage.shouldSaveAccessCodes = false
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = false,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
|
|
|
|||
|
|
@ -93,9 +93,14 @@ private fun readCard() = scope.launch {
|
|||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
|
||||
store.onCardScanned(scanResponse)
|
||||
}
|
||||
.doOnSuccess {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
userWallet.hasAccessCode,
|
||||
)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
.doOnResult {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
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.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -55,6 +59,18 @@ class OnboardingHelper {
|
|||
backupCardsIds: List<String>? = null,
|
||||
) {
|
||||
when {
|
||||
// When should save user wallets but manager is locked, then unlock manager with card
|
||||
preferencesStorage.shouldSaveUserWallets &&
|
||||
userWalletsListManager.isLockedSync -> scope.launch {
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
|
||||
userWalletsListManager.unlockWithCard(userWallet)
|
||||
.doOnSuccess {
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
// When should save user wallets, then save card without navigate to save wallet screen
|
||||
preferencesStorage.shouldSaveUserWallets -> scope.launch {
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
|
|
@ -65,6 +81,8 @@ class OnboardingHelper {
|
|||
)
|
||||
store.dispatchOnMain(SaveWalletAction.Save)
|
||||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
tangemSdkManager.canUseBiometry &&
|
||||
preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch {
|
||||
delay(timeMillis = 1_200)
|
||||
|
|
@ -77,6 +95,7 @@ class OnboardingHelper {
|
|||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
|
||||
}
|
||||
// If device has no biometry and save wallet screen has been shown, then go through old scenario
|
||||
else -> scope.launch {
|
||||
store.onCardScanned(scanResponse)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,7 +234,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
|
||||
when (action) {
|
||||
is BackupAction.StartBackup -> {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
|
||||
Analytics.send(Onboarding.Backup.Started())
|
||||
backupService.discardSavedBackup()
|
||||
val primaryCard = scanResponse?.primaryCard
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -36,9 +40,9 @@ internal class SaveWalletMiddleware {
|
|||
is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state)
|
||||
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
|
||||
is SaveWalletAction.Dismiss -> dismiss(state)
|
||||
is SaveWalletAction.Save.Success,
|
||||
is SaveWalletAction.ProvideBackupInfo,
|
||||
is SaveWalletAction.Dismiss,
|
||||
is SaveWalletAction.CloseError,
|
||||
is SaveWalletAction.Save.Error,
|
||||
is SaveWalletAction.EnrollBiometrics,
|
||||
|
|
@ -74,6 +78,13 @@ internal class SaveWalletMiddleware {
|
|||
?: store.state.globalState.scanResponse
|
||||
?: return
|
||||
|
||||
if (state.backupInfo != null) {
|
||||
// TODO: Remove after onboarding refactoring
|
||||
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
|
||||
} else {
|
||||
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val userWallet = UserWalletBuilder(scanResponse)
|
||||
.backupCardsIds(state.backupInfo?.backupCardsIds)
|
||||
|
|
@ -83,6 +94,7 @@ internal class SaveWalletMiddleware {
|
|||
|
||||
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
|
||||
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }
|
||||
.flatMap { userWalletsListManager.selectWallet(userWallet.walletId) }
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
|
||||
}
|
||||
|
|
@ -93,21 +105,29 @@ internal class SaveWalletMiddleware {
|
|||
preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet ||
|
||||
preferencesStorage.shouldSaveAccessCodes
|
||||
|
||||
val isSavedWalletSelected =
|
||||
userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId
|
||||
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
userWallet.hasAccessCode,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(SaveWalletAction.Save.Success)
|
||||
|
||||
if (isSavedWalletSelected) {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismiss(state: SaveWalletState) {
|
||||
if (state.backupInfo != null) {
|
||||
// TODO: Remove after onboarding refactoring
|
||||
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off))
|
||||
} else {
|
||||
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveWalletWasShown() {
|
||||
preferencesStorage.shouldShowSaveUserWalletScreen = false
|
||||
}
|
||||
|
|
@ -118,13 +138,10 @@ internal class SaveWalletMiddleware {
|
|||
): CompletionResult<Unit> {
|
||||
return when {
|
||||
accessCode != null -> {
|
||||
tangemSdkManager.unlockBiometricKeys()
|
||||
.flatMap {
|
||||
tangemSdkManager.saveAccessCode(
|
||||
accessCode = accessCode,
|
||||
cardsIds = cardsInWallet,
|
||||
)
|
||||
}
|
||||
tangemSdkManager.saveAccessCode(
|
||||
accessCode = accessCode,
|
||||
cardsIds = cardsInWallet,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
CompletionResult.Success(Unit)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import android.view.Menu
|
|||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.accompanist.appcompattheme.AppCompatTheme
|
||||
|
|
@ -22,6 +20,8 @@ import com.tangem.tap.common.extensions.dispatchNotification
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.BaseFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.tokens.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.tokens.redux.TokensAction
|
||||
|
|
@ -40,8 +40,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
||||
StoreSubscriber<TokensState> {
|
||||
class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
|
||||
|
||||
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
|
||||
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
|
||||
|
|
@ -51,33 +50,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
Analytics.send(ManageTokens.ScreenOpened())
|
||||
|
||||
activity?.onBackPressedDispatcher?.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
}
|
||||
},
|
||||
)
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.slide_right)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.tokensState == newState.tokensState
|
||||
}.select { it.tokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
addBackPressHandler(this)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
|
||||
|
|
@ -96,11 +69,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
}
|
||||
|
||||
val onLoadMore = {
|
||||
store.dispatch(
|
||||
TokensAction.LoadMore(
|
||||
scanResponse = store.state.globalState.scanResponse,
|
||||
),
|
||||
)
|
||||
store.dispatch(TokensAction.LoadMore(scanResponse = store.state.globalState.scanResponse))
|
||||
}
|
||||
|
||||
cvCurrencies.setContent {
|
||||
|
|
@ -115,24 +84,23 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
}
|
||||
}
|
||||
|
||||
override fun newState(state: TokensState) {
|
||||
if (activity == null || view == null) return
|
||||
val toolbarTitle =
|
||||
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
|
||||
tokensState.value = state
|
||||
binding.toolbar.title = getString(toolbarTitle)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state
|
||||
.skipRepeats { oldState, newState -> oldState.tokensState == newState.tokensState }
|
||||
.select { it.tokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_search -> true
|
||||
R.id.menu_navigate_add_custom_token -> {
|
||||
Analytics.send(ManageTokens.ButtonCustomToken())
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
|
|
@ -156,25 +124,50 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
return super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_search -> true
|
||||
R.id.menu_navigate_add_custom_token -> {
|
||||
Analytics.send(ManageTokens.ButtonCustomToken())
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureTransitions() {
|
||||
super.configureTransitions()
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.slide_right)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
super.handleOnBackPressed()
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(TokensAction.ResetState)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun newState(state: TokensState) {
|
||||
if (activity == null || view == null) return
|
||||
val toolbarTitle =
|
||||
if (state.allowToAdd) R.string.main_manage_tokens else R.string.search_tokens_title
|
||||
tokensState.value = state
|
||||
binding.toolbar.title = getString(toolbarTitle)
|
||||
}
|
||||
}
|
||||
|
||||
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
|
||||
val watcher =
|
||||
setOnQueryTextListener(
|
||||
object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
val watcher = setOnQueryTextListener(
|
||||
object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
trySend(newText ?: "")
|
||||
return false
|
||||
}
|
||||
},
|
||||
)
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
trySend(newText ?: "")
|
||||
return false
|
||||
}
|
||||
},
|
||||
)
|
||||
awaitClose { (watcher) }
|
||||
}
|
||||
|
|
@ -83,25 +83,21 @@ fun CurrenciesScreen(
|
|||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
if (tokensState.value.allowToAdd) SaveChangesButton(isKeyboardOpen) {
|
||||
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
|
||||
if (tokensState.value.allowToAdd) {
|
||||
SaveChangesButton(isKeyboardOpen) {
|
||||
onSaveChanges(addedTokensState.value, addedBlockchainsState.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) {
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = Color(0xFF1ACE80),
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = Color(0xFF1ACE80))
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
|
|
@ -148,21 +144,19 @@ private fun toggleBlockchain(
|
|||
currencySymbol = blockchain.currency,
|
||||
),
|
||||
)
|
||||
} else if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
onOk = {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
if (isAddedOnMainScreen) {
|
||||
store.dispatchDialogShow(
|
||||
WalletDialog.RemoveWalletDialog(
|
||||
currencyTitle = blockchain.name,
|
||||
onOk = {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOff()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
}
|
||||
analyticsCurrencyTypeParam.sendOff()
|
||||
addedBlockchainsState.removeAndNotify(blockchain)
|
||||
}
|
||||
} else {
|
||||
analyticsCurrencyTypeParam.sendOn()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.domain.tokens.Contract
|
||||
import com.tangem.tap.domain.tokens.Currency
|
||||
import com.tangem.tap.features.tokens.redux.ContractAddress
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
|
|
@ -30,22 +31,20 @@ fun CurrencyExpandedContent(
|
|||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
val blockchains = currency.contracts.map { it.blockchain }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
blockchains.mapIndexed { index, blockchain ->
|
||||
val contract = currency.contracts.firstOrNull { it.blockchain == blockchain }
|
||||
val currencyContract = currency.contracts
|
||||
.firstOrNull { it.blockchain == blockchain }
|
||||
?: return@mapIndexed
|
||||
|
||||
val added = if (contract.address != null) {
|
||||
addedTokens.map { it.token.contractAddress }.contains(contract.address)
|
||||
val added = if (currencyContract.address != null) {
|
||||
addedTokens.any(currencyContract::isInclude)
|
||||
} else {
|
||||
addedBlockchains.contains(blockchain)
|
||||
}
|
||||
NetworkItem(
|
||||
currency = currency,
|
||||
contract = contract,
|
||||
contract = currencyContract,
|
||||
blockchain = blockchain,
|
||||
allowToAdd = allowToAdd,
|
||||
added = added,
|
||||
|
|
@ -57,4 +56,7 @@ fun CurrencyExpandedContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Contract.isInclude(addedToken: TokenWithBlockchain): Boolean =
|
||||
address == addedToken.token.contractAddress && blockchain == addedToken.blockchain
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
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
|
||||
|
|
@ -181,6 +182,7 @@ class MultiWalletMiddleware {
|
|||
selectedWallet: UserWallet,
|
||||
state: WalletState?,
|
||||
) = scope.launch {
|
||||
Analytics.send(MainScreen.CardWasScanned())
|
||||
ScanCardProcessor.scan(
|
||||
cardId = selectedWallet.cardId,
|
||||
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext
|
|||
import com.tangem.operations.attestation.Attestation
|
||||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.MainScreen
|
||||
import com.tangem.tap.common.analytics.events.Token
|
||||
import com.tangem.tap.common.extensions.copyToClipboard
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
|
|
@ -349,9 +350,11 @@ class WalletMiddleware {
|
|||
private fun changeWallet() {
|
||||
when {
|
||||
userWalletsListManager.hasSavedUserWallets -> {
|
||||
Analytics.send(MainScreen.ButtonMyWallets())
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
|
||||
}
|
||||
else -> {
|
||||
Analytics.send(MainScreen.ButtonScanCard())
|
||||
store.dispatch(WalletAction.Scan)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class MultiWalletView : WalletView() {
|
|||
btnAddToken.show()
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated() {
|
||||
setupWalletsRecyclerView()
|
||||
}
|
||||
|
|
@ -69,7 +68,7 @@ class MultiWalletView : WalletView() {
|
|||
val fragment = fragment ?: return
|
||||
val binding = binding ?: return
|
||||
|
||||
handleTotalBalance(binding, state.totalBalance, state.state)
|
||||
handleTotalBalance(binding, state.totalBalance, state.state, state.walletsData.size)
|
||||
handleBackupWarning(binding, state.showBackupWarning)
|
||||
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
|
||||
setupWalletCardNumber(binding, state.walletCardsCount)
|
||||
|
|
@ -137,29 +136,34 @@ class MultiWalletView : WalletView() {
|
|||
binding: FragmentWalletBinding,
|
||||
totalBalance: TotalBalance?,
|
||||
progressState: ProgressState,
|
||||
walletsCount: Int,
|
||||
) = with(binding.lCardTotalBalance) {
|
||||
if (totalBalance == null) {
|
||||
veilBalance.animateVisibility(show = true)
|
||||
root.isVisible = progressState == ProgressState.Loading
|
||||
if (walletsCount == 0) {
|
||||
root.isVisible = false
|
||||
} else {
|
||||
root.isVisible = true
|
||||
if (totalBalance == null) {
|
||||
veilBalance.animateVisibility(show = true)
|
||||
root.isVisible = progressState == ProgressState.Loading
|
||||
} else {
|
||||
root.isVisible = true
|
||||
|
||||
// Skip changes when on refreshing state
|
||||
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
|
||||
return@with
|
||||
}
|
||||
// Skip changes when on refreshing state
|
||||
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
|
||||
return@with
|
||||
}
|
||||
|
||||
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
|
||||
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
|
||||
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
|
||||
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
|
||||
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
|
||||
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
|
||||
|
||||
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
|
||||
currencySymbol = totalBalance.fiatCurrency.symbol,
|
||||
)
|
||||
tvCurrencyName.text = totalBalance.fiatCurrency.code
|
||||
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
|
||||
currencySymbol = totalBalance.fiatCurrency.symbol,
|
||||
)
|
||||
tvCurrencyName.text = totalBalance.fiatCurrency.code
|
||||
|
||||
tvCurrencyName.setOnClickListener {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
tvCurrencyName.setOnClickListener {
|
||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ data class UserWalletModel(
|
|||
val artworkUrl: String,
|
||||
val type: Type,
|
||||
val fiatBalance: TotalFiatBalance,
|
||||
val isLocked: Boolean,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data class SingleCurrency(
|
||||
val blockchainName: String? = null,
|
||||
val blockchainName: String,
|
||||
) : Type
|
||||
|
||||
data class MultiCurrency(
|
||||
val cardsInWallet: Int,
|
||||
val tokensCount: Int = 0,
|
||||
val tokensCount: Int,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,10 @@ internal sealed interface WalletSelectorAction : Action {
|
|||
val walletId: String,
|
||||
) : WalletSelectorAction
|
||||
|
||||
data class UnlockWalletWithCard(
|
||||
val walletId: String,
|
||||
) : WalletSelectorAction
|
||||
|
||||
data class RenameWallet(
|
||||
val walletId: String,
|
||||
val newName: String,
|
||||
|
|
@ -56,5 +60,6 @@ internal sealed interface WalletSelectorAction : Action {
|
|||
) : WalletSelectorAction
|
||||
|
||||
data class HandleError(val error: TangemError) : WalletSelectorAction
|
||||
|
||||
object CloseError : WalletSelectorAction
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import com.tangem.common.flatMap
|
|||
import com.tangem.common.map
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.MyWallets
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -24,7 +26,6 @@ import com.tangem.tap.tangemSdkManager
|
|||
import com.tangem.tap.totalFiatBalanceCalculator
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
|
@ -60,6 +61,9 @@ internal class WalletSelectorMiddleware {
|
|||
is WalletSelectorAction.SelectWallet -> {
|
||||
selectWallet(action.walletId)
|
||||
}
|
||||
is WalletSelectorAction.UnlockWalletWithCard -> {
|
||||
unlockWalletWithCard(action.walletId)
|
||||
}
|
||||
is WalletSelectorAction.RemoveWallets -> {
|
||||
removeWallets(action.walletIdsToRemove, state)
|
||||
}
|
||||
|
|
@ -105,6 +109,8 @@ internal class WalletSelectorMiddleware {
|
|||
}
|
||||
|
||||
private fun unlockWalletsWithBiometry() {
|
||||
Analytics.send(MyWallets.Button.UnlockWithBiometrics)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.unlockWithBiometry()
|
||||
.doOnFailure { error ->
|
||||
|
|
@ -117,6 +123,8 @@ internal class WalletSelectorMiddleware {
|
|||
}
|
||||
|
||||
private fun addWallet() = scope.launch {
|
||||
Analytics.send(MyWallets.Button.ScanNewCard)
|
||||
|
||||
scanCardInternal { scanResponse ->
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
|
||||
|
|
@ -125,15 +133,13 @@ internal class WalletSelectorMiddleware {
|
|||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
|
||||
}
|
||||
.doOnSuccess {
|
||||
val selectedWallet = userWalletsListManager.selectedUserWallet.first()
|
||||
val isSavedWalletSelected = userWallet == selectedWallet
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
Analytics.send(MyWallets.CardWasScanned)
|
||||
|
||||
if (isSavedWalletSelected) {
|
||||
updateAccessCodeRequestPolicy(selectedWallet)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(selectedWallet)
|
||||
}
|
||||
userWalletsListManager.selectWallet(userWallet.walletId)
|
||||
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
|
||||
updateAccessCodeRequestPolicy(userWallet)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +158,43 @@ internal class WalletSelectorMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun unlockWalletWithCard(id: String) {
|
||||
scope.launch {
|
||||
userWalletsListManager.get(UserWalletId(id))
|
||||
.flatMap { userWallet ->
|
||||
updateUserWalletWithScannedCard(userWallet)
|
||||
}
|
||||
.flatMap { updatedUserWallet ->
|
||||
unlockUserWallet(updatedUserWallet)
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<UserWallet> {
|
||||
return tangemSdkManager.scanCard(userWallet.cardId)
|
||||
.map { scannedCard ->
|
||||
userWallet.copy(
|
||||
scanResponse = userWallet.scanResponse.copy(
|
||||
card = scannedCard,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockUserWallet(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return userWalletsListManager.unlockWithCard(userWallet)
|
||||
.doOnSuccess {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeWallets(walletIdsToRemove: List<String>, state: WalletSelectorState) {
|
||||
Analytics.send(MyWallets.Button.DeleteWalletTapped)
|
||||
|
||||
scope.launch {
|
||||
when (walletIdsToRemove.size) {
|
||||
state.wallets.size -> clearUserWallets()
|
||||
|
|
@ -165,6 +207,8 @@ internal class WalletSelectorMiddleware {
|
|||
}
|
||||
|
||||
private fun renameWallet(walletId: String, newName: String) {
|
||||
Analytics.send(MyWallets.Button.EditWalletTapped)
|
||||
|
||||
scope.launch {
|
||||
userWalletsListManager.get(walletId = UserWalletId(walletId))
|
||||
.map { it.copy(name = newName) }
|
||||
|
|
@ -221,7 +265,7 @@ internal class WalletSelectorMiddleware {
|
|||
private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
userWallet.scanResponse.card.isAccessCodeSet,
|
||||
userWallet.hasAccessCode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -233,13 +277,7 @@ internal class WalletSelectorMiddleware {
|
|||
is UserWalletModel.Type.MultiCurrency -> type.copy(
|
||||
tokensCount = walletStores.flatMap { it.walletsData }.size,
|
||||
)
|
||||
is UserWalletModel.Type.SingleCurrency -> type.copy(
|
||||
blockchainName = walletStores
|
||||
.firstOrNull()
|
||||
?.blockchainNetwork
|
||||
?.blockchain
|
||||
?.fullName,
|
||||
)
|
||||
is UserWalletModel.Type.SingleCurrency -> type
|
||||
},
|
||||
fiatBalance = totalFiatBalanceCalculator.calculate(
|
||||
prevAmount = fiatBalance.amount,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ internal object WalletSelectorReducer {
|
|||
)
|
||||
is WalletSelectorAction.WalletStoresChanged,
|
||||
is WalletSelectorAction.SelectWallet,
|
||||
is WalletSelectorAction.UnlockWalletWithCard,
|
||||
is WalletSelectorAction.RemoveWallets,
|
||||
is WalletSelectorAction.RenameWallet,
|
||||
-> state
|
||||
|
|
@ -66,10 +67,14 @@ internal object WalletSelectorReducer {
|
|||
return this.map { userWallet ->
|
||||
prevWallets
|
||||
.find { it.id == userWallet.walletId.stringValue }
|
||||
?.copy(
|
||||
name = userWallet.name,
|
||||
artworkUrl = userWallet.artworkUrl,
|
||||
)
|
||||
?.let {
|
||||
it.copy(
|
||||
name = userWallet.name,
|
||||
artworkUrl = userWallet.artworkUrl,
|
||||
isLocked = userWallet.isLocked,
|
||||
type = userWallet.getType(prevType = it.type),
|
||||
)
|
||||
}
|
||||
?: with(userWallet) {
|
||||
UserWalletModel(
|
||||
id = walletId.stringValue,
|
||||
|
|
@ -77,6 +82,7 @@ internal object WalletSelectorReducer {
|
|||
artworkUrl = artworkUrl,
|
||||
type = getType(),
|
||||
fiatBalance = TotalFiatBalance.Loading,
|
||||
isLocked = isLocked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -90,15 +96,18 @@ internal object WalletSelectorReducer {
|
|||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.getType(): UserWalletModel.Type {
|
||||
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
|
||||
return if (scanResponse.card.isMultiwalletAllowed) {
|
||||
UserWalletModel.Type.MultiCurrency(
|
||||
cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active)
|
||||
?.cardCount?.inc()
|
||||
?: 1,
|
||||
tokensCount = (prevType as? UserWalletModel.Type.MultiCurrency)?.tokensCount ?: 0,
|
||||
)
|
||||
} else {
|
||||
UserWalletModel.Type.SingleCurrency()
|
||||
UserWalletModel.Type.SingleCurrency(
|
||||
blockchainName = scanResponse.getBlockchain().fullName,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ private fun List<UserWalletModel>.toUiModels(
|
|||
name = name,
|
||||
imageUrl = artworkUrl,
|
||||
balance = balance,
|
||||
isLocked = isLocked,
|
||||
cardsInWallet = type.cardsInWallet,
|
||||
tokensCount = type.tokensCount,
|
||||
)
|
||||
|
|
@ -68,6 +69,7 @@ private fun List<UserWalletModel>.toUiModels(
|
|||
name = name,
|
||||
imageUrl = artworkUrl,
|
||||
balance = balance,
|
||||
isLocked = isLocked,
|
||||
tokenName = type.blockchainName ?: "—",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.features.walletSelector.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -14,6 +16,8 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
|||
import androidx.fragment.app.viewModels
|
||||
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.MyWallets
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent
|
||||
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
|
||||
|
|
@ -22,6 +26,12 @@ import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
|
|||
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
|
||||
private val viewModel by viewModels<WalletSelectorViewModel>()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
Analytics.send(MyWallets.MyWalletsScreenOpened)
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun provideState(): State<WalletSelectorScreenState> {
|
||||
return viewModel.state.collectAsState()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
|
|||
|
||||
fun walletClicked(walletId: String) = with(state.value) {
|
||||
when {
|
||||
isLocked -> Unit
|
||||
isLocked -> {
|
||||
store.dispatch(WalletSelectorAction.UnlockWalletWithCard(walletId))
|
||||
}
|
||||
editingWalletsIds.isNotEmpty() && !editingWalletsIds.contains(walletId) -> {
|
||||
editWallet(walletId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal object MockData {
|
|||
),
|
||||
name = "Wallet",
|
||||
imageUrl = "https://app.tangem.com/cards/card_default.png",
|
||||
isLocked = false,
|
||||
tokensCount = 12,
|
||||
cardsInWallet = 3,
|
||||
)
|
||||
|
|
@ -26,6 +27,7 @@ internal object MockData {
|
|||
),
|
||||
name = "Wallet",
|
||||
imageUrl = "https://app.tangem.com/cards/card_default.png",
|
||||
isLocked = false,
|
||||
tokenName = "Ethereum",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ internal fun WalletSelectorScreenContent(
|
|||
singleCurrencyWallets = state.singleCurrencyWallets,
|
||||
selectedWalletId = state.selectedWalletId,
|
||||
checkedWalletIds = state.editingWalletsIds,
|
||||
isLocked = state.isLocked,
|
||||
onWalletClick = onWalletClick,
|
||||
onWalletLongClick = onWalletLongClick,
|
||||
)
|
||||
|
|
@ -141,7 +140,6 @@ private fun WalletsList(
|
|||
singleCurrencyWallets: List<UserWalletItem>,
|
||||
selectedWalletId: String?,
|
||||
checkedWalletIds: List<String>,
|
||||
isLocked: Boolean,
|
||||
onWalletClick: (walletId: String) -> Unit,
|
||||
onWalletLongClick: (walletId: String) -> Unit,
|
||||
) {
|
||||
|
|
@ -166,7 +164,6 @@ private fun WalletsList(
|
|||
wallet = wallet,
|
||||
isSelected = wallet.id == selectedWalletId,
|
||||
isChecked = wallet.id in checkedWalletIds,
|
||||
isLocked = isLocked && wallet.id != selectedWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ internal fun WalletItem(
|
|||
wallet: UserWalletItem,
|
||||
isSelected: Boolean,
|
||||
isChecked: Boolean,
|
||||
isLocked: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
|
|
@ -59,7 +58,7 @@ internal fun WalletItem(
|
|||
SpacerW6()
|
||||
TokensInfo(
|
||||
modifier = Modifier.weight(weight = .4f),
|
||||
isLocked = isLocked,
|
||||
isLocked = wallet.isLocked,
|
||||
balance = wallet.balance,
|
||||
tokensCount = (wallet as? MultiCurrencyUserWalletItem)?.tokensCount,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
package com.tangem.tap.features.walletSelector.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Immutable
|
||||
internal sealed interface UserWalletItem {
|
||||
val id: String
|
||||
val name: String
|
||||
val imageUrl: String
|
||||
val balance: Balance
|
||||
val isLocked: Boolean
|
||||
|
||||
val headerText: TextReference
|
||||
get() = when (this) {
|
||||
|
|
@ -17,7 +16,6 @@ internal sealed interface UserWalletItem {
|
|||
is SingleCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_single_header)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Balance(
|
||||
val amount: String,
|
||||
val isLoading: Boolean,
|
||||
|
|
@ -29,6 +27,7 @@ internal data class MultiCurrencyUserWalletItem(
|
|||
override val name: String,
|
||||
override val imageUrl: String,
|
||||
override val balance: UserWalletItem.Balance,
|
||||
override val isLocked: Boolean,
|
||||
val tokensCount: Int,
|
||||
val cardsInWallet: Int,
|
||||
) : UserWalletItem
|
||||
|
|
@ -38,5 +37,6 @@ internal data class SingleCurrencyUserWalletItem(
|
|||
override val name: String,
|
||||
override val imageUrl: String,
|
||||
override val balance: UserWalletItem.Balance,
|
||||
override val isLocked: Boolean,
|
||||
val tokenName: String,
|
||||
) : UserWalletItem
|
||||
|
|
@ -61,7 +61,7 @@ internal class WelcomeMiddleware {
|
|||
if (selectedUserWallet != null) {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
selectedUserWallet.scanResponse.card.isAccessCodeSet,
|
||||
selectedUserWallet.hasAccessCode,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
|
|
@ -78,14 +78,12 @@ internal class WelcomeMiddleware {
|
|||
scanCardInternal { scanResponse ->
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
|
||||
userWalletsListManager.unlockWithCard(userWallet)
|
||||
.doOnFailure { error ->
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
|
||||
}
|
||||
.doOnSuccess {
|
||||
tangemSdkManager.setAccessCodeRequestPolicy(
|
||||
useBiometricsForAccessCode = false,
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ object Versions {
|
|||
|
||||
// region Tangem
|
||||
const val tangemBlockchainSdk = "develop-141"
|
||||
const val tangemCardSgk = "develop-171"
|
||||
|
||||
const val tangemCardSgk = "develop-173"
|
||||
// endregion Tangem
|
||||
|
||||
// region Testing
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface TangemTechApi {
|
|||
@GET("coins")
|
||||
suspend fun coins(
|
||||
@Query("contractAddress") contractAddress: String? = null,
|
||||
@Query("exchangeable") exchangeable: Boolean? = false,
|
||||
@Query("exchangeable") exchangeable: Boolean? = null,
|
||||
@Query("networkIds") networkIds: String? = null,
|
||||
@Query("active") active: Boolean? = null,
|
||||
@Query("searchText") searchText: String? = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue