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 d1e7b40573..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")
}
}
@@ -192,6 +193,7 @@ dependencies {
implementation(Library.viewBindingDelegate)
implementation(Library.armadillo)
implementation(Library.googlePlayServicesWallet)
+ implementation(Library.composeShimmer)
/** Testing libraries */
testImplementation(Test.junit)
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/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
new file mode 100644
index 0000000000..16a05117ac
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap
+
+import android.content.Intent
+import androidx.activity.result.ActivityResultLauncher
+
+interface ActivityResultCaller {
+ val activityResultLauncher: ActivityResultLauncher?
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
index 806769df5f..9664f38966 100644
--- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
+++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt
@@ -2,11 +2,18 @@ package com.tangem.tap
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
+import android.content.Intent
import android.os.Bundle
-import java.util.WeakHashMap
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import java.util.*
import kotlin.reflect.KClass
-class ForegroundActivityObserver {
+class ForegroundActivityObserver : ActivityResultCaller {
+ override var activityResultLauncher: ActivityResultLauncher? = null
+ private set
+
private val activities = WeakHashMap, Activity>()
val foregroundActivity: Activity?
@@ -15,25 +22,38 @@ class ForegroundActivityObserver {
.firstOrNull()
?.value
- val callbacks get() = Callbacks()
+ internal val callbacks: ActivityLifecycleCallbacks
+ get() = Callbacks()
+
+ internal inner class Callbacks : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult(),
+ ) {
+ /* no-op */
+ }
+ }
- inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity
}
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity::class)
+ if (activities.isEmpty()) {
+ activityResultLauncher = null
+ }
}
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
- }
override fun onActivityStarted(activity: Activity) {
}
+
override fun onActivityPaused(activity: Activity) {
}
+
override fun onActivityStopped(activity: Activity) {
}
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
}
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 05e9eed54d..b7c9ad608a 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -18,6 +18,7 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
+import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@@ -25,6 +26,8 @@ import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
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.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
@@ -40,6 +43,11 @@ import kotlin.coroutines.CoroutineContext
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
@@ -73,6 +81,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
+ userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
+ context = applicationContext,
+ tangemSdkManager = tangemSdkManager,
+ )
+ userWalletsListManagerSafe = userWalletsListManager
+ lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
@@ -82,7 +96,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun systemActions() {
-
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
@@ -106,7 +119,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
- store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
+ val navigateTo = if (userWalletsListManager.hasSavedUserWallets) AppScreen.Welcome else AppScreen.Home
+
+ store.dispatchOnMain(NavigationAction.NavigateTo(navigateTo))
}
intentHandler.handleIntent(intent)
}
@@ -170,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 287f2d7a1e..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
@@ -37,6 +39,16 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
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.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
@@ -53,12 +65,51 @@ import javax.inject.Inject
lateinit var store: Store
lateinit var foregroundActivityObserver: ForegroundActivityObserver
+lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
+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()
+}
+
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
@@ -82,6 +133,7 @@ class TapApplication : Application(), ImageLoaderFactory {
}
foregroundActivityObserver = ForegroundActivityObserver()
+ activityResultCaller = foregroundActivityObserver
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
initMoshiConverter()
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
index 17e31d88c4..491ef8f683 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt
@@ -74,7 +74,7 @@ object TangemSdk {
is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage)
is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
- is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable)
+ is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.cause)
is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards()
is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired()
is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired()
@@ -107,6 +107,8 @@ object TangemSdk {
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
+ is TangemSdkError.BiometricsUnavailable -> error
+ is TangemSdkError.BiometricsAuthenticationFailed -> error
}
}
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt
index 3950f458bc..301ae0cb07 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt
@@ -3,10 +3,10 @@ package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
-import com.tangem.tap.domain.extensions.getUserWalletId
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
@@ -29,7 +29,7 @@ class BasicSignInEventConverter(
currency = cardCurrency,
batch = scanResponse.card.batchId,
).apply {
- filterData = scanResponse.card.getUserWalletId()
+ filterData = scanResponse.card.userWalletId.stringValue
}
}
}
@@ -43,7 +43,7 @@ class BasicTopUpEventConverter(
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
val data = BasicTopUpFilter.Data(
- walletId = scanResponse.card.getUserWalletId(),
+ walletId = scanResponse.card.userWalletId.stringValue,
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsData),
)
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
index 97575ecd31..3abbbac6d1 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
@@ -22,12 +22,15 @@ import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
+import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
+import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
+import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.wallet.R
import timber.log.Timber
@@ -50,10 +53,14 @@ fun FragmentActivity.openFragment(
}
if (screen.isDialogFragment) {
(fragment as DialogFragment).show(transaction, screen.name)
- if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
+ if (addToBackstack) {
+ transaction.addToBackStack(null)
+ }
} else {
transaction.replace(R.id.fragment_container, fragment, screen.name)
- if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
+ if (addToBackstack && (screen != AppScreen.Home && screen != AppScreen.Welcome)) {
+ transaction.addToBackStack(null)
+ }
transaction.commitAllowingStateLoss()
}
}
@@ -110,5 +117,8 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
AppScreen.ReferralProgram -> ReferralFragment()
+ AppScreen.Welcome -> WelcomeFragment()
+ AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
+ AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
}
}
\ No newline at end of file
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 d88560669e..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
@@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
+import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@@ -26,6 +27,11 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
+@Suppress("unused") // receiver type
+suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
+ store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
+}
+
fun Store<*>.dispatchToastNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
}
@@ -71,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/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
index 632f9f965b..16e1bb559a 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
@@ -8,10 +8,10 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
-import com.tangem.common.card.CardWallet
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.extensions.stripZeroPlainString
-import com.tangem.tap.domain.extensions.getUserWalletId
class AdditionalFeedbackInfo {
class EmailWalletInfo(
@@ -54,7 +54,7 @@ class AdditionalFeedbackInfo {
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
- userWalletId = data.card.getUserWalletId()
+ userWalletId = data.card.userWalletId.stringValue
}
fun setWalletsInfo(walletManagers: List) {
@@ -92,7 +92,7 @@ class AdditionalFeedbackInfo {
)
}
- private fun formatSignedHashes(wallets: List): String {
+ private fun formatSignedHashes(wallets: List): String {
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
index a14cdb9925..f03dfc70ca 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
@@ -10,11 +10,14 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteRedu
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
+import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
+import com.tangem.tap.features.welcome.redux.WelcomeReducer
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
@@ -36,6 +39,9 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
+ welcomeState = WelcomeReducer.reduce(action, state),
+ saveWalletState = SaveWalletReducer.reduce(action, state),
+ walletSelectorState = WalletSelectorReducer.reduce(action, state),
)
}
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 af79cb81e1..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
@@ -25,6 +25,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
+import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
+import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
@@ -33,6 +35,10 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
+import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
+import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
+import com.tangem.tap.features.welcome.redux.WelcomeState
import org.rekotlin.Middleware
import org.rekotlin.StateType
@@ -51,6 +57,9 @@ data class AppState(
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
+ val welcomeState: WelcomeState = WelcomeState(),
+ val saveWalletState: SaveWalletState = SaveWalletState(),
+ val walletSelectorState: WalletSelectorState = WalletSelectorState(),
) : StateType {
val domainState: DomainState
@@ -62,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,
@@ -78,8 +89,11 @@ data class AppState(
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
+ WelcomeMiddleware().middleware,
+ SaveWalletMiddleware().middleware,
+ WalletSelectorMiddleware().middleware,
+ LockUserWalletsTimerMiddleware().middleware,
)
}
}
-}
-
+}
\ No newline at end of file
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/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
index f8563c233a..f4aa80ad03 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt
@@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
+import com.tangem.tap.common.extensions.replaceBy
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
@@ -49,12 +50,19 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanResponse?.card ?: return globalState
- val wallet = card.wallet(action.walletPublicKey) ?: return globalState
- val newCardInstance = card.updateWallet(
- wallet.copy(
- totalSignedHashes = action.walletSignedHashes,
- remainingSignatures = action.remainingSignatures,
- ),
+ val wallet = card.wallets
+ .firstOrNull { it.publicKey.contentEquals(action.walletPublicKey) }
+ ?: return globalState
+
+ val newCardInstance = card.copy(
+ wallets = card.wallets.toMutableList().also { walletsMutable ->
+ walletsMutable.replaceBy(
+ item = wallet.copy(
+ totalSignedHashes = action.walletSignedHashes,
+ remainingSignatures = action.remainingSignatures,
+ ),
+ ) { it.index == wallet.index }
+ },
)
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
index e397d78d8a..df26b85edc 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt
@@ -18,6 +18,8 @@ sealed class NavigationAction : Action {
data class OpenDocument(val url: Uri) : NavigationAction()
+ object OpenBiometricsSettings : NavigationAction()
+
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference) : NavigationAction()
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
index 8a2f5c5ac1..da9430b1e1 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt
@@ -1,14 +1,20 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
+import android.hardware.biometrics.BiometricManager
+import android.os.Build
+import android.provider.Settings
+import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
+import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
+import com.tangem.tap.store
import org.rekotlin.Middleware
-val navigationMiddleware: Middleware = { dispatch, state ->
+val navigationMiddleware: Middleware = { _, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
@@ -16,16 +22,24 @@ val navigationMiddleware: Middleware = { dispatch, state ->
when (action) {
is NavigationAction.NavigateTo -> {
navState?.activity?.get()?.openFragment(
- action.screen,
- action.addToBackstack,
- action.fragmentShareTransition
+ screen = action.screen,
+ addToBackstack = action.addToBackstack,
+ fgShareTransition = action.fragmentShareTransition,
)
}
is NavigationAction.PopBackTo -> {
- if (action.screen == AppScreen.Home) {
- navState?.activity?.get()?.popBackTo(null, true)
- } else {
- navState?.activity?.get()?.popBackTo(action.screen)
+ when (val screen = action.screen) {
+ AppScreen.Home,
+ AppScreen.Welcome,
+ -> {
+ navState?.activity?.get()?.popBackTo(screen = null, inclusive = true)
+ if (navState?.backStack?.contains(screen) != true) {
+ store.dispatchOnMain(NavigationAction.NavigateTo(screen))
+ }
+ }
+ else -> {
+ navState?.activity?.get()?.popBackTo(screen = action.screen)
+ }
}
}
is NavigationAction.OpenUrl -> {
@@ -38,11 +52,34 @@ val navigationMiddleware: Middleware = { dispatch, state ->
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
- is NavigationAction.Share -> {
- navState?.activity?.get()?.let {
- it.shareText(action.data)
+ is NavigationAction.OpenBiometricsSettings -> {
+ val settingsAction = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
+ Settings.ACTION_BIOMETRIC_ENROLL
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
+ Settings.ACTION_FINGERPRINT_ENROLL
+ }
+ else -> {
+ Settings.ACTION_SETTINGS
+ }
}
+ val intent = Intent(settingsAction).apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ putExtra(
+ Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
+ BiometricManager.Authenticators.BIOMETRIC_STRONG,
+ )
+ }
+ }
+ activityResultCaller.activityResultLauncher?.launch(intent)
}
+ is NavigationAction.Share -> {
+ navState?.activity?.get()?.shareText(action.data)
+ }
+ is NavigationAction.ActivityCreated,
+ is NavigationAction.ActivityDestroyed,
+ -> Unit
}
}
next(action)
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
index 6d5993cbc0..cf60dc79f6 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
@@ -5,7 +5,7 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
- val backStack: List = listOf(AppScreen.Home),
+ val backStack: List = emptyList(),
val activity: WeakReference? = null,
) : StateType
@@ -22,5 +22,8 @@ enum class AppScreen(
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan,
- ReferralProgram
+ ReferralProgram,
+ Welcome,
+ SaveWallet(isDialogFragment = true),
+ WalletSelector(isDialogFragment = true),
}
\ No newline at end of file
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 4fe7252ac7..4d58f2467d 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
@@ -1,7 +1,5 @@
package com.tangem.tap.domain
-import CreateProductWalletTask
-import CreateProductWalletTaskResponse
import android.content.Context
import androidx.annotation.StringRes
import com.tangem.Message
@@ -10,14 +8,20 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
-import com.tangem.common.card.Card
+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.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
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
@@ -29,9 +33,12 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
+import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
+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
@@ -40,16 +47,33 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
+ val canUseBiometry: Boolean
+ get() = tangemSdk.biometricManager.canAuthenticate || canEnrollBiometrics
+
+ 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,
): CompletionResult {
+ setAccessCodeRequestPolicy(useBiometricsForAccessCode)
+
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) }
}
@@ -73,27 +97,65 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
}
- suspend fun createWallet(cardId: String?): CompletionResult {
+ suspend fun createWallet(cardId: String?): CompletionResult {
return runTaskAsyncReturnOnMain(
CreateWalletAndRescanTask(),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)),
)
+ .map { CardDTO(it) }
}
suspend fun derivePublicKeys(
cardId: String,
derivations: Map>,
+ useBiometricsForAccessCode: Boolean = false,
): CompletionResult {
+ setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
- suspend fun resetToFactorySettings(card: Card): CompletionResult {
+ suspend fun resetToFactorySettings(cardId: String): CompletionResult {
return runTaskAsyncReturnOnMain(
- ResetToFactorySettingsTask(),
- card.cardId,
+ runnable = ResetToFactorySettingsTask(),
+ cardId = cardId,
initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)),
)
+ .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,
+ userCode = UserCode(
+ type = UserCodeType.AccessCode,
+ 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 {
@@ -128,11 +190,17 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
- suspend fun scanCard(): CompletionResult {
+ suspend fun scanCard(
+ cardId: String? = null,
+ useBiometricsForAccessCode: Boolean = false,
+ ): CompletionResult {
+ setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(
- ScanTask(),
+ runnable = ScanTask(),
+ cardId = cardId,
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
)
+ .map { CardDTO(it) }
}
suspend fun runTaskAsync(
@@ -150,7 +218,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private suspend fun runTaskAsyncReturnOnMain(
- runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null,
+ runnable: CardSessionRunnable,
+ cardId: String? = null,
+ initialMessage: Message? = null,
): CompletionResult {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
@@ -169,6 +239,23 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
return context.getString(stringResId, formatArgs)
}
+ fun setAccessCodeRequestPolicy(
+ useBiometricsForAccessCode: Boolean,
+ ) {
+ tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) {
+ UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode)
+ } else {
+ UserCodeRequestPolicy.Default
+ }
+ }
+
+ private fun createUserCodeRepository() = with(tangemSdk) {
+ UserCodeRepository(
+ biometricManager = biometricManager,
+ secureStorage = secureStorage,
+ )
+ }
+
companion object {
val config = Config(
linkedTerminal = true,
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
index af5cdb6a5c..1979b1df8a 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
@@ -5,13 +5,13 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
-import com.tangem.common.card.Card
+import com.tangem.domain.common.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
- private val card: Card,
+ private val card: CardDTO,
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val accessCode: String? = null,
diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
index 5a0cf85668..0bc9a2c283 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt
@@ -64,8 +64,7 @@ sealed class TapError(
) : TapError(-1), MultiMessageError
}
-sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError {
- final override val code: Int = 50100
+sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) {
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)
@@ -88,4 +87,4 @@ fun TangemTechError.toTapError(): TapError {
}
}
-class NoDataError(message: String) : TapError.CustomError(customMessage = message)
+class NoDataError(message: String) : TapError.CustomError(customMessage = message)
\ No newline at end of file
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 b15f2a7431..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,18 +1,16 @@
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.common.card.Card
+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
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)
@@ -96,7 +153,7 @@ class TapWalletManager {
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
- && data.card.backupStatus == Card.BackupStatus.NoBackup,
+ && data.card.backupStatus == CardDTO.BackupStatus.NoBackup,
),
)
loadData(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/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
index ab2ba5610b..fd46ad2fde 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
@@ -1,58 +1,45 @@
package com.tangem.tap.domain.extensions
-import com.tangem.common.card.Card
-import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
-import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TwinCardNumber
-import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.isTangemTwin
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.features.wallet.redux.Artwork
-val Card.remainingSignatures: Int?
- get() = this.getSingleWallet()?.remainingSignatures
+val CardDTO.remainingSignatures: Int?
+ get() = this.wallets.firstOrNull()?.remainingSignatures
-val Card.isWalletDataSupported: Boolean
+val CardDTO.isWalletDataSupported: Boolean
get() = this.firmwareVersion.major >= 4
-val Card.isMultiwalletAllowed: Boolean
+val CardDTO.isMultiwalletAllowed: Boolean
get() {
- return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay
- && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
- getSingleWallet()?.curve == EllipticCurve.Secp256k1)
+ return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay &&
+ (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
+ wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1)
}
-val Card.isHdWalletAllowedByApp: Boolean
+val CardDTO.isHdWalletAllowedByApp: Boolean
get() = settings.isHDWalletAllowed && !isSaltPay
-fun Card.getSingleWallet(): CardWallet? {
- return wallets.firstOrNull()
+fun CardDTO.hasSignedHashes(): Boolean {
+ return wallets.any { (it.totalSignedHashes ?: 0) > 0 }
}
-fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
-
-fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
-
-fun Card.hasSingleWallet(): Boolean = wallets.size == 1
-
-fun Card.hasSignedHashes(): Boolean {
- return wallets.any { it.totalSignedHashes ?: 0 > 0 }
+fun CardDTO.signedHashesCount(): Int {
+ return wallets.sumOf { it.totalSignedHashes ?: 0 }
}
-fun Card.signedHashesCount(): Int {
- return wallets.map { it.totalSignedHashes ?: 0 }.sum()
-}
-
-suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String {
+suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@@ -67,42 +54,28 @@ suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result {
val artworkId = cardInfoResult.data.artwork?.id
- if (artworkId == null || artworkId.isEmpty()) {
+ if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
+
is Result.Failure -> ifAnyError()
}
}
-fun Card.getArtworkUrl(artworkId: String?): String? {
+fun CardDTO.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
+
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
-}
-
-fun Card.getUserWalletId(): String {
- val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
- return UserWalletId(walletPublicKey).stringValue
-}
-
-class UserWalletId(val walletPublicKey: ByteArray) {
- val stringValue: String = calculateUserId(walletPublicKey)
-
- private fun calculateUserId(walletPublicKey: ByteArray): String {
- val message = "UserWalletID".toByteArray()
- val keyHash = walletPublicKey.calculateSha256()
- return message.calculateHmacSha256(keyHash).toHexString()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
index ec95e13dc6..874e562a2a 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
@@ -7,13 +7,12 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
-import com.tangem.common.card.Card
-import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
@@ -82,7 +81,7 @@ fun WalletManagerFactory.makeWalletManagerForApp(
)
}
-private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
+private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {
return derivationPath?.let {
DerivationParams.Custom(
DerivationPath(it),
@@ -132,7 +131,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
)
}
-private fun selectWallet(wallets: List): CardWallet? {
+private fun selectWallet(wallets: List): CardDTO.Wallet? {
return when (wallets.size) {
0 -> null
1 -> wallets[0]
diff --git a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt
new file mode 100644
index 0000000000..ac3ac6d56a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt
@@ -0,0 +1,25 @@
+package com.tangem.tap.domain.model
+
+import java.math.BigDecimal
+
+/**
+ * Represents fiat balance of [WalletStoreModel] list
+ * @property amount Amount of the total balance
+ * */
+sealed class TotalFiatBalance {
+ open val amount: BigDecimal = BigDecimal.ZERO
+
+ object Loading : TotalFiatBalance()
+
+ data class Refreshing(
+ override val amount: BigDecimal,
+ ) : TotalFiatBalance()
+
+ data class Error(
+ override val amount: BigDecimal,
+ ) : TotalFiatBalance()
+
+ data class Loaded(
+ override val amount: BigDecimal,
+ ) : TotalFiatBalance()
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..a4433cb32a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
@@ -0,0 +1,27 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.UserWalletId
+
+/**
+ * Represents user's wallet which stored in app persistence
+ * @param name User's wallet name
+ * @param walletId User's wallet [UserWalletId]
+ * @param artworkUrl User wallet card artwork URL
+ * @param cardsInWallet List of cards IDs assigned with this user's wallet
+ * @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
+ * */
+data class UserWallet(
+ val name: String,
+ val walletId: UserWalletId,
+ val artworkUrl: String,
+ val cardsInWallet: Set,
+ val scanResponse: ScanResponse,
+) {
+ 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
new file mode 100644
index 0000000000..439b900c39
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt
@@ -0,0 +1,85 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.tap.domain.model.WalletDataModel.Status
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.features.wallet.models.PendingTransaction
+import com.tangem.tap.features.wallet.redux.AddressData
+import java.math.BigDecimal
+
+/**
+ * Contains info about wallet store currency
+ * @param currency Wallet's [Currency]
+ * @param status Wallet's [Status], represents current status of that currency
+ * @param walletAddresses List of wallet data [AddressData]
+ * @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
+ * founds will be destroyed. Null if currency don't have existential deposit
+ * @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
+ * */
+data class WalletDataModel(
+ val currency: Currency,
+ val status: Status,
+ val walletAddresses: List,
+ val existentialDeposit: BigDecimal?,
+ val fiatRate: BigDecimal?,
+) {
+
+ /**
+ * Represent current status of currency
+ * @property amount Currency amount
+ * @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain
+ * @property errorMessage Status error message, null if not provided
+ * @property isErrorStatus true if current status is error status, false otherwise
+ * */
+ sealed class Status {
+ open val amount: BigDecimal = BigDecimal.ZERO
+ 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()
+
+ data class VerifiedOnline(
+ override val amount: BigDecimal,
+ ) : Status()
+
+ data class TransactionInProgress(
+ override val amount: BigDecimal,
+ override val pendingTransactions: List,
+ ) : Status()
+
+ data class SameCurrencyTransactionInProgress(
+ override val amount: BigDecimal,
+ override val pendingTransactions: List,
+ ) : Status()
+
+ data class NoAccount(
+ val amountToCreateAccount: BigDecimal?,
+ ) : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+
+ data class Unreachable(
+ override val errorMessage: String?,
+ ) : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+
+ object MissedDerivation : Status() {
+ override val isErrorStatus: Boolean = true
+ }
+
+ data class Refreshing(
+ override val amount: BigDecimal,
+ override val pendingTransactions: List,
+ override val errorMessage: String?,
+ ) : Status() {
+ override val isErrorStatus: Boolean = errorMessage != null
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..cba89801a3
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt
@@ -0,0 +1,38 @@
+package com.tangem.tap.domain.model
+
+import com.tangem.blockchain.common.WalletManager
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
+import com.tangem.tap.domain.tokens.models.BlockchainNetwork
+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. 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]
+ * */
+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?,
+) {
+
+ /**
+ * Represents wallet blockchain rent
+ * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
+ * the [WalletRent.exemptionAmount]
+ * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent
+ * */
+ data class WalletRent(
+ val rent: BigDecimal,
+ val exemptionAmount: BigDecimal,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
new file mode 100644
index 0000000000..377b8cf8c6
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
@@ -0,0 +1,30 @@
+package com.tangem.tap.domain.model.builders
+
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.userWalletId
+import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
+import com.tangem.tap.domain.model.UserWallet
+
+class UserWalletBuilder(
+ private val scanResponse: ScanResponse,
+) {
+ private var backupCardsIds: Set = emptySet()
+
+ fun backupCardsIds(backupCardsIds: Set?) = this.apply {
+ if (backupCardsIds != null) {
+ this.backupCardsIds = backupCardsIds
+ }
+ }
+
+ suspend fun build(): UserWallet {
+ return with(scanResponse) {
+ UserWallet(
+ walletId = card.userWalletId,
+ name = productType.name,
+ artworkUrl = card.getOrLoadCardArtworkUrl(),
+ cardsInWallet = backupCardsIds.plus(card.cardId),
+ scanResponse = this,
+ )
+ }
+ }
+}
\ No newline at end of file
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..003c85904a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
@@ -0,0 +1,122 @@
+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
+
+interface WalletStoreBuilder {
+ fun build(): WalletStoreModel
+
+ interface BlockchainNetworkWalletStoreBuilder : WalletStoreBuilder {
+ fun walletManager(walletManager: WalletManager?): WalletStoreBuilder
+ }
+
+ interface WalletMangerWalletStoreBuilder : WalletStoreBuilder {
+ fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?): WalletStoreBuilder
+ }
+
+ companion object {
+ operator fun invoke(
+ userWallet: UserWallet,
+ blockchainNetwork: BlockchainNetwork,
+ ): BlockchainNetworkWalletStoreBuilder {
+ return BlockchainNetworkWalletStoreBuilderImpl(userWallet, blockchainNetwork)
+ }
+
+ operator fun invoke(
+ userWallet: UserWallet,
+ walletManager: WalletManager,
+ ): WalletMangerWalletStoreBuilder {
+ return WalletMangerWalletStoreBuilderImpl(userWallet, walletManager)
+ }
+ }
+}
+
+private class BlockchainNetworkWalletStoreBuilderImpl(
+ private val userWallet: UserWallet,
+ private val blockchainNetwork: BlockchainNetwork,
+) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder {
+ private var walletManager: WalletManager? = null
+
+ override fun walletManager(walletManager: WalletManager?) = this.apply {
+ this.walletManager = walletManager
+ }
+
+ override fun build(): WalletStoreModel {
+ 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 class WalletMangerWalletStoreBuilderImpl(
+ private val userWallet: UserWallet,
+ private val walletManager: WalletManager,
+) : WalletStoreBuilder.WalletMangerWalletStoreBuilder {
+ private var blockchainNetwork: BlockchainNetwork? = null
+
+ override fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply {
+ this.blockchainNetwork = blockchainNetwork
+ }
+
+ override fun build(): WalletStoreModel {
+ val blockchainNetwork = this.blockchainNetwork ?: BlockchainNetwork.fromWalletManager(walletManager)
+ 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
new file mode 100644
index 0000000000..eef2f2afb5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
@@ -0,0 +1,235 @@
+package com.tangem.tap.domain.scanCard
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.core.TangemError
+import com.tangem.common.core.TangemSdkError
+import com.tangem.common.doOnFailure
+import com.tangem.common.doOnSuccess
+import com.tangem.common.services.Result
+import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.operations.backup.BackupService
+import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
+import com.tangem.tap.backupService
+import com.tangem.tap.common.analytics.Analytics
+import com.tangem.tap.common.analytics.events.IntroductionProcess
+import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
+import com.tangem.tap.common.extensions.dispatchDialogShow
+import com.tangem.tap.common.extensions.dispatchOnMain
+import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
+import com.tangem.tap.common.redux.AppDialog
+import com.tangem.tap.common.redux.global.GlobalAction
+import com.tangem.tap.common.redux.navigation.AppScreen
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
+import com.tangem.tap.features.disclaimer.redux.DisclaimerType
+import com.tangem.tap.features.disclaimer.redux.isAccepted
+import com.tangem.tap.features.onboarding.OnboardingHelper
+import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
+import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
+import com.tangem.tap.preferencesStorage
+import com.tangem.tap.scope
+import com.tangem.tap.store
+import com.tangem.tap.tangemSdkManager
+import com.tangem.tap.userTokensRepository
+import com.tangem.wallet.R
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+// TODO: Create repository for that
+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) = {},
+ onFailure: suspend (error: TangemError) -> Unit = {},
+ onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
+ ) = withMainContext {
+ onProgressStateChange(true)
+ onScanStateChange(true)
+ tangemSdkManager.scanProduct(
+ userTokensRepository = userTokensRepository,
+ cardId = cardId,
+ additionalBlockchainsToDerive = additionalBlockchainsToDerive,
+ useBiometricsForAccessCode = useBiometricsForAccessCode,
+ )
+ .doOnFailure { error ->
+ onProgressStateChange(false)
+ onScanStateChange(false)
+ onFailure(error)
+ }
+ .doOnSuccess { scanResponse ->
+ onScanStateChange(false)
+ checkForUnfinishedBackupForSaltPay(
+ backupService = backupService,
+ scanResponse = scanResponse,
+ onProgressStateChange = { onProgressStateChange(it) },
+ nextHandler = {
+ showDisclaimerIfNeed(
+ scanResponse = scanResponse,
+ nextHandler = {
+ onScanSuccess(
+ scanResponse = scanResponse,
+ onProgressStateChange = onProgressStateChange,
+ onSuccess = onSuccess,
+ onWalletNotCreated = onWalletNotCreated,
+ onFailure = onFailure,
+ )
+ },
+ )
+ },
+ )
+
+ }
+ }
+
+ /**
+ * It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
+ * see BackupAction.CheckForUnfinishedBackup
+ * If user touches card other than Visa SaltPay - show dialog and block next processing
+ */
+ private inline fun checkForUnfinishedBackupForSaltPay(
+ backupService: BackupService,
+ scanResponse: ScanResponse,
+ onProgressStateChange: (showProgress: Boolean) -> Unit,
+ nextHandler: (ScanResponse) -> Unit,
+ ) {
+ if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
+ nextHandler(scanResponse)
+ return
+ }
+
+ val isTheSamePrimaryCard = backupService.primaryCardId
+ ?.let { it == scanResponse.card.cardId }
+ ?: false
+
+ if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard) {
+ onProgressStateChange(false)
+ showSaltPayTapVisaLogoCardDialog()
+ } else {
+ nextHandler(scanResponse)
+ }
+ }
+
+ private suspend inline fun showDisclaimerIfNeed(
+ scanResponse: ScanResponse,
+ crossinline nextHandler: suspend (ScanResponse) -> Unit,
+ ) {
+ val disclaimerType = DisclaimerType.get(scanResponse)
+ store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
+
+ if (disclaimerType.isAccepted()) {
+ nextHandler((scanResponse))
+ } else scope.launch(Dispatchers.Main) {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ store.dispatch(
+ DisclaimerAction.Show {
+ scope.launch {
+ nextHandler(scanResponse)
+ }
+ },
+ )
+ }
+ }
+
+ private suspend inline fun onScanSuccess(
+ scanResponse: ScanResponse,
+ crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
+ crossinline onWalletNotCreated: suspend () -> Unit,
+ crossinline onSuccess: suspend (ScanResponse) -> Unit,
+ crossinline onFailure: suspend (error: TangemError) -> Unit,
+ ) {
+ Analytics.send(IntroductionProcess.CardWasScanned())
+
+ val globalState = store.state.globalState
+ val tapWalletManager = globalState.tapWalletManager
+ tapWalletManager.updateConfigManager(scanResponse)
+
+ Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
+
+ store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
+
+ if (scanResponse.isSaltPay()) {
+ if (scanResponse.isSaltPayVisa()) {
+ val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
+ val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
+ delay(500)
+ withMainContext {
+ when (result) {
+ is Result.Success -> {
+ val isOnboardingCase = result.data
+ if (isOnboardingCase) {
+ onWalletNotCreated()
+ store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
+ store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
+ store.dispatch(OnboardingSaltPayAction.Update)
+ navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onSuccess(scanResponse)
+ onProgressStateChange(false)
+ }
+ }
+ is Result.Failure -> {
+ SaltPayExceptionHandler.handle(result.error)
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onFailure(TangemSdkError.ExceptionError(result.error))
+ onProgressStateChange(false)
+ }
+ }
+ }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ if (scanResponse.card.backupStatus?.isActive == false) {
+ showSaltPayTapVisaLogoCardDialog()
+ } else {
+ onSuccess(scanResponse)
+ }
+ onProgressStateChange(false)
+ }
+ } else {
+ if (OnboardingHelper.isOnboardingCase(scanResponse)) {
+ onWalletNotCreated()
+ store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
+ val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
+ navigateTo(appScreen) { onProgressStateChange(it) }
+ } else {
+ if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
+ onWalletNotCreated()
+ store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
+ navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
+ } else {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ onSuccess(scanResponse)
+ onProgressStateChange(false)
+ }
+ }
+ }
+ }
+
+ private fun showSaltPayTapVisaLogoCardDialog() {
+ store.dispatchDialogShow(
+ AppDialog.SimpleOkDialogRes(
+ headerId = R.string.saltpay_error_empty_backup_title,
+ messageId = R.string.saltpay_error_empty_backup_message,
+ ),
+ )
+ }
+
+ private suspend inline fun navigateTo(
+ screen: AppScreen,
+ onProgressStateChange: (showProgress: Boolean) -> Unit,
+ ) {
+ delay(DELAY_SDK_DIALOG_CLOSE)
+ store.dispatchOnMain(NavigationAction.NavigateTo(screen))
+ onProgressStateChange(false)
+ }
+}
\ No newline at end of file
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 982ed86857..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
@@ -1,3 +1,5 @@
+package com.tangem.tap.domain.tasks.product
+
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@@ -9,6 +11,8 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.common.map
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@@ -19,49 +23,72 @@ import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeysMap
-import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
-import com.tangem.tap.domain.tasks.product.CreateWalletsTask
-import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
-import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.features.demo.DemoHelper
-
+import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse
data class CreateProductWalletTaskResponse(
- val card: Card,
+ val card: CardDTO,
val derivedKeys: Map = mapOf(),
- val primaryCard: PrimaryCard? = null
-) : CommandResponse
+ val primaryCard: PrimaryCard? = null,
+) : CommandResponse {
+ constructor(
+ card: Card,
+ derivedKeys: Map = mapOf(),
+ primaryCard: PrimaryCard? = null,
+ ) : this(
+ card = CardDTO(card),
+ derivedKeys = derivedKeys,
+ primaryCard = primaryCard,
+ )
+}
+
+private data class CreateWalletResponse(
+ val cardId: String,
+ val wallet: CardDTO.Wallet,
+) {
+ constructor(
+ sdkResponse: SdkCreateWalletResponse,
+ ) : this(
+ cardId = sdkResponse.cardId,
+ wallet = CardDTO.Wallet(sdkResponse.wallet),
+ )
+}
class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable {
+ override val allowsAccessCodeFromRepository: Boolean = false
+
override fun run(
session: CardSession,
- callback: (result: CompletionResult) -> Unit
+ callback: (result: CompletionResult) -> Unit,
) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
+ val cardDto = CardDTO(card)
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet()
}
- commandProcessor.proceed(card, session) {
+ commandProcessor.proceed(cardDto, session) {
when (it) {
is CompletionResult.Success -> {
val result = when (commandProcessor) {
is CreateWalletTangemWallet -> {
it.data as CreateProductWalletTaskResponse
}
+
else -> CreateProductWalletTaskResponse(card = session.environment.card!!)
}
callback(CompletionResult.Success(result))
}
+
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
@@ -70,7 +97,7 @@ class CreateProductWalletTask(
private class CreateWalletTangemNote : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -94,52 +121,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor
+ callback(result.map { CreateWalletResponse(it) })
+ }
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor {
- private lateinit var card: Card
private var primaryCard: PrimaryCard? = null
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
- this.card = card
val curves = card.getCurvesForNonCreatedWallets()
if (curves.isEmpty()) {
- val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) }
- proceedWithCreatedWallets(createWalletResponses, session, callback)
+ val createWalletResponses = card.wallets.map { wallet ->
+ CreateWalletResponse(card.cardId, wallet)
+ }
+ proceedWithCreatedWallets(card, createWalletResponses, session, callback)
return
}
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
- proceedWithCreatedWallets(result.data.createWalletResponses, session, callback)
+ proceedWithCreatedWallets(
+ card = card,
+ createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) },
+ session = session,
+ callback = callback,
+ )
+ }
+
+ is CompletionResult.Failure -> {
+ callback(CompletionResult.Failure(result.error))
}
- is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun proceedWithCreatedWallets(
+ card: CardDTO,
createWalletResponses: List,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
when {
card.settings.isBackupAllowed -> {
- linkPrimaryCard(createWalletResponses, session, callback)
+ linkPrimaryCard(card, createWalletResponses, session, callback)
}
+
card.settings.isHDWalletAllowed -> {
- deriveKeys(createWalletResponses, session, callback)
+ deriveKeys(card, createWalletResponses, session, callback)
}
+
else -> {
callback(
CompletionResult.Success(
@@ -151,6 +191,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
@@ -161,8 +202,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor {
- deriveKeys(createWalletResponse, session, callback)
+ deriveKeys(card, createWalletResponse, session, callback)
}
+
else -> {
callback(
CompletionResult.Success(
@@ -174,6 +216,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor {
callback(CompletionResult.Failure(result.error))
}
@@ -182,13 +225,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
val map = mutableMapOf>()
createWalletResponse.forEach { response ->
- val blockchainsForCurve = getBlockchains(response.cardId).filter {
+ val blockchainsForCurve = getBlockchains(response.cardId, card).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
@@ -210,17 +254,21 @@ private class CreateWalletTangemWallet : ProductCommandProcessor callback(CompletionResult.Failure(result.error))
}
}
}
- private fun getBlockchains(cardId: String): List {
+ private fun getBlockchains(
+ cardId: String,
+ card: CardDTO,
+ ): List {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
index c0810ff887..8b9d8d9d2f 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
@@ -1,15 +1,15 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
-import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
+import com.tangem.domain.common.CardDTO
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor {
fun proceed(
- card: Card,
+ card: CardDTO,
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 459191b260..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
@@ -13,6 +13,7 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
@@ -31,7 +32,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
-import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@@ -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,
@@ -53,19 +56,20 @@ class ScanProductTask(
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
+ val cardDto = CardDTO(card)
- val error = getErrorIfExcludedCard(card)
+ val error = getErrorIfExcludedCard(cardDto)
if (error != null) {
callback(CompletionResult.Failure(error))
return
}
val commandProcessor = when {
- card.isTangemNote -> ScanNoteProcessor()
- card.isTangemTwins -> ScanTwinProcessor()
+ cardDto.isTangemNote -> ScanNoteProcessor()
+ cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
- commandProcessor.proceed(card, session) { processorResult ->
+ commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
@@ -73,7 +77,7 @@ class ScanProductTask(
// it need because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
val processorScanResponseWithNewCard = processorResult.data.copy(
- card = scanTaskResult.data,
+ card = CardDTO(scanTaskResult.data),
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
@@ -85,7 +89,7 @@ class ScanProductTask(
}
}
- private fun getErrorIfExcludedCard(card: Card): TangemError? {
+ private fun getErrorIfExcludedCard(card: CardDTO): TangemError? {
if (card.isExcluded) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
return null
@@ -94,7 +98,7 @@ class ScanProductTask(
private class ScanNoteProcessor : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -117,7 +121,7 @@ private class ScanWalletProcessor(
var primaryCard: PrimaryCard? = null
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -125,7 +129,7 @@ private class ScanWalletProcessor(
}
private fun createMissingWalletsIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -159,13 +163,13 @@ private class ScanWalletProcessor(
}
private fun startLinkingForBackupIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
- if ((card.backupStatus == Card.BackupStatus.NoBackup && card.wallets.isNotEmpty())
+ if ((card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty())
&& (activationInProgress || card.isSaltPay)
) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
@@ -185,7 +189,7 @@ private class ScanWalletProcessor(
}
private fun deriveKeysIfNeeded(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
@@ -227,25 +231,46 @@ private class ScanWalletProcessor(
}
}
- private suspend fun getBlockchainsToDerive(card: Card): List {
+ private suspend fun getBlockchainsToDerive(card: CardDTO): List {
val userTokensRepository = userTokensRepository ?: return emptyList()
- val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
- mutableListOf(
- BlockchainNetwork(Blockchain.Bitcoin, card),
- BlockchainNetwork(Blockchain.Ethereum, card),
- )
- }
+ val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card)
+ .toMutableList()
+ .ifEmpty {
+ mutableListOf(
+ BlockchainNetwork(
+ blockchain = Blockchain.Bitcoin,
+ card = card,
+ ),
+ BlockchainNetwork(
+ blockchain = Blockchain.Ethereum,
+ card = card,
+ ),
+ )
+ }
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
- BlockchainNetwork(Blockchain.Ethereum, card),
- BlockchainNetwork(Blockchain.EthereumTestnet, card),
+ BlockchainNetwork(
+ blockchain = Blockchain.Ethereum,
+ card = card,
+ ),
+ BlockchainNetwork(
+ blockchain = Blockchain.EthereumTestnet,
+ card = card,
+ ),
),
)
}
if (additionalBlockchainsToDerive != null) {
- blockchainsToDerive.addAll(additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) })
+ blockchainsToDerive.addAll(
+ additionalBlockchainsToDerive.map {
+ BlockchainNetwork(
+ blockchain = it,
+ card = card,
+ )
+ },
+ )
}
if (!card.useOldStyleDerivation) {
blockchainsToDerive.removeAll(
@@ -255,13 +280,18 @@ private class ScanWalletProcessor(
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
- ).map { BlockchainNetwork(it, card) },
+ ).map {
+ BlockchainNetwork(
+ blockchain = it,
+ card = card,
+ )
+ },
)
}
return blockchainsToDerive.distinct()
}
- private suspend fun collectDerivations(card: Card): Map> {
+ private suspend fun collectDerivations(card: CardDTO): Map> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf>()
@@ -287,14 +317,14 @@ private class ScanWalletProcessor(
private class ScanTwinProcessor : ProductCommandProcessor {
override fun proceed(
- card: Card,
+ card: CardDTO,
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
- val publicKey = card.getSingleWallet()?.publicKey
+ val publicKey = card.wallets.firstOrNull()?.publicKey
if (publicKey == null) {
val response = ScanResponse(
card = card,
@@ -332,7 +362,7 @@ private class ScanTwinProcessor : ProductCommandProcessor {
}
}
-fun Card.getCurvesForNonCreatedWallets(): List {
+fun CardDTO.getCurvesForNonCreatedWallets(): List {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()
diff --git a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
index f05065bc8d..6644362bf5 100644
--- a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
+++ b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt
@@ -3,8 +3,8 @@ package com.tangem.tap.domain.termsOfUse
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
-import com.tangem.common.card.Card
-import java.util.Locale
+import com.tangem.domain.common.CardDTO
+import java.util.*
/**
[REDACTED_AUTHOR]
@@ -13,7 +13,7 @@ class CardTou {
private val locale: Locale =
ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!!
- fun getUrl(card: Card): Uri? {
+ fun getUrl(card: CardDTO): Uri? {
val issuerName = card.issuer.name
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
index d3d5836701..28bcdc9c35 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
@@ -2,10 +2,11 @@ package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.FirmwareVersion
+import com.tangem.domain.common.CardDTO
object CurrenciesRepository {
fun getBlockchains(
- cardFirmware: FirmwareVersion,
+ cardFirmware: CardDTO.FirmwareVersion,
isTestNet: Boolean = false,
): List {
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
@@ -26,6 +27,4 @@ object CurrenciesRepository {
)
}
}
-}
-
-
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
index 40b8389049..4d6646ed32 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
@@ -2,13 +2,13 @@ package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
-import com.tangem.common.card.Card
import com.tangem.common.services.Result
+import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.util.userWalletId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
-import com.tangem.tap.domain.extensions.getUserWalletId
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
@@ -23,8 +23,8 @@ class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
) {
- suspend fun getUserTokens(card: Card): List {
- val userId = card.getUserWalletId()
+ suspend fun getUserTokens(card: CardDTO): List {
+ val userId = card.userWalletId.stringValue
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
}
@@ -36,25 +36,28 @@ class UserTokensRepository(
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) }
- storageService.saveUserTokens(card.getUserWalletId(), tokens.toUserTokensResponse())
+ storageService.saveUserTokens(userId, tokens.toUserTokensResponse())
tokens.distinct()
}
+
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
}
- suspend fun saveUserTokens(card: Card, tokens: List) {
+ suspend fun saveUserTokens(card: CardDTO, tokens: List) {
+ val userId = card.userWalletId.stringValue
val userTokens = tokens.toUserTokensResponse()
- networkService.saveUserTokens(card.getUserWalletId(), userTokens)
- storageService.saveUserTokens(card.getUserWalletId(), userTokens)
+ networkService.saveUserTokens(userId, userTokens)
+ storageService.saveUserTokens(userId, userTokens)
}
- suspend fun removeUserTokens(card: Card) {
+ suspend fun removeUserTokens(card: CardDTO) {
+ val userId = card.userWalletId.stringValue
val userTokens = emptyList().toUserTokensResponse()
- networkService.saveUserTokens(card.getUserWalletId(), userTokens)
- storageService.saveUserTokens(card.getUserWalletId(), userTokens)
+ networkService.saveUserTokens(userId, userTokens)
+ storageService.saveUserTokens(userId, userTokens)
}
private fun List.toUserTokensResponse(): UserTokensResponse {
@@ -66,8 +69,8 @@ class UserTokensRepository(
)
}
- suspend fun loadBlockchainsToDerive(card: Card): List {
- val userId = card.getUserWalletId()
+ suspend fun loadBlockchainsToDerive(card: CardDTO): List {
+ val userId = card.userWalletId.stringValue
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
@@ -89,7 +92,7 @@ class UserTokensRepository(
}
private suspend fun handleGetUserTokensFailure(
- card: Card,
+ card: CardDTO,
userId: String,
error: Throwable,
): List {
@@ -107,7 +110,7 @@ class UserTokensRepository(
}
}
- private suspend fun loadTokensOffline(card: Card, userId: String): List {
+ private suspend fun loadTokensOffline(card: CardDTO, userId: String): List {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
index c71e5afa53..5f36f9bd49 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
-import com.tangem.common.card.Card
+import com.tangem.domain.common.CardDTO
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
@@ -28,7 +28,7 @@ class UserTokensStorageService(
}
@Deprecated("")
- suspend fun getUserTokens(card: Card): List {
+ suspend fun getUserTokens(card: CardDTO): List {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
index 36e376a08b..3b9558c303 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
@@ -4,8 +4,8 @@ import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
-import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateHashCode
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@JsonClass(generateAdapter = true)
@@ -15,16 +15,18 @@ data class BlockchainNetwork(
val tokens: List
) {
- constructor(blockchain: Blockchain, card: Card) : this(
+ constructor(
+ blockchain: Blockchain,
+ card: CardDTO,
+ ) : this(
blockchain = blockchain,
derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null,
- tokens = emptyList()
+ tokens = emptyList(),
)
-
fun updateTokens(tokens: List): BlockchainNetwork {
return copy(
- tokens = (this.tokens + tokens).distinct()
+ tokens = (this.tokens + tokens).distinct(),
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt
new file mode 100644
index 0000000000..d9b586d3e6
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt
@@ -0,0 +1,14 @@
+package com.tangem.tap.domain.totalBalance
+
+import com.tangem.tap.domain.model.TotalFiatBalance
+import com.tangem.tap.domain.model.WalletStoreModel
+import java.math.BigDecimal
+
+interface TotalFiatBalanceCalculator {
+ suspend fun calculate(
+ prevAmount: BigDecimal,
+ walletStores: List,
+ ): TotalFiatBalance
+
+ companion object
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt
new file mode 100644
index 0000000000..ea285cc404
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt
@@ -0,0 +1,8 @@
+package com.tangem.tap.domain.totalBalance.di
+
+import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
+import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator
+
+fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator {
+ return DefaultTotalFiatBalanceCalculator()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt
new file mode 100644
index 0000000000..be318667dd
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt
@@ -0,0 +1,102 @@
+package com.tangem.tap.domain.totalBalance.implementation
+
+import com.tangem.tap.common.extensions.toFiatValue
+import com.tangem.tap.domain.model.TotalFiatBalance
+import com.tangem.tap.domain.model.WalletDataModel
+import com.tangem.tap.domain.model.WalletStoreModel
+import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.math.BigDecimal
+
+internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
+ override suspend fun calculate(
+ prevAmount: BigDecimal,
+ walletStores: List,
+ ): TotalFiatBalance {
+ return if (walletStores.isEmpty()) {
+ TotalFiatBalance.Loading
+ } else {
+ withContext(Dispatchers.Default) {
+ val walletsData = walletStores
+ .asSequence()
+ .flatMap { it.walletsData }
+ val calculateAmount = { walletsData.calculateTotalFiatAmount() }
+
+ when (walletsData.findStatus()) {
+ TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading
+ TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount)
+ TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount())
+ TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount())
+ }
+ }
+ }
+ }
+
+ private fun Sequence.findStatus(): TotalFiatBalanceStatus {
+ return this
+ .mapToStatus()
+ .reduce { prevStatus, newStatus ->
+ getCurrentStatus(prevStatus, newStatus)
+ }
+ }
+
+ private fun Sequence.mapToStatus(): Sequence {
+ return this.map { walletData ->
+ when (walletData.status) {
+ is WalletDataModel.Refreshing -> TotalFiatBalanceStatus.Refreshing
+ is WalletDataModel.VerifiedOnline,
+ is WalletDataModel.SameCurrencyTransactionInProgress,
+ is WalletDataModel.TransactionInProgress,
+ is WalletDataModel.NoAccount,
+ -> TotalFiatBalanceStatus.Loaded
+ is WalletDataModel.Unreachable,
+ is WalletDataModel.MissedDerivation,
+ -> TotalFiatBalanceStatus.Error
+ is WalletDataModel.Loading -> TotalFiatBalanceStatus.Loading
+ }
+ }
+ }
+
+ private fun Sequence.calculateTotalFiatAmount(): BigDecimal {
+ return this
+ .map { walletData ->
+ walletData.fiatRate
+ ?.let { walletData.status.amount.toFiatValue(it) }
+ ?: BigDecimal.ZERO
+ }
+ .reduce(BigDecimal::plus)
+ }
+
+ private fun getCurrentStatus(
+ prevStatus: TotalFiatBalanceStatus,
+ newStatus: TotalFiatBalanceStatus,
+ ): TotalFiatBalanceStatus {
+ return when (prevStatus) {
+ TotalFiatBalanceStatus.Loading -> prevStatus
+ TotalFiatBalanceStatus.Refreshing -> when (newStatus) {
+ TotalFiatBalanceStatus.Loading -> prevStatus
+ TotalFiatBalanceStatus.Refreshing,
+ TotalFiatBalanceStatus.Error,
+ TotalFiatBalanceStatus.Loaded,
+ -> newStatus
+ }
+ TotalFiatBalanceStatus.Loaded,
+ TotalFiatBalanceStatus.Error,
+ -> when (newStatus) {
+ TotalFiatBalanceStatus.Loading,
+ TotalFiatBalanceStatus.Refreshing,
+ TotalFiatBalanceStatus.Error,
+ -> newStatus
+ TotalFiatBalanceStatus.Loaded -> prevStatus
+ }
+ }
+ }
+
+ private enum class TotalFiatBalanceStatus {
+ Loading,
+ Refreshing,
+ Error,
+ Loaded,
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
index 95e8753b3d..d812507524 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt
@@ -9,7 +9,6 @@ import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
class CreateFirstTwinWalletTask : CardSessionRunnable {
override fun run(
@@ -17,7 +16,7 @@ class CreateFirstTwinWalletTask : CardSessionRunnable {
callback: (result: CompletionResult) -> Unit,
) {
val card = session.environment.card
- val publicKey = card?.getSingleWallet()?.publicKey
+ val publicKey = card?.wallets?.firstOrNull()?.publicKey
if (publicKey != null) {
if (TwinsHelper.getTwinCardNumber(card.cardId) == TwinCardNumber.Second) {
callback(CompletionResult.Failure(WrongTwinCard(TwinCardNumber.First)))
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
index 8780d00055..8950bc0a02 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt
@@ -12,7 +12,6 @@ import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
@@ -23,7 +22,7 @@ class CreateSecondTwinWalletTask(
override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) {
val card = session.environment.card
- val publicKey = card?.getSingleWallet()?.publicKey
+ val publicKey = card?.wallets?.firstOrNull()?.publicKey
if (publicKey != null) {
if (TwinsHelper.getTwinCardNumber(card.cardId) == TwinCardNumber.First) {
callback(CompletionResult.Failure(WrongTwinCard(TwinCardNumber.Second)))
@@ -63,4 +62,4 @@ class CreateSecondTwinWalletTask(
}
}
}
-}
+}
\ No newline at end of file
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 ad9df6d287..9aed6f968c 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
@@ -6,9 +6,9 @@ import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
-import com.tangem.common.card.Card
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
+import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
@@ -16,19 +16,24 @@ import com.tangem.tap.common.AssetReader
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
- card: Card,
+ 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 -> {}
@@ -51,6 +56,7 @@ class TwinCardsManager(
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
+ secondCardId = response.data.cardId
}
is CompletionResult.Failure -> {}
}
@@ -59,8 +65,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)
@@ -91,7 +98,6 @@ class TwinCardsManager(
}
private class Issuer(
- val id: String,
val privateKey: String,
val publicKey: String,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
index e4ae0109bd..31d35c4a06 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt
@@ -13,7 +13,6 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.operations.issuerAndUserData.ReadIssuerDataResponse
import com.tangem.operations.issuerAndUserData.WriteIssuerDataCommand
import com.tangem.operations.sign.SignHashCommand
-import com.tangem.tap.domain.extensions.getSingleWallet
class WriteProtectedIssuerDataTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
@@ -25,7 +24,7 @@ class WriteProtectedIssuerDataTask(
) {
SignHashCommand(
twinPublicKey.calculateSha256(),
- session.environment.card!!.getSingleWallet()!!.publicKey
+ session.environment.card!!.wallets.first().publicKey,
)
.run(session) { signResult ->
when (signResult) {
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt b/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt
index 24d032e94f..d05d28217e 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/WrongTwinCard.kt
@@ -5,8 +5,7 @@ import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
-data class WrongTwinCard(private val twinCardNumber: TwinCardNumber) : TangemError {
- override val code: Int = 50005
+data class WrongTwinCard(private val twinCardNumber: TwinCardNumber) : TangemError(code = 50005) {
override var customMessage: String = tangemSdkManager.getString(
R.string.twin_error_same_card,
twinCardNumber.number,
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/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt
new file mode 100644
index 0000000000..3dc337ae61
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt
@@ -0,0 +1,31 @@
+package com.tangem.tap.domain.userWalletList
+
+import com.tangem.common.CompletionResult
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+import kotlinx.coroutines.flow.Flow
+
+interface UserWalletsListManager {
+ val userWallets: Flow>
+ val selectedUserWallet: Flow
+ val selectedUserWalletSync: UserWallet?
+ val isLocked: Flow
+ val isLockedSync: Boolean
+ val hasSavedUserWallets: Boolean
+
+ suspend fun unlockWithBiometry(): CompletionResult
+ suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult
+ fun lock()
+
+ suspend fun selectWallet(walletId: UserWalletId): CompletionResult
+
+ suspend fun save(userWallet: UserWallet): CompletionResult
+ suspend fun update(userWallet: UserWallet): CompletionResult
+
+ suspend fun delete(walletIds: List): CompletionResult
+ suspend fun clear(): CompletionResult
+
+ suspend fun get(walletId: UserWalletId): CompletionResult
+
+ companion object
+}
\ 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
new file mode 100644
index 0000000000..28475b75fa
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
@@ -0,0 +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.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.*
+
+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/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt
new file mode 100644
index 0000000000..5135966db1
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt
@@ -0,0 +1,64 @@
+package com.tangem.tap.domain.userWalletList.implementation
+
+import com.tangem.common.CompletionResult
+import com.tangem.common.catching
+import com.tangem.domain.common.util.UserWalletId
+import com.tangem.tap.domain.model.UserWallet
+import com.tangem.tap.domain.userWalletList.UserWalletsListManager
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+class DummyUserWalletsListManager : UserWalletsListManager {
+ override val userWallets: Flow>
+ get() = flowOf(emptyList())
+ override val selectedUserWallet: Flow
+ get() = flowOf()
+ override val selectedUserWalletSync: UserWallet?
+ get() = null
+ override val isLocked: Flow
+ get() = flowOf(true)
+ override val isLockedSync: Boolean
+ get() = true
+ override val hasSavedUserWallets: Boolean
+ get() = false
+
+ override suspend fun unlockWithBiometry(): CompletionResult {
+ return CompletionResult.Success(null)
+ }
+
+ override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult {
+ return CompletionResult.Success(Unit)
+ }
+
+ override fun lock() {
+ /* no-op */
+ }
+
+ override suspend fun selectWallet(walletId: UserWalletId): CompletionResult {
+ return catching {
+ error("Not implemented")
+ }
+ }
+
+ override suspend fun save(userWallet: UserWallet): CompletionResult {
+ return CompletionResult.Success(Unit)
+ }
+
+ override suspend fun update(userWallet: UserWallet): CompletionResult {
+ return CompletionResult.Success(Unit)
+ }
+
+ override suspend fun delete(walletIds: List): CompletionResult {
+ return CompletionResult.Success(Unit)
+ }
+
+ override suspend fun clear(): CompletionResult {
+ return CompletionResult.Success(Unit)
+ }
+
+ override suspend fun get(walletId: UserWalletId): CompletionResult {
+ return catching {
+ error("Not implemented")
+ }
+ }
+}
\ 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