diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 76a59bafa3..6527894333 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -12,6 +12,7 @@
+
@@ -199,4 +200,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2b..0f7bc519db 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index c914631447..ce1b62532a 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -61,8 +61,8 @@
-
-
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 37bc6d5ed2..8dd72df3e7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -75,6 +75,7 @@ android {
initWith(getByName("release"))
versionNameSuffix = "-beta"
applicationIdSuffix = ".debug"
+ signingConfig = signingConfigs.getByName("debug")
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 874783a15d..e8a5618a92 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -34,11 +34,10 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
- android:allowBackup="true"
- android:fullBackupContent="true"
+ android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning"
- tools:replace="android:fullBackupContent">
+ tools:replace="android:allowBackup">
-
diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config
index a1658496e7..bfc2bf8157 160000
--- a/app/src/main/assets/tangem-app-config
+++ b/app/src/main/assets/tangem-app-config
@@ -1 +1 @@
-Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6
+Subproject commit bfc2bf8157089bce6b44779bdae66df2c920de70
diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
new file mode 100644
index 0000000000..c4b3d48e73
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
@@ -0,0 +1,115 @@
+package com.tangem.tap
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.tangem.tap.common.extensions.dispatchOnMain
+import com.tangem.tap.common.redux.navigation.AppScreen
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import kotlinx.coroutines.*
+import timber.log.Timber
+import kotlin.time.Duration
+
+internal class LockUserWalletsTimer(
+ owner: LifecycleOwner,
+ private val duration: Duration = with(Duration) { 5.minutes },
+) : LifecycleOwner by owner,
+ DefaultLifecycleObserver {
+
+ private var delayJob: Job? = null
+ set(value) {
+ field?.cancel()
+ field = value
+ }
+ private var isStopped = false
+ private var openWelcomeScreenWhenResumed = false
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ Timber.d(
+ """
+ Owner resumed
+ |- Was stopped: $isStopped
+ |- Need to open welcome screen: $openWelcomeScreenWhenResumed
+ """.trimIndent(),
+ )
+ isStopped = false
+ start()
+ if (openWelcomeScreenWhenResumed) {
+ store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ openWelcomeScreenWhenResumed = false
+ }
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ Timber.d("Owner stopped")
+ isStopped = true
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ Timber.d("Owner destroyed")
+ stop()
+ }
+
+ fun restart() {
+ if (delayJob == null) return
+ Timber.d(
+ """
+ Timer restart
+ |- Duration millis: ${duration.inWholeMilliseconds}
+ """.trimIndent(),
+ )
+ start(log = false)
+ }
+
+ private fun start(log: Boolean = true) {
+ if (log) {
+ Timber.d(
+ """
+ Timer start
+ |- Duration millis: ${duration.inWholeMilliseconds}
+ """.trimIndent(),
+ )
+ }
+ delayJob = createDelayJob()
+ }
+
+ private fun stop(log: Boolean = true) {
+ if (log) {
+ Timber.d(
+ """
+ Timer stop
+ |- Was started: ${delayJob?.isActive ?: false}
+ """.trimIndent(),
+ )
+ }
+ delayJob = null
+ }
+
+ private fun createDelayJob(): Job = lifecycleScope.launch(Dispatchers.Default) {
+ val startTime = System.currentTimeMillis()
+ delay(duration)
+ if (isActive) {
+ val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
+ if (userWalletsListManager.hasSavedUserWallets) {
+ val currentTime = System.currentTimeMillis()
+ Timber.d(
+ """
+ Finished
+ |- App is stopped: $isStopped
+ |- Millis passed: ${currentTime - startTime}
+ """.trimIndent(),
+ )
+ userWalletsListManager.lock()
+ if (isStopped) {
+ openWelcomeScreenWhenResumed = true
+ } else {
+ store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index f89ccbeb0a..b7c9ad608a 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -27,7 +27,7 @@ import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_R
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
-import com.tangem.tap.domain.userWalletList.di.provideDummyImplementation
+import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
@@ -44,6 +44,10 @@ lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
lateinit var userWalletsListManager: UserWalletsListManager
+internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
+ private set
+var userWalletsListManagerSafe: UserWalletsListManager? = null
+ private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@@ -77,7 +81,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
- userWalletsListManager = UserWalletsListManager.provideDummyImplementation()
+ userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
+ context = applicationContext,
+ tangemSdkManager = tangemSdkManager,
+ )
+ userWalletsListManagerSafe = userWalletsListManager
+ lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
@@ -176,4 +185,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
onActivityResultCallbacks.remove(callback)
}
+
+ override fun onUserInteraction() {
+ super.onUserInteraction()
+
+ lockUserWalletsTimer?.restart()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 82e2081d78..41b4fb59bb 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -7,6 +7,8 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import com.tangem.Log
import com.tangem.LogFormat
+import com.tangem.blockchain.common.BlockchainSdkConfig
+import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.DomainLayer
@@ -39,8 +41,14 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
+import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
+import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
-import com.tangem.tap.domain.walletStores.di.provideDummyImplementation
+import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
+import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
+import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
+import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
+import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.CardBalanceStateAdapter
@@ -64,10 +72,40 @@ lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
-val walletStoresManager by lazy {
- WalletStoresManager.provideDummyImplementation()
+private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
+private val walletManagersRepository by lazy {
+ WalletManagersRepository.provideDefaultImplementation(
+ walletManagerFactory = WalletManagerFactory(
+ blockchainSdkConfig = store.state.globalState.configManager
+ ?.config
+ ?.blockchainSdkConfig
+ ?: BlockchainSdkConfig(),
+ ),
+ )
+}
+private val walletAmountsRepository by lazy {
+ WalletAmountsRepository.provideDefaultImplementation(
+ tangemTechService = store.state.domainNetworks.tangemTechService,
+ )
+}
+val walletStoresManager by lazy {
+ WalletStoresManager.provideDefaultImplementation(
+ userTokensRepository = userTokensRepository,
+ walletStoresRepository = walletStoresRepository,
+ walletManagersRepository = walletManagersRepository,
+ walletAmountsRepository = walletAmountsRepository,
+ appCurrencyProvider = { store.state.globalState.appCurrency },
+ )
+}
+val walletCurrenciesManager by lazy {
+ WalletCurrenciesManager.provideDefaultImplementation(
+ userTokensRepository = userTokensRepository,
+ walletStoresRepository = walletStoresRepository,
+ walletManagersRepository = walletManagersRepository,
+ walletAmountsRepository = walletAmountsRepository,
+ appCurrencyProvider = { store.state.globalState.appCurrency },
+ )
}
-
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
index 1919cd3dbf..ed66f4c74b 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt
@@ -28,8 +28,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
}
@Suppress("unused") // receiver type
-fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
- // TODO: Load tokens for selected user wallet. Will be created in further MRs
+suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
+ store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
}
fun Store<*>.dispatchToastNotification(resId: Int) {
@@ -77,7 +77,6 @@ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
-
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
index c89e7c0133..fad0770f65 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
@@ -71,7 +71,9 @@ data class AppState(
companion object {
fun getMiddleware(): List> {
return listOf(
- logMiddleware, navigationMiddleware, notificationsMiddleware,
+ logMiddleware,
+ navigationMiddleware,
+ notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
@@ -90,6 +92,7 @@ data class AppState(
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
+ LockUserWalletsTimerMiddleware().middleware,
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt
new file mode 100644
index 0000000000..67bb53a5b5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt
@@ -0,0 +1,15 @@
+package com.tangem.tap.common.redux
+
+import com.tangem.tap.lockUserWalletsTimer
+import org.rekotlin.Middleware
+
+class LockUserWalletsTimerMiddleware {
+ val middleware: Middleware = { _, _ ->
+ { nextDispatch ->
+ { action ->
+ lockUserWalletsTimer?.restart()
+ nextDispatch(action)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
index 0b13471f65..dfcdf942bf 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt
@@ -167,9 +167,9 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
- userTokensRepository,
- action.additionalBlockchainsToDerive,
- action.messageResId,
+ userTokensRepository = userTokensRepository,
+ additionalBlockchainsToDerive = action.additionalBlockchainsToDerive,
+ messageRes = action.messageResId,
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
index b7a54c7d6d..4d58f2467d 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
@@ -5,11 +5,21 @@ import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
-import com.tangem.common.*
+import com.tangem.common.CardFilter
+import com.tangem.common.CompletionResult
+import com.tangem.common.SuccessResponse
+import com.tangem.common.UserCode
+import com.tangem.common.UserCodeType
+import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
-import com.tangem.common.core.*
+import com.tangem.common.core.CardIdDisplayFormat
+import com.tangem.common.core.CardSessionRunnable
+import com.tangem.common.core.Config
+import com.tangem.common.core.TangemSdkError
+import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
@@ -28,6 +38,7 @@ 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
@@ -42,8 +53,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val canEnrollBiometrics: Boolean
get() = tangemSdk.biometricManager.canEnrollBiometrics
+ val biometricManager: BiometricManager
+ get() = tangemSdk.biometricManager
+
suspend fun scanProduct(
userTokensRepository: UserTokensRepository,
+ cardId: String? = null,
additionalBlockchainsToDerive: Collection? = null,
messageRes: Int? = null,
useBiometricsForAccessCode: Boolean = false,
@@ -52,8 +67,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
- runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
- cardId = null, initialMessage = message,
+ runnable = ScanProductTask(
+ card = null,
+ userTokensRepository = userTokensRepository,
+ additionalBlockchainsToDerive = additionalBlockchainsToDerive,
+ ),
+ cardId = cardId,
+ initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
}
@@ -89,7 +109,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun derivePublicKeys(
cardId: String,
derivations: Map>,
+ useBiometricsForAccessCode: Boolean = false,
): CompletionResult {
+ setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
@@ -102,6 +124,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
.map { CardDTO(it) }
}
+ suspend fun unlockBiometricKeys(): CompletionResult {
+ 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): CompletionResult {
return createUserCodeRepository().save(
cardIds = cardsIds,
@@ -110,6 +142,20 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
stringValue = accessCode,
),
)
+ .map {
+ biometricManager.unauthenticate(
+ keyName = tangemSdk.config.userCodesBiometricKeyName,
+ )
+ }
+ }
+
+ suspend fun clearSavedUserCodes(): CompletionResult {
+ return createUserCodeRepository().clear()
+ .map {
+ biometricManager.unauthenticate(
+ keyName = tangemSdk.config.userCodesBiometricKeyName,
+ )
+ }
}
suspend fun setPasscode(cardId: String?): CompletionResult {
diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
index bb07af060e..368ed91c74 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
@@ -1,11 +1,8 @@
package com.tangem.tap.domain
-import com.tangem.blockchain.common.Blockchain
-import com.tangem.blockchain.common.BlockchainSdkConfig
-import com.tangem.blockchain.common.Token
-import com.tangem.blockchain.common.Wallet
-import com.tangem.blockchain.common.WalletManager
-import com.tangem.blockchain.common.WalletManagerFactory
+import com.tangem.blockchain.common.*
+import com.tangem.common.doOnFailure
+import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
@@ -13,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
@@ -20,16 +18,21 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
+import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
+import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
@@ -81,6 +84,60 @@ class TapWalletManager {
}
}
+ suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
+ val scanResponse = userWallet.scanResponse
+ val card = scanResponse.card
+ val attestationFailed = card.attestation.status == Attestation.Status.Failed
+
+ store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
+ updateConfigManager(scanResponse)
+
+ withMainContext {
+ store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
+ store.dispatch(WalletConnectAction.ResetState)
+ store.dispatch(GlobalAction.SaveScanNoteResponse(scanResponse))
+ store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
+ store.dispatch(WalletAction.UserWalletChanged(userWallet))
+ store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
+ store.dispatch(WalletAction.Warnings.CheckIfNeeded)
+
+ if (refresh) {
+ loadData(userWallet, refresh = true)
+ }
+ }
+ }
+
+ suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
+ walletStoresManager.fetch(userWallet, refresh)
+ .doOnSuccess {
+ Timber.d("Wallet stores fetched for ${userWallet.walletId}")
+ store.dispatchOnMain(WalletAction.LoadData.Success)
+ }
+ .doOnFailure { error ->
+ val errorAction = when (error) {
+ is WalletStoresError -> when (error) {
+ is WalletStoresError.FetchFiatRatesError,
+ is WalletStoresError.UpdateWalletManagerError,
+ -> WalletAction.LoadData.Failure(error = null)
+ is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
+ error = TapError.WalletManager.CreationError,
+ )
+ is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
+ error = TapError.UnknownBlockchain,
+ )
+ is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
+ error = TapError.NoInternetConnection,
+ )
+ }
+ else -> WalletAction.LoadData.Failure(error = null)
+ }
+
+ Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
+
+ store.dispatchOnMain(errorAction)
+ }
+ }
+
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
@@ -143,7 +200,7 @@ class TapWalletManager {
private fun checkIfDerivationsAreMissing(blockchainNetworks: List, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
- WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
+ WalletAction.MultiWallet.AddTokens(it.tokens, it)
}
}
val missingDerivations = blockchainNetworks
@@ -171,7 +228,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
- save = false,
),
WalletAction.LoadFiatRate(),
)
@@ -187,7 +243,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
- save = false,
),
)
@@ -197,7 +252,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
- save = false,
),
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
index bd530b0796..b95d540bf0 100644
--- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
@@ -106,7 +106,7 @@ class ConfigManager {
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
- infuraProjectId = values.infuraProjectId
+ infuraProjectId = values.infuraProjectId,
),
appsFlyerDevKey = values.appsFlyerDevKey,
amplitudeApiKey = values.amplitudeApiKey,
diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
index 4cb6fbfdd8..a4433cb32a 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
@@ -22,4 +22,6 @@ data class UserWallet(
) {
val cardId: String
get() = scanResponse.card.cardId
+
+ internal var isSaved: Boolean = true
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
index 173ed53786..439b900c39 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
@@ -35,6 +35,12 @@ data class WalletDataModel(
open val pendingTransactions: List = emptyList()
open val errorMessage: String? = null
open val isErrorStatus: Boolean = false
+
+ fun asRefreshing() = Refreshing(
+ amount = amount,
+ pendingTransactions = pendingTransactions,
+ errorMessage = errorMessage,
+ )
}
object Loading : Status()
diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
index 1753b9b0ba..cba89801a3 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
@@ -10,7 +10,8 @@ import java.math.BigDecimal
* Contains info about the blockchain and its currencies
* @param userWalletId ID of the [UserWallet] which uses that store
* @param blockchainNetwork Store's [BlockchainNetwork]
- * @param walletManager Store's [WalletManager], may be null if it fails to create this manager
+ * @param walletManager Store's [WalletManager], may be null if it fails to create this manager. TODO: Remove after
+ * WalletMiddleware refactoring
* @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
* @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then
* [WalletRent.exemptionAmount]
@@ -18,6 +19,7 @@ import java.math.BigDecimal
data class WalletStoreModel(
val userWalletId: UserWalletId,
val blockchainNetwork: BlockchainNetwork,
+ @Deprecated("Don't use it, will be removed")
val walletManager: WalletManager?,
val walletsData: List,
val walletRent: WalletRent?,
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
new file mode 100644
index 0000000000..004184aacf
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
@@ -0,0 +1,77 @@
+package com.tangem.tap.domain.model.builders
+
+import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
+import com.tangem.blockchain.common.WalletManager
+import com.tangem.tap.domain.model.UserWallet
+import com.tangem.tap.domain.model.WalletDataModel
+import com.tangem.tap.domain.model.WalletStoreModel
+import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
+import java.math.BigDecimal
+
+class WalletStoreBuilder(
+ private val userWallet: UserWallet,
+) {
+ private var walletManager: WalletManager? = null
+ private var blockchainNetwork: BlockchainNetwork? = null
+
+ fun setWalletManager(walletManager: WalletManager?) = this.apply {
+ this.walletManager = walletManager
+ }
+
+ fun setBlockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply {
+ this.blockchainNetwork = blockchainNetwork
+ }
+
+ fun build(): WalletStoreModel {
+ val blockchainNetwork = this.blockchainNetwork
+ ?: walletManager?.let(BlockchainNetwork::fromWalletManager)
+ ?: error("Blockchain network and wallet manager must not be null")
+
+ val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
+ val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
+
+ return WalletStoreModel(
+ userWalletId = userWallet.walletId,
+ blockchainNetwork = blockchainNetwork,
+ walletManager = walletManager,
+ walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
+ walletRent = null,
+ )
+ }
+}
+
+private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
+ return WalletDataModel(
+ currency = Currency.Blockchain(
+ blockchain = blockchain,
+ derivationPath = derivationPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+}
+
+private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List {
+ return this.tokens
+ .map { token ->
+ WalletDataModel(
+ currency = Currency.Token(
+ token = token,
+ blockchain = blockchain,
+ derivationPath = derivationPath,
+ ),
+ status = WalletDataModel.Loading,
+ walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
+ existentialDeposit = getExistentialDeposit(walletManager),
+ fiatRate = null,
+ )
+ }
+}
+
+private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? {
+ return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
index 954888733e..eef2f2afb5 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
@@ -46,6 +46,7 @@ object ScanCardProcessor {
suspend fun scan(
useBiometricsForAccessCode: Boolean = false,
additionalBlockchainsToDerive: Collection? = null,
+ cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend (() -> Unit) = {},
@@ -56,6 +57,7 @@ object ScanCardProcessor {
onScanStateChange(true)
tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
+ cardId = cardId,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
useBiometricsForAccessCode = useBiometricsForAccessCode,
)
@@ -202,7 +204,7 @@ object ScanCardProcessor {
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
onWalletNotCreated()
- store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
+ store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
index 91006e8dab..a219814de1 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
@@ -59,6 +59,8 @@ class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable {
+ override val allowsAccessCodeFromRepository: Boolean = false
+
override fun run(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
index 575f5fc05f..537500d0b4 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
@@ -45,6 +45,9 @@ class ScanProductTask(
private val additionalBlockchainsToDerive: Collection? = null,
) : CardSessionRunnable {
+ override val allowsAccessCodeFromRepository: Boolean
+ get() = !additionalBlockchainsToDerive.isNullOrEmpty()
+
override fun run(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
index c29d226df4..2e2e0f9642 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
@@ -19,16 +19,21 @@ class TwinCardsManager(
card: CardDTO,
assetReader: AssetReader,
) {
-
- private val currentCardId: String = card.cardId
+ private val firstCardId: String = card.cardId
+ private var secondCardId: String? = null
private var currentCardPublicKey: String? = null
- private var secondCardPublicKey: String? = null
+ var secondCardPublicKey: String? = null
+ private set
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): CompletionResult {
- val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
+ val response = tangemSdkManager.runTaskAsync(
+ runnable = CreateFirstTwinWalletTask(),
+ cardId = firstCardId,
+ initialMessage = message,
+ )
when (response) {
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {}
@@ -43,6 +48,7 @@ class TwinCardsManager(
): CompletionResult {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
+ firstCardId = firstCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
@@ -51,6 +57,7 @@ class TwinCardsManager(
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
+ secondCardId = response.data.cardId
}
is CompletionResult.Failure -> {}
}
@@ -59,8 +66,9 @@ class TwinCardsManager(
suspend fun complete(message: Message): Result {
val response = tangemSdkManager.runTaskAsync(
- FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
- currentCardId, message,
+ runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
+ cardId = firstCardId,
+ initialMessage = message,
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt
new file mode 100644
index 0000000000..6291f7ae4d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt
@@ -0,0 +1,40 @@
+package com.tangem.tap.domain.userWalletList
+
+import com.tangem.common.core.TangemError
+import com.tangem.wallet.R
+
+sealed class UserWalletListError(code: Int) : TangemError(code) {
+ override val silent: Boolean
+ get() = (cause as? TangemError)?.silent == true
+
+ override val messageResId: Int? = null
+
+ object WalletAlreadySaved : UserWalletListError(code = 60001) {
+ override var customMessage: String = "This wallet has already been saved, you can add another one"
+ override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
+ }
+
+ class SaveEncryptionKeysError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60001) {
+ override var customMessage: String = "Encryption keys could not be saved: ${cause.localizedMessage}"
+ }
+
+ class ReceiveEncryptionKeysError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60002) {
+ override var customMessage: String = "Encryption keys could not be received: ${cause.localizedMessage}"
+ }
+
+ class SaveSensitiveInformationError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60003) {
+ override var customMessage: String = "Sensitive information could not be saved: ${cause.localizedMessage}"
+ }
+
+ class ReceiveSensitiveInformationError(
+ override val cause: Throwable,
+ ) : UserWalletListError(code = 60004) {
+ override var customMessage: String = "Sensitive information could not be received: ${cause.localizedMessage}"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
index f60c7f3de0..28475b75fa 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
@@ -1,8 +1,70 @@
package com.tangem.tap.domain.userWalletList.di
+import android.content.Context
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import com.tangem.common.json.TangemSdkAdapter
+import com.tangem.common.services.secure.SecureStorage
+import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage
+import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
+import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
-import com.tangem.tap.domain.userWalletList.implementation.DummyUserWalletsListManager
+import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
+import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
+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.*
-fun UserWalletsListManager.Companion.provideDummyImplementation(): UserWalletsListManager {
- return DummyUserWalletsListManager()
+const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
+const val USER_WALLETS_BIOMETRIC_KEY_NAME = "user_wallets"
+
+fun UserWalletsListManager.Companion.provideBiometricImplementation(
+ context: Context,
+ tangemSdkManager: TangemSdkManager,
+): UserWalletsListManager {
+ val moshi = Moshi.Builder()
+ .add(WalletDerivedKeysMapAdapter())
+ .add(ScanResponseDerivedKeysMapAdapter())
+ .add(ByteArrayKeyAdapter())
+ .add(ExtendedPublicKeysMapAdapter())
+ .add(CardBackupStatusAdapter())
+ .add(TangemSdkAdapter.DateAdapter())
+ .add(TangemSdkAdapter.DerivationPathAdapter())
+ .add(TangemSdkAdapter.DerivationNodeAdapter())
+ .add(KotlinJsonAdapterFactory())
+ .build()
+
+ val secureStorage = AndroidSecureStorage(
+ preferences = SecureStorage.createEncryptedSharedPreferences(
+ context = context,
+ storageName = USER_WALLETS_STORAGE_NAME,
+ ),
+ )
+
+ val keysRepository = BiometricUserWalletsKeysRepository(
+ biometricKeyName = USER_WALLETS_BIOMETRIC_KEY_NAME,
+ moshi = moshi,
+ secureStorage = secureStorage,
+ biometricManager = tangemSdkManager.biometricManager,
+ )
+ val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
+ moshi = moshi,
+ secureStorage = secureStorage,
+ )
+ val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
+ moshi = moshi,
+ secureStorage = secureStorage,
+ )
+ val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
+ secureStorage = secureStorage,
+ )
+
+ return BiometricUserWalletsListManager(
+ tangemSdkManager = tangemSdkManager,
+ keysRepository = keysRepository,
+ publicInformationRepository = publicInformationRepository,
+ sensitiveInformationRepository = sensitiveInformationRepository,
+ selectedUserWalletRepository = selectedUserWalletRepository,
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
new file mode 100644
index 0000000000..d7ab7b396d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
@@ -0,0 +1,303 @@
+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.userWalletList.UserWalletListError
+import com.tangem.tap.domain.userWalletList.UserWalletsListManager
+import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
+import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
+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.toUserWallets
+import com.tangem.tap.domain.userWalletList.utils.updateWith
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.*
+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,
+ private val selectedUserWalletRepository: SelectedUserWalletRepository,
+) : UserWalletsListManager {
+ private val state = MutableStateFlow(State())
+
+ override val userWallets: Flow>
+ get() = state
+ .mapLatest { it.wallets }
+ .distinctUntilChanged()
+
+ override val selectedUserWallet: Flow
+ get() = state
+ .mapLatest { state ->
+ state.wallets.find {
+ it.walletId == state.selectedWalletId
+ }
+ }
+ .filterNotNull()
+ .distinctUntilChanged()
+
+ override val selectedUserWalletSync: UserWallet?
+ get() = findSelectedWallet()
+
+ override val isLocked: Flow
+ get() = state
+ .mapLatest { it.isLocked }
+ .distinctUntilChanged()
+
+ override val isLockedSync: Boolean
+ get() = state.value.isLocked
+
+ override val hasSavedUserWallets: Boolean
+ get() = selectedUserWalletRepository.get() != null
+
+ override suspend fun unlockWithBiometry(): CompletionResult {
+ return unlockWithBiometryInternal()
+ .map { selectedUserWalletSync }
+ }
+
+ override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult {
+ state.update { prevState ->
+ userWallet.isSaved = false
+ prevState.copy(
+ encryptionKeys = listOf(UserWalletEncryptionKey(userWallet)),
+ wallets = listOf(userWallet),
+ )
+ }
+
+ return loadModels()
+ .map {
+ state.update { prevState ->
+ prevState.copy(
+ selectedWalletId = userWallet.walletId,
+ isLocked = prevState.wallets.size != 1,
+ )
+ }
+ }
+ }
+
+ override fun lock() {
+ tangemSdkManager.biometricManager.unauthenticate()
+ state.update { prevState ->
+ prevState.copy(
+ encryptionKeys = emptyList(),
+ isLocked = true,
+ )
+ }
+ }
+
+ override suspend fun selectWallet(walletId: UserWalletId): CompletionResult = catching {
+ if (state.value.selectedWalletId == walletId) {
+ return@catching findSelectedWallet()!!
+ }
+
+ if (!state.value.isLocked) {
+ selectedUserWalletRepository.set(walletId)
+
+ state.update { prevState ->
+ prevState.copy(
+ selectedWalletId = walletId,
+ )
+ }
+ }
+
+ findSelectedWallet()!!
+ }
+
+ override suspend fun save(userWallet: UserWallet): CompletionResult {
+ return saveInternal(userWallet, override = false)
+ }
+
+ override suspend fun update(userWallet: UserWallet): CompletionResult {
+ return saveInternal(userWallet, override = true)
+ }
+
+ override suspend fun delete(walletIds: List): CompletionResult {
+ if (state.value.isLocked) {
+ return CompletionResult.Success(Unit)
+ }
+
+ val walletIdsToRemove = state.value.wallets
+ .map { it.walletId }
+ .filter { it in walletIds }
+
+ changeSelectedWalletIfNeeded(walletIdsToRemove)
+
+ return sensitiveInformationRepository.delete(walletIdsToRemove)
+ .flatMap { publicInformationRepository.delete(walletIdsToRemove) }
+ .flatMap { keysRepository.delete(walletIdsToRemove) }
+ .map { keys ->
+ state.update { prevState ->
+ prevState.copy(
+ encryptionKeys = keys,
+ wallets = prevState.wallets.filter { it.walletId !in walletIdsToRemove },
+ )
+ }
+ }
+ .flatMap { loadModels() }
+ }
+
+ override suspend fun clear(): CompletionResult {
+ return sensitiveInformationRepository.delete(
+ walletIds = state.value.wallets.map { it.walletId },
+ )
+ .flatMap { publicInformationRepository.clear() }
+ .flatMap { keysRepository.clear() }
+ .map {
+ selectedUserWalletRepository.set(null)
+ tangemSdkManager.biometricManager.unauthenticate()
+ state.update { State() }
+ }
+ }
+
+ override suspend fun get(walletId: UserWalletId): CompletionResult = withUnlock {
+ return catching {
+ state.value.wallets.first { it.walletId == walletId }
+ }
+ }
+
+ private suspend fun saveInternal(
+ userWallet: UserWallet,
+ override: Boolean,
+ ): CompletionResult = withUnlock {
+ val isWalletSaved = state.value.wallets
+ .filter { it.isSaved }
+ .flatMap(UserWallet::cardsInWallet)
+ .contains(userWallet.cardId)
+
+ if (isWalletSaved && !override) {
+ CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
+ } else {
+ keysRepository.save(
+ walletId = userWallet.walletId,
+ encryptionKey = userWallet.scanResponse.card.encryptionKey,
+ )
+ .doOnSuccess { keys ->
+ state.update { prevState ->
+ prevState.copy(
+ encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
+ )
+ }
+ }
+ .flatMap { publicInformationRepository.save(userWallet) }
+ .flatMap { sensitiveInformationRepository.save(userWallet) }
+ .flatMap { loadModels() }
+ .doOnSuccess {
+ userWallet.isSaved = true
+ }
+ }
+ }
+
+ private suspend inline fun withUnlock(
+ block: () -> CompletionResult,
+ ): CompletionResult {
+ return (if (state.value.isLocked) unlockWithBiometryInternal() else CompletionResult.Success(Unit))
+ .flatMap { block() }
+ }
+
+ private suspend fun unlockWithBiometryInternal(): CompletionResult {
+ return keysRepository.getAll()
+ .map { keys ->
+ state.update { prevState ->
+ prevState.copy(
+ encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
+ )
+ }
+ }
+ .flatMap { loadModels() }
+ .map {
+ state.update { prevState ->
+ prevState.copy(
+ isLocked = false,
+ )
+ }
+ }
+ }
+
+ private suspend fun loadModels(): CompletionResult {
+ return getSavedUserWallets()
+ .map { userWallets ->
+ if (userWallets.isNotEmpty()) state.update { prevState ->
+ val wallets = (userWallets + prevState.wallets).distinctBy { it.walletId }
+
+ prevState.copy(
+ wallets = wallets,
+ selectedWalletId = findOrSetSelectedWallet(prevState.selectedWalletId, wallets),
+ )
+ }
+ }
+ .doOnFailure { error ->
+ Timber.e(error, "Unable to load user wallets")
+ }
+ }
+
+ private suspend fun getSavedUserWallets(): CompletionResult> {
+ return publicInformationRepository.getAll()
+ .map { it.toUserWallets() }
+ .flatMap { userWallets ->
+ sensitiveInformationRepository.getAll(state.value.encryptionKeys)
+ .map { walletIdToSensitiveInformation ->
+ userWallets.updateWith(walletIdToSensitiveInformation)
+ }
+ }
+ }
+
+ private fun findOrSetSelectedWallet(
+ prevSelectedWalletId: UserWalletId?,
+ userWallets: List,
+ ): UserWalletId? {
+ return prevSelectedWalletId
+ ?: (selectedUserWalletRepository.get()
+ ?: (userWallets.firstOrNull()?.walletId
+ ?.also { selectedUserWalletRepository.set(it) }))
+ }
+
+ private fun changeSelectedWalletIfNeeded(
+ walletsIdsToRemove: List,
+ ) {
+ val remainingWallets = state.value.wallets.filter {
+ it.walletId !in walletsIdsToRemove
+ }
+ val selectedWallet = findSelectedWallet()
+ when {
+ remainingWallets.isEmpty() -> {
+ state.update { prevState ->
+ prevState.copy(
+ selectedWalletId = null,
+ )
+ }
+ selectedUserWalletRepository.set(null)
+ }
+ !remainingWallets.contains(selectedWallet) -> {
+ val newSelectedWallet = remainingWallets.first()
+ state.update { prevState ->
+ prevState.copy(
+ selectedWalletId = newSelectedWallet.walletId,
+ )
+ }
+ selectedUserWalletRepository.set(newSelectedWallet.walletId)
+ }
+ }
+ }
+
+ private fun findSelectedWallet(): UserWallet? {
+ return with(state.value) {
+ wallets.find {
+ it.walletId == selectedWalletId
+ }
+ }
+ }
+
+ private data class State(
+ val encryptionKeys: List = emptyList(),
+ val wallets: List = emptyList(),
+ val selectedWalletId: UserWalletId? = null,
+ val isLocked: Boolean = true,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletEncryptionKey.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletEncryptionKey.kt
new file mode 100644
index 0000000000..cdf1edbdfb
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletEncryptionKey.kt
@@ -0,0 +1,33 @@
+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
+
+ if (walletId != other.walletId) return false
+ if (!encryptionKey.contentEquals(other.encryptionKey)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = walletId.hashCode()
+ result = 31 * result + encryptionKey.contentHashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
new file mode 100644
index 0000000000..b2308343ff
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
@@ -0,0 +1,20 @@
+package com.tangem.tap.domain.userWalletList.model
+
+import com.squareup.moshi.JsonClass
+import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.UserWalletId
+
+@JsonClass(generateAdapter = true)
+internal data class UserWalletSensitiveInformation(
+ val wallets: List,
+)
+
+@JsonClass(generateAdapter = true)
+internal data class UserWalletPublicInformation(
+ val name: String,
+ val walletId: UserWalletId,
+ val artworkUrl: String,
+ val cardsInWallet: Set,
+ val scanResponse: ScanResponse,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/SelectedUserWalletRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/SelectedUserWalletRepository.kt
new file mode 100644
index 0000000000..aee7766d49
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/SelectedUserWalletRepository.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap.domain.userWalletList.repository
+
+import com.tangem.domain.common.util.UserWalletId
+
+internal interface SelectedUserWalletRepository {
+ fun get(): UserWalletId?
+ fun set(walletId: UserWalletId?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt
new file mode 100644
index 0000000000..9fc67b367b
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt
@@ -0,0 +1,12 @@
+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>
+ suspend fun save(walletId: UserWalletId, encryptionKey: ByteArray): CompletionResult>
+ suspend fun delete(walletIds: List): CompletionResult>
+ suspend fun clear(): CompletionResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt
new file mode 100644
index 0000000000..8765a6b804
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt
@@ -0,0 +1,15 @@
+package com.tangem.tap.domain.userWalletList.repository
+
+import com.tangem.common.CompletionResult
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
+
+internal interface UserWalletsPublicInformationRepository {
+ suspend fun save(userWallet: UserWallet): CompletionResult
+
+ suspend fun getAll(): CompletionResult>
+
+ suspend fun delete(walletIds: List): CompletionResult
+ suspend fun clear(): CompletionResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt
new file mode 100644
index 0000000000..dcc5677814
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt
@@ -0,0 +1,16 @@
+package com.tangem.tap.domain.userWalletList.repository
+
+import com.tangem.common.CompletionResult
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+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
+ suspend fun getAll(
+ encryptionKeys: List,
+ ): CompletionResult