Updated on 2026-08-14
This commit is contained in:
commit
6f4e6374d3
214 changed files with 8994 additions and 1031 deletions
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
|
|
@ -12,6 +12,7 @@
|
|||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<Properties>
|
||||
|
|
@ -199,4 +200,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
2
.idea/codeStyles/codeStyleConfig.xml
generated
2
.idea/codeStyles/codeStyleConfig.xml
generated
|
|
@ -2,4 +2,4 @@
|
|||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
|||
4
.idea/inspectionProfiles/Project_Default.xml
generated
4
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -61,8 +61,8 @@
|
|||
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSetter" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantSuspendModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantUnitExpression" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantUnitReturnType" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="RedundantVisibilityModifier" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RedundantWith" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="RemoveCurlyBracesFromTemplate" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
|
|
@ -142,5 +141,4 @@
|
|||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6
|
||||
Subproject commit bfc2bf8157089bce6b44779bdae66df2c920de70
|
||||
8
app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
Normal file
8
app/src/main/java/com/tangem/tap/ActivityResultCaller.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
|
||||
interface ActivityResultCaller {
|
||||
val activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
}
|
||||
|
|
@ -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<Intent>? = null
|
||||
private set
|
||||
|
||||
private val activities = WeakHashMap<KClass<out Activity>, 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) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
115
app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppState>
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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<WalletManager>) {
|
||||
|
|
@ -92,7 +92,7 @@ class AdditionalFeedbackInfo {
|
|||
)
|
||||
}
|
||||
|
||||
private fun formatSignedHashes(wallets: List<CardWallet>): String {
|
||||
private fun formatSignedHashes(wallets: List<CardDTO.Wallet>): String {
|
||||
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Middleware<AppState>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.tap.lockUserWalletsTimer
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class LockUserWalletsTimerMiddleware {
|
||||
val middleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
lockUserWalletsTimer?.restart()
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AppCompatActivity>) : NavigationAction()
|
||||
|
|
|
|||
|
|
@ -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<AppState> = { dispatch, state ->
|
||||
val navigationMiddleware: Middleware<AppState> = { _, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
if (action is NavigationAction) {
|
||||
|
|
@ -16,16 +22,24 @@ val navigationMiddleware: Middleware<AppState> = { 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<AppState> = { 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)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import org.rekotlin.StateType
|
|||
import java.lang.ref.WeakReference
|
||||
|
||||
data class NavigationState(
|
||||
val backStack: List<AppScreen> = listOf(AppScreen.Home),
|
||||
val backStack: List<AppScreen> = emptyList(),
|
||||
val activity: WeakReference<AppCompatActivity>? = null,
|
||||
) : StateType
|
||||
|
||||
|
|
@ -22,5 +22,8 @@ enum class AppScreen(
|
|||
AddTokens, AddCustomToken,
|
||||
WalletConnectSessions,
|
||||
QrScan,
|
||||
ReferralProgram
|
||||
ReferralProgram,
|
||||
Welcome,
|
||||
SaveWallet(isDialogFragment = true),
|
||||
WalletSelector(isDialogFragment = true),
|
||||
}
|
||||
|
|
@ -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<Blockchain>? = null,
|
||||
messageRes: Int? = null,
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
): CompletionResult<ScanResponse> {
|
||||
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<Card> {
|
||||
suspend fun createWallet(cardId: String?): CompletionResult<CardDTO> {
|
||||
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<ByteArrayKey, List<DerivationPath>>,
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
): CompletionResult<DerivationTaskResponse> {
|
||||
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
|
||||
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
|
||||
}
|
||||
|
||||
suspend fun resetToFactorySettings(card: Card): CompletionResult<Card> {
|
||||
suspend fun resetToFactorySettings(cardId: String): CompletionResult<CardDTO> {
|
||||
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<Unit> {
|
||||
return biometricManager.authenticate(
|
||||
mode = BiometricManager.AuthenticationMode.Keys(
|
||||
USER_WALLETS_BIOMETRIC_KEY_NAME,
|
||||
tangemSdk.config.userCodesBiometricKeyName,
|
||||
),
|
||||
)
|
||||
.map { /* no-op */ }
|
||||
}
|
||||
|
||||
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().save(
|
||||
cardIds = cardsIds,
|
||||
userCode = UserCode(
|
||||
type = UserCodeType.AccessCode,
|
||||
stringValue = accessCode,
|
||||
),
|
||||
)
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
|
||||
return createUserCodeRepository().clear()
|
||||
.map {
|
||||
biometricManager.unauthenticate(
|
||||
keyName = tangemSdk.config.userCodesBiometricKeyName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
|
||||
|
|
@ -128,11 +190,17 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun scanCard(): CompletionResult<Card> {
|
||||
suspend fun scanCard(
|
||||
cardId: String? = null,
|
||||
useBiometricsForAccessCode: Boolean = false,
|
||||
): CompletionResult<CardDTO> {
|
||||
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
|
||||
return runTaskAsyncReturnOnMain(
|
||||
ScanTask(),
|
||||
runnable = ScanTask(),
|
||||
cardId = cardId,
|
||||
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
|
||||
)
|
||||
.map { CardDTO(it) }
|
||||
}
|
||||
|
||||
suspend fun <T : CommandResponse> runTaskAsync(
|
||||
|
|
@ -150,7 +218,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
|
||||
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
|
||||
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
|
||||
runnable: CardSessionRunnable<T>,
|
||||
cardId: String? = null,
|
||||
initialMessage: Message? = null,
|
||||
): CompletionResult<T> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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<BlockchainNetwork>, 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<CardVerifyAndGetInfo.Response.Item>? = null): String {
|
||||
suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = 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<CardVerifyAndGetInfo.R
|
|||
}
|
||||
}
|
||||
|
||||
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
|
||||
return when (cardInfoResult) {
|
||||
return when (val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
|
||||
is Result.Success -> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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>): CardWallet? {
|
||||
private fun selectWallet(wallets: List<CardDTO.Wallet>): CardDTO.Wallet? {
|
||||
return when (wallets.size) {
|
||||
0 -> null
|
||||
1 -> wallets[0]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
27
app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
Normal file
27
app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
Normal file
|
|
@ -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<String>,
|
||||
val scanResponse: ScanResponse,
|
||||
) {
|
||||
val cardId: String
|
||||
get() = scanResponse.card.cardId
|
||||
|
||||
internal var isSaved: Boolean = true
|
||||
}
|
||||
|
|
@ -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<AddressData>,
|
||||
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<PendingTransaction> = 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<PendingTransaction>,
|
||||
) : Status()
|
||||
|
||||
data class SameCurrencyTransactionInProgress(
|
||||
override val amount: BigDecimal,
|
||||
override val pendingTransactions: List<PendingTransaction>,
|
||||
) : 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<PendingTransaction>,
|
||||
override val errorMessage: String?,
|
||||
) : Status() {
|
||||
override val isErrorStatus: Boolean = errorMessage != null
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletDataModel>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String> = emptySet()
|
||||
|
||||
fun backupCardsIds(backupCardsIds: Set<String>?) = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletDataModel> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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<Blockchain>? = 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
|
||||
val primaryCard: PrimaryCard? = null
|
||||
) : CommandResponse
|
||||
val primaryCard: PrimaryCard? = null,
|
||||
) : CommandResponse {
|
||||
constructor(
|
||||
card: Card,
|
||||
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = 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<CreateProductWalletTaskResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean = false
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateWalletResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -94,52 +121,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
|
|||
} else {
|
||||
intersectCurves[0]
|
||||
}
|
||||
CreateWalletTask(curve).run(session, callback)
|
||||
CreateWalletTask(curve).run(session) { result ->
|
||||
callback(result.map { CreateWalletResponse(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
|
||||
|
||||
private lateinit var card: Card
|
||||
private var primaryCard: PrimaryCard? = null
|
||||
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> 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<CreateProductWa
|
|||
}
|
||||
|
||||
private fun linkPrimaryCard(
|
||||
card: CardDTO,
|
||||
createWalletResponse: List<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
|
|
@ -161,8 +202,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
primaryCard = result.data
|
||||
when {
|
||||
card.settings.isHDWalletAllowed -> {
|
||||
deriveKeys(createWalletResponse, session, callback)
|
||||
deriveKeys(card, createWalletResponse, session, callback)
|
||||
}
|
||||
|
||||
else -> {
|
||||
callback(
|
||||
CompletionResult.Success(
|
||||
|
|
@ -174,6 +216,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is CompletionResult.Failure -> {
|
||||
callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
|
|
@ -182,13 +225,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
}
|
||||
|
||||
private fun deriveKeys(
|
||||
card: CardDTO,
|
||||
createWalletResponse: List<CreateWalletResponse>,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
|
||||
) {
|
||||
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
||||
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<CreateProductWa
|
|||
CreateProductWalletTaskResponse(
|
||||
card = session.environment.card!!,
|
||||
derivedKeys = result.data.entries,
|
||||
primaryCard = primaryCard
|
||||
)
|
||||
)
|
||||
primaryCard = primaryCard,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockchains(cardId: String): List<Blockchain> {
|
||||
private fun getBlockchains(
|
||||
cardId: String,
|
||||
card: CardDTO,
|
||||
): List<Blockchain> {
|
||||
return when {
|
||||
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
|
||||
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
|
||||
|
|
|
|||
|
|
@ -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<T> {
|
||||
fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<T>) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Blockchain>? = null,
|
||||
) : CardSessionRunnable<ScanResponse> {
|
||||
|
||||
override val allowsAccessCodeFromRepository: Boolean
|
||||
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
|
||||
|
||||
override fun run(
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -117,7 +121,7 @@ private class ScanWalletProcessor(
|
|||
|
||||
var primaryCard: PrimaryCard? = null
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -125,7 +129,7 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private fun createMissingWalletsIfNeeded(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -159,13 +163,13 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
|
||||
private fun startLinkingForBackupIfNeeded(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse>) -> Unit,
|
||||
) {
|
||||
|
|
@ -227,25 +231,46 @@ private class ScanWalletProcessor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
private suspend fun getBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
|
||||
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<ByteArrayKey, List<DerivationPath>> {
|
||||
private suspend fun collectDerivations(card: CardDTO): Map<ByteArrayKey, List<DerivationPath>> {
|
||||
val blockchains = getBlockchainsToDerive(card)
|
||||
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
|
||||
|
||||
|
|
@ -287,14 +317,14 @@ private class ScanWalletProcessor(
|
|||
|
||||
private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
|
||||
override fun proceed(
|
||||
card: Card,
|
||||
card: CardDTO,
|
||||
session: CardSession,
|
||||
callback: (result: CompletionResult<ScanResponse>) -> 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<ScanResponse> {
|
|||
}
|
||||
}
|
||||
|
||||
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
|
||||
fun CardDTO.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
|
||||
val curvesPresent = wallets.map { it.curve }.toSet()
|
||||
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
|
||||
return curvesForNonCreatedWallets.toList()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Blockchain> {
|
||||
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
|
||||
|
|
@ -26,6 +27,4 @@ object CurrenciesRepository {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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<Currency> {
|
||||
val userId = card.getUserWalletId()
|
||||
suspend fun getUserTokens(card: CardDTO): List<Currency> {
|
||||
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<Currency>) {
|
||||
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) {
|
||||
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<Currency>().toUserTokensResponse()
|
||||
networkService.saveUserTokens(card.getUserWalletId(), userTokens)
|
||||
storageService.saveUserTokens(card.getUserWalletId(), userTokens)
|
||||
networkService.saveUserTokens(userId, userTokens)
|
||||
storageService.saveUserTokens(userId, userTokens)
|
||||
}
|
||||
|
||||
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
|
||||
|
|
@ -66,8 +69,8 @@ class UserTokensRepository(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun loadBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
|
||||
val userId = card.getUserWalletId()
|
||||
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
|
||||
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<Currency> {
|
||||
|
|
@ -107,7 +110,7 @@ class UserTokensRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun loadTokensOffline(card: Card, userId: String): List<Currency> {
|
||||
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> {
|
||||
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Currency> {
|
||||
suspend fun getUserTokens(card: CardDTO): List<Currency> {
|
||||
val blockchainNetworks =
|
||||
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
|
||||
return blockchainNetworks.flatMap { it.toCurrencies() }
|
||||
|
|
|
|||
|
|
@ -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<Token>
|
||||
) {
|
||||
|
||||
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<Token>): BlockchainNetwork {
|
||||
return copy(
|
||||
tokens = (this.tokens + tokens).distinct()
|
||||
tokens = (this.tokens + tokens).distinct(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<WalletStoreModel>,
|
||||
): TotalFiatBalance
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<WalletStoreModel>,
|
||||
): 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<WalletDataModel>.findStatus(): TotalFiatBalanceStatus {
|
||||
return this
|
||||
.mapToStatus()
|
||||
.reduce { prevStatus, newStatus ->
|
||||
getCurrentStatus(prevStatus, newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Sequence<WalletDataModel>.mapToStatus(): Sequence<TotalFiatBalanceStatus> {
|
||||
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<WalletDataModel>.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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CreateWalletResponse> {
|
||||
override fun run(
|
||||
|
|
@ -17,7 +16,7 @@ class CreateFirstTwinWalletTask : CardSessionRunnable<CreateWalletResponse> {
|
|||
callback: (result: CompletionResult<CreateWalletResponse>) -> 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)))
|
||||
|
|
|
|||
|
|
@ -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<CreateWalletResponse>) -> 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(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CreateWalletResponse> {
|
||||
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<ScanResponse> {
|
||||
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,
|
||||
)
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<UserWallet>>
|
||||
val selectedUserWallet: Flow<UserWallet>
|
||||
val selectedUserWalletSync: UserWallet?
|
||||
val isLocked: Flow<Boolean>
|
||||
val isLockedSync: Boolean
|
||||
val hasSavedUserWallets: Boolean
|
||||
|
||||
suspend fun unlockWithBiometry(): CompletionResult<UserWallet?>
|
||||
suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit>
|
||||
fun lock()
|
||||
|
||||
suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
|
||||
suspend fun update(userWallet: UserWallet): CompletionResult<Unit>
|
||||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<List<UserWallet>>
|
||||
get() = state
|
||||
.mapLatest { it.wallets }
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = state
|
||||
.mapLatest { state ->
|
||||
state.wallets.find {
|
||||
it.walletId == state.selectedWalletId
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = findSelectedWallet()
|
||||
|
||||
override val isLocked: Flow<Boolean>
|
||||
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<UserWallet?> {
|
||||
return unlockWithBiometryInternal()
|
||||
.map { selectedUserWalletSync }
|
||||
}
|
||||
|
||||
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
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<UserWallet> = 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<Unit> {
|
||||
return saveInternal(userWallet, override = false)
|
||||
}
|
||||
|
||||
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return saveInternal(userWallet, override = true)
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
if (state.value.isLocked) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val walletIdsToRemove = state.value.wallets
|
||||
.map { it.walletId }
|
||||
.filter { it in walletIds }
|
||||
|
||||
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<Unit> {
|
||||
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<UserWallet> = withUnlock {
|
||||
return catching {
|
||||
state.value.wallets.first { it.walletId == walletId }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveInternal(
|
||||
userWallet: UserWallet,
|
||||
override: Boolean,
|
||||
): CompletionResult<Unit> = 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 <reified T> withUnlock(
|
||||
block: () -> CompletionResult<T>,
|
||||
): CompletionResult<T> {
|
||||
return (if (state.value.isLocked) unlockWithBiometryInternal() else CompletionResult.Success(Unit))
|
||||
.flatMap { block() }
|
||||
}
|
||||
|
||||
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
|
||||
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<Unit> {
|
||||
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<List<UserWallet>> {
|
||||
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<UserWallet>,
|
||||
): UserWalletId? {
|
||||
return prevSelectedWalletId
|
||||
?: (selectedUserWalletRepository.get()
|
||||
?: (userWallets.firstOrNull()?.walletId
|
||||
?.also { selectedUserWalletRepository.set(it) }))
|
||||
}
|
||||
|
||||
private fun changeSelectedWalletIfNeeded(
|
||||
walletsIdsToRemove: List<UserWalletId>,
|
||||
) {
|
||||
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<UserWalletEncryptionKey> = emptyList(),
|
||||
val wallets: List<UserWallet> = emptyList(),
|
||||
val selectedWalletId: UserWalletId? = null,
|
||||
val isLocked: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<List<UserWallet>>
|
||||
get() = flowOf(emptyList())
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = flowOf()
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = null
|
||||
override val isLocked: Flow<Boolean>
|
||||
get() = flowOf(true)
|
||||
override val isLockedSync: Boolean
|
||||
get() = true
|
||||
override val hasSavedUserWallets: Boolean
|
||||
get() = false
|
||||
|
||||
override suspend fun unlockWithBiometry(): CompletionResult<UserWallet?> {
|
||||
return CompletionResult.Success(null)
|
||||
}
|
||||
|
||||
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override fun lock() {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> {
|
||||
return catching {
|
||||
error("Not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CardDTO.Wallet>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserWalletPublicInformation(
|
||||
val name: String,
|
||||
val walletId: UserWalletId,
|
||||
val artworkUrl: String,
|
||||
val cardsInWallet: Set<String>,
|
||||
val scanResponse: ScanResponse,
|
||||
)
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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<List<UserWalletEncryptionKey>>
|
||||
suspend fun save(walletId: UserWalletId, encryptionKey: ByteArray): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -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<Unit>
|
||||
|
||||
suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>>
|
||||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -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<Unit>
|
||||
suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>
|
||||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.biometric.BiometricManager
|
||||
import com.tangem.common.biometric.BiometricStorage
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
||||
|
||||
internal class BiometricUserWalletsKeysRepository(
|
||||
biometricKeyName: String,
|
||||
moshi: Moshi,
|
||||
secureStorage: SecureStorage,
|
||||
biometricManager: BiometricManager,
|
||||
) : UserWalletsKeysRepository {
|
||||
private val biometricStorage = BiometricStorage(
|
||||
biometricKeyName = biometricKeyName,
|
||||
biometricManager = biometricManager,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
private val walletsKeysAdapter: JsonAdapter<List<UserWalletEncryptionKey>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, UserWalletEncryptionKey::class.java),
|
||||
)
|
||||
|
||||
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return biometricStorage.get(key = StorageKey.WalletEncryptionKeys.name)
|
||||
.map { encryptionKeys ->
|
||||
encryptionKeys.decodeToKeys()
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.ReceiveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(
|
||||
walletId: UserWalletId,
|
||||
encryptionKey: ByteArray,
|
||||
): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.flatMap { keys ->
|
||||
if (keys.any { it.walletId == walletId }) {
|
||||
return@flatMap CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
val encodedKeys = keys.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(UserWalletEncryptionKey(walletId, encryptionKey)) {
|
||||
it.walletId == walletId
|
||||
}
|
||||
}
|
||||
.encode()
|
||||
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveEncryptionKeysError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<List<UserWalletEncryptionKey>> {
|
||||
return getAll()
|
||||
.map { keys ->
|
||||
val keysToRemove = keys.filter { it.walletId in walletIds }.toSet()
|
||||
(keys - keysToRemove).encode()
|
||||
}
|
||||
.flatMap { encodedKeys ->
|
||||
biometricStorage.store(
|
||||
key = StorageKey.WalletEncryptionKeys.name,
|
||||
data = encodedKeys,
|
||||
)
|
||||
}
|
||||
.flatMap { getAll() }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return biometricStorage.delete(key = StorageKey.WalletEncryptionKeys.name)
|
||||
}
|
||||
|
||||
private fun List<UserWalletEncryptionKey>.encode(): ByteArray {
|
||||
return this.let(walletsKeysAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
}
|
||||
|
||||
private fun ByteArray?.decodeToKeys(): List<UserWalletEncryptionKey> {
|
||||
return this?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(walletsKeysAdapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
WalletEncryptionKeys
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
|
||||
|
||||
internal class DefaultSelectedUserWalletRepository(
|
||||
private val secureStorage: SecureStorage,
|
||||
) : SelectedUserWalletRepository {
|
||||
override fun get(): UserWalletId? {
|
||||
return secureStorage.get(StorageKey.SelectedWalletId.name)
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let { UserWalletId(it) }
|
||||
}
|
||||
|
||||
override fun set(walletId: UserWalletId?) {
|
||||
if (walletId == null) {
|
||||
secureStorage.delete(StorageKey.SelectedWalletId.name)
|
||||
} else {
|
||||
secureStorage.store(
|
||||
data = walletId.stringValue.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
account = StorageKey.SelectedWalletId.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
SelectedWalletId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.publicInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultUserWalletsPublicInformationRepository(
|
||||
moshi: Moshi,
|
||||
private val secureStorage: SecureStorage,
|
||||
) : UserWalletsPublicInformationRepository {
|
||||
private val publicInformationAdapter: JsonAdapter<List<UserWalletPublicInformation>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
|
||||
)
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return getAll()
|
||||
.flatMap { savedInformation ->
|
||||
val infoToSave = withContext(Dispatchers.Default) {
|
||||
savedInformation.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(userWallet.publicInformation) {
|
||||
userWallet.walletId == it.walletId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save(infoToSave)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>> = catching {
|
||||
withContext(Dispatchers.IO) {
|
||||
secureStorage.get(StorageKey.UserWalletPublicInformation.name)
|
||||
?.decodeToString()
|
||||
?.let(publicInformationAdapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
|
||||
return getAll()
|
||||
.flatMap { publicInformation ->
|
||||
val infoToRemove = publicInformation
|
||||
.filter { it.walletId in walletIds }
|
||||
.toSet()
|
||||
|
||||
save(
|
||||
publicInformation = publicInformation - infoToRemove,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> = catching {
|
||||
secureStorage.delete(StorageKey.UserWalletPublicInformation.name)
|
||||
}
|
||||
|
||||
@JvmName("saveWithPublicInformation")
|
||||
private suspend fun save(
|
||||
publicInformation: List<UserWalletPublicInformation>,
|
||||
): CompletionResult<Unit> = catching {
|
||||
withContext(Dispatchers.IO) {
|
||||
publicInformation
|
||||
.let(publicInformationAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
.also { secureStorage.store(it, StorageKey.UserWalletPublicInformation.name) }
|
||||
}
|
||||
}
|
||||
|
||||
private enum class StorageKey {
|
||||
UserWalletPublicInformation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.domain.common.util.encryptionKey
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.UserWalletListError
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal class DefaultUserWalletsSensitiveInformationRepository(
|
||||
moshi: Moshi,
|
||||
private val secureStorage: SecureStorage,
|
||||
) : UserWalletsSensitiveInformationRepository {
|
||||
private val sensitiveInformationAdapter: JsonAdapter<UserWalletSensitiveInformation> = moshi.adapter(
|
||||
UserWalletSensitiveInformation::class.java,
|
||||
)
|
||||
private val cipher: Cipher by lazy {
|
||||
Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding")
|
||||
}
|
||||
|
||||
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
|
||||
return catching {
|
||||
withContext(Dispatchers.Default) {
|
||||
userWallet.sensitiveInformation
|
||||
.let(sensitiveInformationAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
.encryptAndStoreIv(
|
||||
walletId = userWallet.walletId,
|
||||
encryptionKey = userWallet.scanResponse.card.encryptionKey,
|
||||
)
|
||||
.let { encryptedInformation ->
|
||||
withContext(Dispatchers.IO) {
|
||||
secureStorage.store(
|
||||
data = encryptedInformation,
|
||||
account = StorageKey.SensitiveInformation(userWallet.walletId).name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.SaveSensitiveInformationError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAll(
|
||||
encryptionKeys: List<UserWalletEncryptionKey>,
|
||||
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>> {
|
||||
return catching {
|
||||
if (encryptionKeys.isEmpty()) {
|
||||
return@catching emptyMap()
|
||||
}
|
||||
|
||||
val keyToEncryptedInformation = withContext(Dispatchers.IO) {
|
||||
encryptionKeys.associateWith { encryptionKey ->
|
||||
secureStorage.get(StorageKey.SensitiveInformation(encryptionKey.walletId).name)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val keyToInformation =
|
||||
mutableMapOf<UserWalletId, UserWalletSensitiveInformation>()
|
||||
|
||||
keyToEncryptedInformation.forEach { (key, encryptedInformation) ->
|
||||
val information = encryptedInformation
|
||||
?.getIvAndDecrypt(
|
||||
walletId = key.walletId,
|
||||
encryptionKey = key.encryptionKey,
|
||||
)
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(sensitiveInformationAdapter::fromJson)
|
||||
|
||||
if (information != null) {
|
||||
keyToInformation[key.walletId] = information
|
||||
}
|
||||
}
|
||||
|
||||
keyToInformation
|
||||
}
|
||||
}
|
||||
.mapFailure { error ->
|
||||
UserWalletListError.ReceiveSensitiveInformationError(error.cause ?: error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletIds
|
||||
.forEach { walletId ->
|
||||
secureStorage.delete(StorageKey.SensitiveInformation(walletId).name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.encryptAndStoreIv(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray {
|
||||
val secretKey = SecretKeySpec(encryptionKey, algorithm)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
val encryptedData = cipher.doFinal(this)
|
||||
secureStorage.store(data = cipher.iv, account = StorageKey.SensitiveInformationIv(walletId).name)
|
||||
return encryptedData
|
||||
}
|
||||
|
||||
private fun ByteArray.getIvAndDecrypt(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray? {
|
||||
val iv = secureStorage.get(StorageKey.SensitiveInformationIv(walletId).name)
|
||||
?: error("IV not found")
|
||||
val ivParam = IvParameterSpec(iv)
|
||||
val secretKeySpec = SecretKeySpec(encryptionKey, algorithm)
|
||||
return cipher
|
||||
.also { it.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParam) }
|
||||
.doFinal(this)
|
||||
}
|
||||
|
||||
private sealed interface StorageKey {
|
||||
val name: String
|
||||
|
||||
class SensitiveInformation(walletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_sensitive_information_${walletId.stringValue}"
|
||||
}
|
||||
|
||||
class SensitiveInformationIv(walletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_sensitive_information_iv_${walletId.stringValue}"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val algorithm = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val blockMode = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val encryptionPadding = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
internal fun List<ByteArray>.containsBA(element: ByteArray?): Boolean {
|
||||
this.forEach {
|
||||
if (it.contentEquals(element)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils
|
||||
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
|
||||
|
||||
internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
|
||||
get() = UserWalletSensitiveInformation(scanResponse.card.wallets)
|
||||
|
||||
internal val UserWallet.publicInformation: UserWalletPublicInformation
|
||||
get() = UserWalletPublicInformation(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
artworkUrl = artworkUrl,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse.copy(
|
||||
card = scanResponse.card.copy(
|
||||
wallets = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
|
||||
return UserWallet(
|
||||
name = name,
|
||||
walletId = walletId,
|
||||
artworkUrl = artworkUrl,
|
||||
cardsInWallet = cardsInWallet,
|
||||
scanResponse = scanResponse,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet> {
|
||||
return this.map { it.toUserWallet() }
|
||||
}
|
||||
|
||||
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
card = scanResponse.card.copy(
|
||||
wallets = sensitiveInformation.wallets,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<UserWallet>.updateWith(
|
||||
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
|
||||
): List<UserWallet> {
|
||||
return if (walletIdToSensitiveInformation.isEmpty()) this else {
|
||||
this.map { wallet ->
|
||||
walletIdToSensitiveInformation[wallet.walletId]
|
||||
?.let(wallet::updateWith)
|
||||
?: wallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
|
||||
internal class ByteArrayKeyAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: ByteArrayKey,
|
||||
byteArrayAdapter: JsonAdapter<ByteArray>,
|
||||
) {
|
||||
byteArrayAdapter.toJson(writer, src.bytes)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
byteArrayAdapter: JsonAdapter<ByteArray>,
|
||||
): ByteArrayKey? {
|
||||
return byteArrayAdapter.fromJson(reader)?.let {
|
||||
ByteArrayKey(bytes = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.domain.common.CardDTO
|
||||
|
||||
internal class CardBackupStatusAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: CardDTO.BackupStatus?,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
when (src) {
|
||||
is CardDTO.BackupStatus.Active -> {
|
||||
jsonMap["status"] = "active"
|
||||
jsonMap["cardCount"] = src.cardCount.toString()
|
||||
}
|
||||
is CardDTO.BackupStatus.CardLinked -> {
|
||||
jsonMap["status"] = "card_linked"
|
||||
jsonMap["cardCount"] = src.cardCount.toString()
|
||||
}
|
||||
is CardDTO.BackupStatus.NoBackup -> {
|
||||
jsonMap["status"] = "no_backup"
|
||||
}
|
||||
null -> {
|
||||
jsonMap["status"] = "null"
|
||||
}
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
): CardDTO.BackupStatus? {
|
||||
val map = mapAdapter.fromJson(reader) ?: return null
|
||||
|
||||
return when (map["status"]) {
|
||||
"active" -> CardDTO.BackupStatus.Active(
|
||||
cardCount = map["cardCount"]?.toInt() ?: 0,
|
||||
)
|
||||
"card_linked" -> CardDTO.BackupStatus.CardLinked(
|
||||
cardCount = map["cardCount"]?.toInt() ?: 0,
|
||||
)
|
||||
"no_backup" -> CardDTO.BackupStatus.NoBackup
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
internal class ExtendedPublicKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: ExtendedPublicKeysMap,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (derivationPath, extendedPublicKey) ->
|
||||
val derivationPathJson = derivationPathAdapter.toJson(derivationPath)
|
||||
val derivationPathEncoded = derivationPathJson.encodeToByteArray().toHexString()
|
||||
val extendedPublicKeyJson = extendedPublicKeyAdapter.toJson(extendedPublicKey)
|
||||
|
||||
jsonMap[derivationPathEncoded] = extendedPublicKeyJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
): ExtendedPublicKeysMap {
|
||||
val map = mutableMapOf<DerivationPath, ExtendedPublicKey>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (derivationPathEncoded, extendedPublicKeyJson) ->
|
||||
val derivationPathJson = derivationPathEncoded.hexToBytes().decodeToString()
|
||||
val derivationPath = derivationPathAdapter.fromJson(derivationPathJson)
|
||||
val extendedPublicKey = extendedPublicKeyAdapter.fromJson(extendedPublicKeyJson)
|
||||
|
||||
if (derivationPath != null && extendedPublicKey != null) {
|
||||
map[derivationPath] = extendedPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
return ExtendedPublicKeysMap(map)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
internal class ScanResponseDerivedKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: Map<ByteArrayKey, ExtendedPublicKeysMap>,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
byteArrayKeyAdapter: JsonAdapter<ByteArrayKey>,
|
||||
extendedPublicKeysMapAdapter: JsonAdapter<ExtendedPublicKeysMap>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (key, extendedPublicKeysMap) ->
|
||||
val keyJson = byteArrayKeyAdapter.toJson(key)
|
||||
val extendedPublicKeysMapJson = extendedPublicKeysMapAdapter.toJson(extendedPublicKeysMap)
|
||||
|
||||
jsonMap[keyJson] = extendedPublicKeysMapJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
byteArrayKeyAdapter: JsonAdapter<ByteArrayKey>,
|
||||
extendedPublicKeysMapAdapter: JsonAdapter<ExtendedPublicKeysMap>,
|
||||
): Map<ByteArrayKey, ExtendedPublicKeysMap> {
|
||||
val map = mutableMapOf<ByteArrayKey, ExtendedPublicKeysMap>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (keyJson, extendedPublicKeysMapJson) ->
|
||||
val key = byteArrayKeyAdapter.fromJson(keyJson)
|
||||
val extendedPublicKeysMap = extendedPublicKeysMapAdapter.fromJson(extendedPublicKeysMapJson)
|
||||
|
||||
if (key != null && extendedPublicKeysMap != null) {
|
||||
map[key] = extendedPublicKeysMap
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tap.domain.userWalletList.utils.json
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
|
||||
internal class WalletDerivedKeysMapAdapter {
|
||||
@ToJson
|
||||
fun toJson(
|
||||
writer: JsonWriter,
|
||||
src: Map<DerivationPath, ExtendedPublicKey>,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
) {
|
||||
val jsonMap = mutableMapOf<String, String>()
|
||||
|
||||
src.forEach { (derivationPath, extendedPublicKey) ->
|
||||
val derivationPathJson = derivationPathAdapter.toJson(derivationPath)
|
||||
val derivationPathEncoded = derivationPathJson.encodeToByteArray().toHexString()
|
||||
val extendedPublicKeyJson = extendedPublicKeyAdapter.toJson(extendedPublicKey)
|
||||
|
||||
jsonMap[derivationPathEncoded] = extendedPublicKeyJson
|
||||
}
|
||||
|
||||
mapAdapter.toJson(writer, jsonMap)
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(
|
||||
reader: JsonReader,
|
||||
mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
derivationPathAdapter: JsonAdapter<DerivationPath>,
|
||||
extendedPublicKeyAdapter: JsonAdapter<ExtendedPublicKey>,
|
||||
): Map<DerivationPath, ExtendedPublicKey> {
|
||||
val map = mutableMapOf<DerivationPath, ExtendedPublicKey>()
|
||||
|
||||
mapAdapter.fromJson(reader)?.forEach { (derivationPathEncoded, extendedPublicKeyJson) ->
|
||||
val derivationPathJson = derivationPathEncoded.hexToBytes().decodeToString()
|
||||
val derivationPath = derivationPathAdapter.fromJson(derivationPathJson)
|
||||
val extendedPublicKey = extendedPublicKeyAdapter.fromJson(extendedPublicKeyJson)
|
||||
|
||||
if (derivationPath != null && extendedPublicKey != null) {
|
||||
map[derivationPath] = extendedPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletCurrencies
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
|
||||
interface WalletCurrenciesManager {
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun addCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToAdd: List<Currency>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun removeCurrency(
|
||||
userWallet: UserWallet,
|
||||
currencyToRemove: Currency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun removeCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToRemove: List<Currency>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.domain.walletCurrencies.di
|
||||
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletCurrencies.implementation.DefaultWalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
|
||||
fun WalletCurrenciesManager.Companion.provideDefaultImplementation(
|
||||
userTokensRepository: UserTokensRepository,
|
||||
walletStoresRepository: WalletStoresRepository,
|
||||
walletAmountsRepository: WalletAmountsRepository,
|
||||
walletManagersRepository: WalletManagersRepository,
|
||||
appCurrencyProvider: () -> FiatCurrency,
|
||||
): WalletCurrenciesManager {
|
||||
return DefaultWalletCurrenciesManager(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.tap.domain.walletCurrencies.implementation
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
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.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.getTokens
|
||||
import com.tangem.tap.features.wallet.models.toCurrencies
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletCurrenciesManager(
|
||||
private val userTokensRepository: UserTokensRepository,
|
||||
private val walletStoresRepository: WalletStoresRepository,
|
||||
private val walletAmountsRepository: WalletAmountsRepository,
|
||||
private val walletManagersRepository: WalletManagersRepository,
|
||||
private val appCurrencyProvider: () -> FiatCurrency,
|
||||
) : WalletCurrenciesManager {
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork,
|
||||
): CompletionResult<Unit> {
|
||||
val walletStore = walletStoresRepository.get(userWallet.walletId).first()
|
||||
.find {
|
||||
it.blockchainNetwork.blockchain == blockchainNetwork.blockchain
|
||||
&& it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath
|
||||
}
|
||||
|
||||
return if (walletStore != null) {
|
||||
walletAmountsRepository.update(
|
||||
userWallet = userWallet,
|
||||
walletStore = walletStore,
|
||||
fiatCurrency = appCurrencyProvider(),
|
||||
)
|
||||
} else CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun addCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToAdd: List<Currency>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
var newBlockchainNetworks = listOf<BlockchainNetwork>()
|
||||
catching {
|
||||
val card = userWallet.scanResponse.card
|
||||
val savedCurrencies = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(card)
|
||||
}
|
||||
newBlockchainNetworks = (savedCurrencies + currenciesToAdd)
|
||||
.toBlockchainNetworks(userWallet.scanResponse.card)
|
||||
val newCurrencies = newBlockchainNetworks.toCurrencies()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = card,
|
||||
tokens = newCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
newBlockchainNetworks.updateWalletStores(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeCurrency(
|
||||
userWallet: UserWallet,
|
||||
currencyToRemove: Currency,
|
||||
): CompletionResult<Unit> {
|
||||
return removeCurrencies(userWallet, listOf(currencyToRemove))
|
||||
}
|
||||
|
||||
override suspend fun removeCurrencies(
|
||||
userWallet: UserWallet,
|
||||
currenciesToRemove: List<Currency>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
var remainingBlockchainsNetworks = emptyList<BlockchainNetwork>()
|
||||
catching {
|
||||
val card = userWallet.scanResponse.card
|
||||
val savedCurrencies = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(card)
|
||||
}
|
||||
|
||||
val remainingCurrencies = arrayListOf<Currency>()
|
||||
savedCurrencies.forEach { savedCurrency ->
|
||||
if (savedCurrency !in currenciesToRemove) {
|
||||
remainingCurrencies.add(savedCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
remainingBlockchainsNetworks = remainingCurrencies.toBlockchainNetworks(userWallet.scanResponse.card)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
userTokensRepository.saveUserTokens(
|
||||
card = card,
|
||||
tokens = remainingCurrencies,
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
remainingBlockchainsNetworks.updateWalletStores(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Currency>.toBlockchainNetworks(card: CardDTO): List<BlockchainNetwork> {
|
||||
val blockchainNetworks = arrayListOf<BlockchainNetwork>()
|
||||
val findDerivationPath: (currency: Currency) -> String? = { currency ->
|
||||
currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(card.derivationStyle)
|
||||
?.rawPath
|
||||
}
|
||||
|
||||
for (currency in this.sortedByDescending { it.isBlockchain() }) {
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = currency.blockchain,
|
||||
derivationPath = findDerivationPath(currency),
|
||||
tokens = getTokens(currency),
|
||||
)
|
||||
|
||||
blockchainNetworks.add(blockchainNetwork)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val tokenBlockchainNetworkIndex = blockchainNetworks
|
||||
.indexOfFirst {
|
||||
it.blockchain == currency.blockchain &&
|
||||
it.derivationPath == currency.derivationPath
|
||||
}
|
||||
|
||||
if (tokenBlockchainNetworkIndex == -1) {
|
||||
blockchainNetworks.add(
|
||||
BlockchainNetwork(
|
||||
blockchain = currency.blockchain,
|
||||
derivationPath = findDerivationPath(currency),
|
||||
tokens = listOf(currency.token),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val tokenBlockchainNetwork = blockchainNetworks[tokenBlockchainNetworkIndex]
|
||||
if (currency.token in tokenBlockchainNetwork.tokens) {
|
||||
continue
|
||||
} else {
|
||||
blockchainNetworks.add(
|
||||
tokenBlockchainNetworkIndex,
|
||||
tokenBlockchainNetwork.copy(
|
||||
tokens = tokenBlockchainNetwork.tokens + currency.token,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blockchainNetworks
|
||||
}
|
||||
|
||||
private suspend fun List<BlockchainNetwork>.updateWalletStores(
|
||||
userWallet: UserWallet,
|
||||
): CompletionResult<Unit> {
|
||||
val userWalletId = userWallet.walletId
|
||||
return this
|
||||
.also { blockchainNetworks ->
|
||||
walletStoresRepository.deleteDifference(
|
||||
userWalletId = userWalletId,
|
||||
currentBlockchains = blockchainNetworks.map { it.blockchain },
|
||||
)
|
||||
}
|
||||
.map { blockchainNetwork ->
|
||||
walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
refresh = true,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWalletId,
|
||||
walletStore = WalletStoreBuilder(userWallet, walletManager)
|
||||
.blockchainNetwork(blockchainNetwork)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold()
|
||||
.flatMap {
|
||||
walletAmountsRepository.update(
|
||||
userWallet = userWallet,
|
||||
fiatCurrency = appCurrencyProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.domain.walletStores
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.core.TangemError
|
||||
|
||||
sealed class WalletStoresError(code: Int) : TangemError(code) {
|
||||
override val silent: Boolean
|
||||
get() = (cause as? TangemError)?.silent == true
|
||||
|
||||
override val messageResId: Int? = null
|
||||
override val message: String?
|
||||
get() = customMessage
|
||||
|
||||
class FetchFiatRatesError(
|
||||
currencies: List<String>,
|
||||
override val cause: Throwable?,
|
||||
) : WalletStoresError(60011) {
|
||||
override var customMessage: String = "Failed to fetch fiat rates for currencies $currencies"
|
||||
}
|
||||
|
||||
class UnknownBlockchain : WalletStoresError(60012) {
|
||||
override var customMessage: String = "Unknown blockchain"
|
||||
}
|
||||
|
||||
object NoInternetConnection : WalletStoresError(60013) {
|
||||
override var customMessage: String = "No internet connection"
|
||||
}
|
||||
|
||||
class WalletManagerNotCreated(blockchain: Blockchain) : WalletStoresError(60014) {
|
||||
override var customMessage: String = "Wallet manager can not be created for $blockchain"
|
||||
}
|
||||
|
||||
class UpdateWalletManagerError(
|
||||
blockchain: Blockchain,
|
||||
override val cause: Throwable,
|
||||
) : WalletStoresError(600015) {
|
||||
override var customMessage: String = "Unable to update wallet manager for currency $blockchain: $cause"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.walletStores
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletStoresManager {
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
|
||||
|
||||
suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
suspend fun fetch(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean = false,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun fetch(
|
||||
userWallets: List<UserWallet>,
|
||||
refresh: Boolean = false,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletStores.di
|
||||
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.DummyWalletStoresManager
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletStores.implementation.DefaultWalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
|
||||
fun WalletStoresManager.Companion.provideDummyImplementation(): WalletStoresManager {
|
||||
return DummyWalletStoresManager()
|
||||
}
|
||||
|
||||
fun WalletStoresManager.Companion.provideDefaultImplementation(
|
||||
userTokensRepository: UserTokensRepository,
|
||||
walletStoresRepository: WalletStoresRepository,
|
||||
walletAmountsRepository: WalletAmountsRepository,
|
||||
walletManagersRepository: WalletManagersRepository,
|
||||
appCurrencyProvider: () -> FiatCurrency,
|
||||
): WalletStoresManager {
|
||||
return DefaultWalletStoresManager(
|
||||
userTokensRepository = userTokensRepository,
|
||||
walletStoresRepository = walletStoresRepository,
|
||||
walletAmountsRepository = walletAmountsRepository,
|
||||
walletManagersRepository = walletManagersRepository,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.tap.domain.walletStores.implementation
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.flatMapOnFailure
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
|
||||
import com.tangem.tap.domain.tokens.UserTokensRepository
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
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.features.wallet.models.toBlockchainNetworks
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletStoresManager(
|
||||
private val userTokensRepository: UserTokensRepository,
|
||||
private val walletStoresRepository: WalletStoresRepository,
|
||||
private val walletAmountsRepository: WalletAmountsRepository,
|
||||
private val walletManagersRepository: WalletManagersRepository,
|
||||
private val appCurrencyProvider: () -> FiatCurrency,
|
||||
) : WalletStoresManager {
|
||||
private val state = MutableStateFlow(State())
|
||||
|
||||
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return walletStoresRepository.getAll()
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return walletStoresRepository.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
|
||||
val walletIds = userWalletsIds.map { UserWalletId(it) }
|
||||
return walletStoresRepository.delete(walletIds)
|
||||
.flatMap { walletManagersRepository.delete(walletIds) }
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return walletStoresRepository.clear()
|
||||
}
|
||||
|
||||
override suspend fun fetch(
|
||||
userWallets: List<UserWallet>,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
val fiatCurrency = appCurrencyProvider.invoke()
|
||||
val isFiatCurrencyChanged = state.value.fiatCurrency != fiatCurrency
|
||||
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
fiatCurrency = fiatCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
return userWallets
|
||||
.mapNotNull { userWallet ->
|
||||
val hasNotWalletStoresForUserWallet = !walletStoresRepository.contains(userWallet.walletId)
|
||||
if (refresh || hasNotWalletStoresForUserWallet || isFiatCurrencyChanged) {
|
||||
fetchWalletsIfNeeded(userWallet, refresh)
|
||||
} else null
|
||||
}
|
||||
.fold(initial = arrayListOf<UserWallet>()) { acc, data ->
|
||||
acc.apply { add(data) }
|
||||
}
|
||||
.flatMap {
|
||||
walletAmountsRepository.update(it, fiatCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetch(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return fetch(listOf(userWallet), refresh)
|
||||
}
|
||||
|
||||
private suspend fun fetchWalletsIfNeeded(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<UserWallet> {
|
||||
return if (userWallet.scanResponse.card.isMultiwalletAllowed) {
|
||||
fetchMultiWallets(userWallet, refresh)
|
||||
} else {
|
||||
fetchSingleWallet(userWallet, refresh)
|
||||
}
|
||||
.map { userWallet }
|
||||
}
|
||||
|
||||
private suspend fun fetchMultiWallets(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val userTokens = withContext(Dispatchers.IO) {
|
||||
userTokensRepository.getUserTokens(scanResponse.card)
|
||||
}
|
||||
val userWalletId = userWallet.walletId
|
||||
|
||||
return withContext(Dispatchers.Default) {
|
||||
userTokens.toBlockchainNetworks()
|
||||
.also { blockchainNetworks ->
|
||||
walletStoresRepository.deleteDifference(
|
||||
userWalletId = userWalletId,
|
||||
currentBlockchains = blockchainNetworks.map { it.blockchain },
|
||||
)
|
||||
}
|
||||
.map { blockchainNetwork ->
|
||||
val storeWalletStore: suspend (WalletManager?) -> CompletionResult<Unit> =
|
||||
{ walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWalletId,
|
||||
walletStore = WalletStoreBuilder(userWallet, blockchainNetwork)
|
||||
.walletManager(walletManager)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
refresh = refresh,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
storeWalletStore(walletManager)
|
||||
}
|
||||
.flatMapOnFailure { error ->
|
||||
when (error) {
|
||||
is WalletStoresError.WalletManagerNotCreated,
|
||||
is WalletStoresError.UpdateWalletManagerError,
|
||||
-> storeWalletStore(null)
|
||||
else -> CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSingleWallet(
|
||||
userWallet: UserWallet,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return walletManagersRepository.findOrMake(
|
||||
userWallet = userWallet,
|
||||
refresh = refresh,
|
||||
)
|
||||
.flatMap { walletManager ->
|
||||
walletStoresRepository.storeOrUpdate(
|
||||
userWalletId = userWallet.walletId,
|
||||
walletStore = WalletStoreBuilder(userWallet, walletManager)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class State(
|
||||
val fiatCurrency: FiatCurrency? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.tap.domain.walletStores.implementation
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
internal class DummyWalletStoresManager : WalletStoresManager {
|
||||
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun fetch(userWallet: UserWallet, refresh: Boolean): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun fetch(userWallets: List<UserWallet>, refresh: Boolean): CompletionResult<Unit> {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.domain.walletStores.implementation.utils
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
|
||||
internal fun List<CompletionResult<Unit>>.fold(): CompletionResult<Unit> {
|
||||
return fold(Unit) { _, _ -> Unit }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun <reified D, reified R> List<CompletionResult<D>>.fold(
|
||||
initial: R,
|
||||
operation: (acc: R, data: D) -> R,
|
||||
): CompletionResult<R> {
|
||||
var resultData = initial
|
||||
for (result in this) {
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
resultData = operation(resultData, result.data)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
return result as CompletionResult.Failure<R>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(resultData)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
|
||||
interface WalletAmountsRepository {
|
||||
suspend fun update(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
walletStore: WalletStoreModel,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
|
||||
interface WalletManagersRepository {
|
||||
suspend fun findOrMake(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork? = null,
|
||||
refresh: Boolean = false,
|
||||
): CompletionResult<WalletManager>
|
||||
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
|
||||
suspend fun delete(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.domain.walletStores.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletStoresRepository {
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
|
||||
|
||||
suspend fun contains(userWalletId: UserWalletId): Boolean
|
||||
|
||||
suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
|
||||
suspend fun deleteDifference(
|
||||
userWalletId: UserWalletId,
|
||||
currentBlockchains: List<Blockchain>,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
suspend fun storeOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): CompletionResult<Unit>
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.di
|
||||
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
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.implementation.DefaultWalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.DefaultWalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.DefaultWalletStoresRepository
|
||||
|
||||
fun WalletStoresRepository.Companion.provideDefaultImplementation(): WalletStoresRepository {
|
||||
return DefaultWalletStoresRepository()
|
||||
}
|
||||
|
||||
fun WalletManagersRepository.Companion.provideDefaultImplementation(
|
||||
walletManagerFactory: WalletManagerFactory,
|
||||
): WalletManagersRepository {
|
||||
return DefaultWalletManagersRepository(walletManagerFactory)
|
||||
}
|
||||
|
||||
fun WalletAmountsRepository.Companion.provideDefaultImplementation(
|
||||
tangemTechService: TangemTechService,
|
||||
): WalletAmountsRepository {
|
||||
return DefaultWalletAmountsRepository(tangemTechService)
|
||||
}
|
||||
|
|
@ -0,0 +1,427 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result.Failure
|
||||
import com.tangem.blockchain.extensions.Result.Success
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.flatMapOnFailure
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.replaceByOrAdd
|
||||
import com.tangem.tap.domain.model.UserWallet
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresError
|
||||
import com.tangem.tap.domain.walletStores.implementation.utils.fold
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.tap.features.wallet.models.filterByCoin
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultWalletAmountsRepository(
|
||||
private val tangemTechService: TangemTechService,
|
||||
) : WalletAmountsRepository {
|
||||
private val walletStoresStorage = WalletStoresStorage
|
||||
private val walletManagersStorage = WalletManagerStorage
|
||||
|
||||
override suspend fun update(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
return if (userWallets.isEmpty()) CompletionResult.Success(Unit)
|
||||
else withContext(Dispatchers.Default) {
|
||||
awaitAll(
|
||||
async { fetchAmounts(userWallets) },
|
||||
async { fetchFiatRates(userWallets, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
return update(listOf(userWallet), fiatCurrency)
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
userWallet: UserWallet,
|
||||
walletStore: WalletStoreModel,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
val walletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
|
||||
awaitAll(
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmounts(walletId, scanResponse, walletStore, walletManager)
|
||||
.flatMap { fetchRentIfNeeded(walletStore, walletManager) }
|
||||
},
|
||||
async { fetchFiatRates(listOf(userWallet), fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmounts(
|
||||
userWallets: List<UserWallet>,
|
||||
): CompletionResult<Unit> = coroutineScope {
|
||||
userWallets.map { userWallet ->
|
||||
val walletId = userWallet.walletId
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val walletStores = walletStoresStorage.getSync(walletId)
|
||||
|
||||
walletStores.map { walletStore ->
|
||||
async {
|
||||
// TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository]
|
||||
val walletManager = walletStore.walletManager
|
||||
fetchAmounts(walletId, scanResponse, walletStore, walletManager)
|
||||
.flatMap { fetchRentIfNeeded(walletStore, walletManager) }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.fold()
|
||||
}
|
||||
.fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchFiatRates(
|
||||
userWallets: List<UserWallet>,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
val walletsIds = userWallets.map { it.walletId }
|
||||
val walletStores = walletsIds
|
||||
.flatMap { walletStoresStorage.getSync(it) }
|
||||
|
||||
val currencies = walletStores
|
||||
.asSequence()
|
||||
.flatMap { it.walletsData }
|
||||
.map { it.currency }
|
||||
|
||||
val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList()
|
||||
|
||||
val fiatRatesResult = withContext(Dispatchers.IO) {
|
||||
tangemTechService.rates(
|
||||
currency = fiatCurrency.code,
|
||||
ids = coinsIds,
|
||||
)
|
||||
}
|
||||
|
||||
return when (fiatRatesResult) {
|
||||
is Result.Success -> {
|
||||
Timber.d(
|
||||
"""
|
||||
Fetched fiat rates
|
||||
|- User wallets ids: $walletsIds
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStores.forEach { walletStore ->
|
||||
updateWithFiatRates(
|
||||
walletStore = walletStore,
|
||||
fiatRates = fiatRatesResult.data.rates,
|
||||
)
|
||||
}
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
val error = WalletStoresError.FetchFiatRatesError(
|
||||
currencies = currencies.map { it.currencySymbol }.toList(),
|
||||
cause = fiatRatesResult.error,
|
||||
)
|
||||
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Unable to fetch fiat rates
|
||||
|- User wallets ids: $walletsIds
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAmounts(
|
||||
walletId: UserWalletId,
|
||||
scanResponse: ScanResponse,
|
||||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager?,
|
||||
): CompletionResult<Unit> {
|
||||
val hasMissedDerivations = with(walletStore.blockchainNetwork) {
|
||||
derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath)
|
||||
}
|
||||
val blockchain = walletStore.blockchainNetwork.blockchain
|
||||
val tokens = walletStore.blockchainNetwork.tokens.map { it.name }
|
||||
|
||||
return when {
|
||||
hasMissedDerivations -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Missed derivation
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithMissedDerivation(
|
||||
walletStore = walletStore,
|
||||
)
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
walletManager == null -> {
|
||||
Timber.e(
|
||||
"""
|
||||
Wallet manager is null
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithUnreachable(
|
||||
walletStore = walletStore,
|
||||
)
|
||||
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
else -> {
|
||||
withInternetConnection { walletManager.update() }
|
||||
.map { updateWalletManagerWithAmounts(walletId, walletManager) }
|
||||
.doOnSuccess {
|
||||
Timber.d(
|
||||
"""
|
||||
Fetched amounts
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
|- Tokens: $tokens
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
updateWithAmounts(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
)
|
||||
}
|
||||
.flatMapOnFailure { error ->
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Unable to fetch amounts
|
||||
|- User wallet id: $walletId
|
||||
|- Blockchain: $blockchain
|
||||
|- Tokens: $tokens
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (error is BlockchainSdkError) {
|
||||
updateWithError(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
error = error,
|
||||
)
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchRentIfNeeded(
|
||||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager?,
|
||||
): CompletionResult<Unit> {
|
||||
val rentProvider = walletManager as? RentProvider
|
||||
|
||||
if (walletManager == null || rentProvider == null) {
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
when (val result = rentProvider.minimalBalanceForRentExemption()) {
|
||||
is Success -> {
|
||||
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
|
||||
val outgoingTxs = walletManager.wallet.getPendingTransactions(
|
||||
PendingTransactionType.Outgoing,
|
||||
).filterByCoin()
|
||||
|
||||
val rentExempt = result.data
|
||||
val setRent = if (outgoingTxs.isEmpty()) {
|
||||
balance < rentExempt
|
||||
} else {
|
||||
val outgoingAmount = outgoingTxs.sumOf { it.amountValue ?: BigDecimal.ZERO }
|
||||
val rest = balance.minus(outgoingAmount)
|
||||
balance < rest
|
||||
}
|
||||
|
||||
updateWithRent(
|
||||
walletStore = walletStore,
|
||||
rent = if (setRent) {
|
||||
WalletStoreModel.WalletRent(
|
||||
rent = rentProvider.rentAmount(),
|
||||
exemptionAmount = rentExempt,
|
||||
)
|
||||
} else null,
|
||||
)
|
||||
}
|
||||
is Failure -> Unit
|
||||
}
|
||||
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
private suspend inline fun withInternetConnection(crossinline block: suspend () -> Unit): CompletionResult<Unit> {
|
||||
return if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val error = WalletStoresError.NoInternetConnection
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
} else withContext(Dispatchers.IO) {
|
||||
catching { block() }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWalletManagerWithAmounts(
|
||||
walletId: UserWalletId,
|
||||
walletManager: WalletManager,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[walletId].orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == it.wallet.blockchain
|
||||
}
|
||||
}
|
||||
|
||||
prevManagers.apply {
|
||||
set(walletId, newManagersForUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithError(
|
||||
walletStore: WalletStoreModel,
|
||||
wallet: Wallet,
|
||||
error: BlockchainSdkError,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithError(
|
||||
wallet = wallet,
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithAmounts(
|
||||
walletStore: WalletStoreModel,
|
||||
wallet: Wallet,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithAmounts(wallet = wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithMissedDerivation(
|
||||
walletStore: WalletStoreModel,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithMissedDerivation()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithUnreachable(
|
||||
walletStore: WalletStoreModel,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithUnreachable()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithFiatRates(
|
||||
walletStore: WalletStoreModel,
|
||||
fiatRates: Map<String, Double>,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithFiatRates(rates = fiatRates)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWithRent(
|
||||
walletStore: WalletStoreModel,
|
||||
rent: WalletStoreModel.WalletRent?,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletId = walletStore.userWalletId,
|
||||
walletStore = walletStore,
|
||||
update = {
|
||||
it.updateWithRent(rent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.mapFailure
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
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.domain.walletStores.repository.WalletManagersRepository
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultWalletManagersRepository(
|
||||
private val walletManagerFactory: WalletManagerFactory,
|
||||
) : WalletManagersRepository {
|
||||
private val walletManagersStorage = WalletManagerStorage
|
||||
|
||||
override suspend fun findOrMake(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
refresh: Boolean,
|
||||
): CompletionResult<WalletManager> = withContext(Dispatchers.Default) {
|
||||
if (refresh) {
|
||||
deleteInternal(userWallet.walletId, blockchainNetwork?.blockchain)
|
||||
makeAndStore(userWallet, blockchainNetwork)
|
||||
} else {
|
||||
val foundWalletManager = findWalletManager(
|
||||
userWalletId = userWallet.walletId,
|
||||
blockchain = blockchainNetwork?.blockchain,
|
||||
)
|
||||
|
||||
foundWalletManager?.updateTokens(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
)
|
||||
?: makeAndStore(userWallet, blockchainNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun makeAndStore(
|
||||
userWallet: UserWallet,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
): CompletionResult<WalletManager> {
|
||||
val scanResponse = userWallet.scanResponse
|
||||
val blockchain = blockchainNetwork?.blockchain
|
||||
?: scanResponse.getBlockchain().let { blockchain ->
|
||||
if (scanResponse.card.isTestCard) blockchain.getTestnetVersion() else blockchain
|
||||
}
|
||||
val derivationParams = getDerivationParams(
|
||||
derivationPath = blockchainNetwork?.derivationPath,
|
||||
card = scanResponse.card,
|
||||
)
|
||||
|
||||
val walletManager = blockchain?.let {
|
||||
walletManagerFactory.makeWalletManagerForApp(
|
||||
scanResponse = userWallet.scanResponse,
|
||||
blockchain = blockchain,
|
||||
derivationParams = derivationParams,
|
||||
)
|
||||
}
|
||||
|
||||
return when {
|
||||
blockchain == Blockchain.Unknown || blockchain == null -> {
|
||||
val error = WalletStoresError.UnknownBlockchain()
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
walletManager != null -> {
|
||||
walletManager.updateTokens(
|
||||
scanResponse = scanResponse,
|
||||
blockchainNetwork = blockchainNetwork,
|
||||
)
|
||||
.map { updatedWalletManager ->
|
||||
store(userWallet.walletId, updatedWalletManager)
|
||||
updatedWalletManager
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val error = WalletStoresError.WalletManagerNotCreated(blockchain)
|
||||
Timber.e(error)
|
||||
CompletionResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
userWalletIds.forEach { userWalletId ->
|
||||
remove(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
): CompletionResult<Unit> = catching {
|
||||
deleteInternal(userWalletId, blockchain)
|
||||
}
|
||||
|
||||
private suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = this[userWalletId].orEmpty() + walletManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteInternal(userWalletId: UserWalletId, blockchain: Blockchain?) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
prevManagers.apply {
|
||||
if (blockchain == null) {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = emptyList(),
|
||||
)
|
||||
} else {
|
||||
set(
|
||||
key = userWalletId,
|
||||
value = this[userWalletId]
|
||||
?.filter { it.wallet.blockchain == blockchain }
|
||||
.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WalletManager.updateTokens(
|
||||
scanResponse: ScanResponse,
|
||||
blockchainNetwork: BlockchainNetwork?,
|
||||
): CompletionResult<WalletManager> {
|
||||
val walletManager = this
|
||||
return catching {
|
||||
val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.getPrimaryToken())
|
||||
|
||||
if (tokens.isNotEmpty()) {
|
||||
walletManager.addTokens(tokens)
|
||||
}
|
||||
|
||||
walletManager
|
||||
}
|
||||
.mapFailure {
|
||||
val error = WalletStoresError.UpdateWalletManagerError(
|
||||
blockchain = walletManager.wallet.blockchain,
|
||||
cause = it,
|
||||
)
|
||||
Timber.e(error)
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findWalletManager(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain?,
|
||||
): WalletManager? {
|
||||
return walletManagersStorage.getAllSync()[userWalletId]?.let { userWalletManagers ->
|
||||
if (blockchain == null) userWalletManagers.firstOrNull()
|
||||
else userWalletManagers.find { it.wallet.blockchain == blockchain }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {
|
||||
return derivationPath?.let {
|
||||
DerivationParams.Custom(
|
||||
path = DerivationPath(it),
|
||||
)
|
||||
} ?: if (!card.settings.isHDWalletAllowed) {
|
||||
null
|
||||
} else if (card.useOldStyleDerivation) {
|
||||
DerivationParams.Default(DerivationStyle.LEGACY)
|
||||
} else {
|
||||
DerivationParams.Default(DerivationStyle.NEW)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.catching
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.isSameWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
|
||||
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithSelf
|
||||
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultWalletStoresRepository : WalletStoresRepository {
|
||||
private val walletStoresStorage = WalletStoresStorage
|
||||
|
||||
override fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return walletStoresStorage.getAll()
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return walletStoresStorage.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun contains(userWalletId: UserWalletId): Boolean {
|
||||
return walletStoresStorage.getSync(userWalletId).isNotEmpty()
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.filterKeys { it !in userWalletsIds } as HashMap<UserWalletId, List<WalletStoreModel>>
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteDifference(
|
||||
userWalletId: UserWalletId,
|
||||
currentBlockchains: List<Blockchain>,
|
||||
): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.apply {
|
||||
this[userWalletId] = this[userWalletId]
|
||||
?.filter { it.blockchainNetwork.blockchain in currentBlockchains }
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear(): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { hashMapOf() }
|
||||
}
|
||||
|
||||
override suspend fun storeOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): CompletionResult<Unit> = catching {
|
||||
walletStoresStorage.update { prevStores ->
|
||||
prevStores.addOrUpdate(userWalletId, walletStore)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun HashMap<UserWalletId, List<WalletStoreModel>>.addOrUpdate(
|
||||
userWalletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
): HashMap<UserWalletId, List<WalletStoreModel>> = withContext(Dispatchers.Default) {
|
||||
val prevStores = this@addOrUpdate
|
||||
val walletStores = prevStores[userWalletId]
|
||||
|
||||
if (walletStores.isNullOrEmpty()) {
|
||||
prevStores.apply {
|
||||
set(userWalletId, listOf(walletStore))
|
||||
}
|
||||
} else {
|
||||
val oldWalletStore = walletStores.find(walletStore::isSameWalletStore)
|
||||
|
||||
if (oldWalletStore == null) {
|
||||
prevStores.apply {
|
||||
set(userWalletId, walletStores + walletStore)
|
||||
}
|
||||
} else {
|
||||
prevStores.replaceWalletStore(
|
||||
walletId = userWalletId,
|
||||
walletStore = oldWalletStore,
|
||||
update = { it.updateWithSelf(walletStore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation.utils
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.tap.domain.extensions.amountToCreateAccount
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal fun WalletDataModel.updateWithFiatRate(
|
||||
fiatRate: BigDecimal?,
|
||||
): WalletDataModel {
|
||||
return this.copy(
|
||||
fiatRate = fiatRate,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithFiatRates(
|
||||
fiatRates: Map<String, Double>,
|
||||
): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
val rate = fiatRates[walletData.currency.coinId]?.toBigDecimal()
|
||||
walletData.updateWithFiatRate(rate)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
|
||||
val pendingTransactions = wallet.getPendingTransactions()
|
||||
return this.copy(
|
||||
status = when (val currency = this.currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val amount = wallet.fundsAvailable(AmountType.Coin)
|
||||
if (pendingTransactions.isEmpty()) {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
} else {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val token = currency.token
|
||||
val amount = wallet.fundsAvailable(AmountType.Token(token))
|
||||
val hasTokenPendingTransactions = pendingTransactions
|
||||
.any { it.transactionData.amount.currencySymbol == token.symbol }
|
||||
when {
|
||||
hasTokenPendingTransactions -> {
|
||||
WalletDataModel.TransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
pendingTransactions.isNotEmpty() -> {
|
||||
WalletDataModel.SameCurrencyTransactionInProgress(
|
||||
amount = amount,
|
||||
pendingTransactions = pendingTransactions,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
WalletDataModel.VerifiedOnline(
|
||||
amount = amount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithAmount(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): WalletDataModel {
|
||||
return this.copy(
|
||||
status = when (error) {
|
||||
is BlockchainSdkError.AccountNotFound -> {
|
||||
val amountToCreateAccount = wallet.blockchain
|
||||
.amountToCreateAccount(wallet.getFirstToken())
|
||||
|
||||
if (amountToCreateAccount != null) {
|
||||
WalletDataModel.NoAccount(
|
||||
amountToCreateAccount = amountToCreateAccount,
|
||||
)
|
||||
} else {
|
||||
WalletDataModel.Unreachable(
|
||||
errorMessage = error.customMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> WalletDataModel.Unreachable(
|
||||
errorMessage = error.customMessage,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.updateWithError(wallet, error)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.updateWithSelf(
|
||||
newWalletData: WalletDataModel,
|
||||
): WalletDataModel {
|
||||
val oldWalletData = this
|
||||
val oldStatus = oldWalletData.status
|
||||
return oldWalletData.copy(
|
||||
status = when (val newStatus = newWalletData.status) {
|
||||
is WalletDataModel.Loading -> when (oldStatus) {
|
||||
is WalletDataModel.MissedDerivation -> WalletDataModel.Loading
|
||||
else -> oldStatus.asRefreshing()
|
||||
}
|
||||
is WalletDataModel.MissedDerivation,
|
||||
is WalletDataModel.Refreshing,
|
||||
is WalletDataModel.NoAccount,
|
||||
is WalletDataModel.Unreachable,
|
||||
is WalletDataModel.SameCurrencyTransactionInProgress,
|
||||
is WalletDataModel.TransactionInProgress,
|
||||
is WalletDataModel.VerifiedOnline,
|
||||
-> newStatus
|
||||
},
|
||||
walletAddresses = newWalletData.walletAddresses,
|
||||
existentialDeposit = newWalletData.existentialDeposit,
|
||||
fiatRate = newWalletData.fiatRate ?: oldWalletData.fiatRate,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithMissedDerivation(): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.copy(
|
||||
status = WalletDataModel.MissedDerivation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithUnreachable(): List<WalletDataModel> {
|
||||
return this.map { walletData ->
|
||||
walletData.copy(
|
||||
status = WalletDataModel.Unreachable(errorMessage = null),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<WalletDataModel>.updateWithSelf(
|
||||
walletsData: List<WalletDataModel>,
|
||||
): List<WalletDataModel> {
|
||||
val oldWalletsData = this
|
||||
val updatedWalletsData = arrayListOf<WalletDataModel>()
|
||||
|
||||
walletsData.forEach { newWalletData ->
|
||||
val walletDataToUpdate = oldWalletsData.find(newWalletData::isSameWalletData)
|
||||
if (walletDataToUpdate != null) {
|
||||
updatedWalletsData.add(walletDataToUpdate.updateWithSelf(newWalletData))
|
||||
} else {
|
||||
updatedWalletsData.add(newWalletData)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedWalletsData
|
||||
}
|
||||
|
||||
internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean {
|
||||
return currency == other.currency
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.tap.domain.walletStores.repository.implementation.utils
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
|
||||
internal inline fun HashMap<UserWalletId, List<WalletStoreModel>>.replaceWalletStore(
|
||||
walletId: UserWalletId,
|
||||
walletStore: WalletStoreModel,
|
||||
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
|
||||
): HashMap<UserWalletId, List<WalletStoreModel>> {
|
||||
return this.apply {
|
||||
this[walletId] = this[walletId]
|
||||
?.replaceWalletStore(walletStore, update)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithError(
|
||||
wallet: Wallet,
|
||||
error: TangemError,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithError(
|
||||
wallet = wallet,
|
||||
error = error,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithAmounts(
|
||||
wallet: Wallet,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithAmounts(wallet = wallet),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithFiatRates(
|
||||
rates: Map<String, Double>,
|
||||
): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithFiatRates(rates),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithSelf(
|
||||
newWalletStore: WalletStoreModel,
|
||||
): WalletStoreModel {
|
||||
val oldStore = this
|
||||
return oldStore.copy(
|
||||
walletManager = newWalletStore.walletManager,
|
||||
walletRent = newWalletStore.walletRent,
|
||||
walletsData = oldStore.walletsData.updateWithSelf(newWalletStore.walletsData),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithMissedDerivation(): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithMissedDerivation(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithUnreachable(): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletsData = walletsData.updateWithUnreachable(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.updateWithRent(rent: WalletStoreModel.WalletRent?): WalletStoreModel {
|
||||
return this.copy(
|
||||
walletRent = rent,
|
||||
)
|
||||
}
|
||||
|
||||
internal inline fun List<WalletStoreModel>.replaceWalletStore(
|
||||
newWalletStore: WalletStoreModel,
|
||||
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
|
||||
): List<WalletStoreModel> {
|
||||
val mutableStores = ArrayList(this)
|
||||
for ((index, walletStore) in this.withIndex()) {
|
||||
if (walletStore.isSameWalletStore(newWalletStore)) {
|
||||
mutableStores[index] = update(walletStore)
|
||||
break
|
||||
}
|
||||
}
|
||||
return mutableStores
|
||||
}
|
||||
|
||||
internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean {
|
||||
return blockchainNetwork == other.blockchainNetwork
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.domain.walletStores.storage
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal object WalletManagerStorage {
|
||||
private val managers =
|
||||
MutableSharedFlow<HashMap<UserWalletId, List<WalletManager>>>(replay = 1)
|
||||
|
||||
init {
|
||||
managers.tryEmit(hashMapOf())
|
||||
}
|
||||
|
||||
suspend fun getAllSync(): Map<UserWalletId, List<WalletManager>> {
|
||||
return managers.first()
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
suspend fun update(
|
||||
f: suspend (HashMap<UserWalletId, List<WalletManager>>) -> HashMap<UserWalletId, List<WalletManager>>,
|
||||
) {
|
||||
while (mutex.isLocked) {
|
||||
delay(timeMillis = 60)
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
val prevState = managers.first()
|
||||
managers.emit(f(prevState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.tap.domain.walletStores.storage
|
||||
|
||||
import com.tangem.domain.common.util.UserWalletId
|
||||
import com.tangem.tap.domain.model.WalletStoreModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
internal object WalletStoresStorage {
|
||||
private val stores =
|
||||
MutableSharedFlow<HashMap<UserWalletId, List<WalletStoreModel>>>(replay = 1)
|
||||
|
||||
init {
|
||||
stores.tryEmit(hashMapOf())
|
||||
}
|
||||
|
||||
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>> {
|
||||
return stores
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
|
||||
return stores
|
||||
.mapLatest { stores ->
|
||||
stores[userWalletId].orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
|
||||
return stores.first().getOrElse(userWalletId) { emptyList() }
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
suspend fun update(
|
||||
f: suspend (HashMap<UserWalletId, List<WalletStoreModel>>) -> HashMap<UserWalletId, List<WalletStoreModel>>,
|
||||
) {
|
||||
while (mutex.isLocked) {
|
||||
delay(timeMillis = 60)
|
||||
}
|
||||
|
||||
mutex.withLock {
|
||||
val prevState = stores.first()
|
||||
stores.emit(f(prevState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.tap.features.demo
|
||||
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId)
|
||||
fun Card.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId)
|
||||
fun CardDTO.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.domain.termsOfUse.CardTou
|
||||
|
|
@ -30,7 +30,7 @@ sealed class DetailsAction : Action {
|
|||
|
||||
object ScanCard : DetailsAction()
|
||||
|
||||
data class PrepareCardSettingsData(val card: Card) : DetailsAction()
|
||||
data class PrepareCardSettingsData(val card: CardDTO) : DetailsAction()
|
||||
object ResetCardSettingsData : DetailsAction()
|
||||
|
||||
sealed class ManageSecurity : DetailsAction() {
|
||||
|
|
@ -45,8 +45,20 @@ sealed class DetailsAction : Action {
|
|||
}
|
||||
|
||||
sealed class AppSettings : DetailsAction() {
|
||||
data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) :
|
||||
AppSettings()
|
||||
data class SwitchPrivacySetting(
|
||||
val enable: Boolean,
|
||||
val setting: PrivacySetting,
|
||||
) : AppSettings() {
|
||||
data class Success(
|
||||
val enable: Boolean,
|
||||
val setting: PrivacySetting,
|
||||
) : AppSettings()
|
||||
}
|
||||
|
||||
object EnrollBiometrics : AppSettings() {
|
||||
object Enroll : AppSettings()
|
||||
object Cancel : AppSettings()
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
|
||||
|
|
|
|||
|
|
@ -2,51 +2,63 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.util.userWalletId
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.extensions.getUserWalletId
|
||||
import com.tangem.tap.domain.model.builders.UserWalletBuilder
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class DetailsMiddleware {
|
||||
private val eraseWalletMiddleware = EraseWalletMiddleware()
|
||||
private val manageSecurityMiddleware = ManageSecurityMiddleware()
|
||||
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
|
||||
val detailsMiddleware: Middleware<AppState> = { _, state ->
|
||||
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleAction(state, action)
|
||||
if (!DemoHelper.tryHandle(stateProvider, action)) {
|
||||
val detailsState = stateProvider()?.detailsState
|
||||
if (detailsState != null) {
|
||||
handleAction(detailsState, action)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAction(state: () -> AppState?, action: Action) {
|
||||
if (DemoHelper.tryHandle(state, action)) return
|
||||
|
||||
private fun handleAction(state: DetailsState, action: Action) {
|
||||
when (action) {
|
||||
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
|
||||
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
|
||||
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
|
||||
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
|
||||
is DetailsAction.ShowDisclaimer -> {
|
||||
val uri = store.state.detailsState.cardTermsOfUseUrl
|
||||
if (uri != null) {
|
||||
|
|
@ -65,12 +77,16 @@ class DetailsMiddleware {
|
|||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
scope.launch {
|
||||
when (val result = tangemSdkManager.scanCard()) {
|
||||
is CompletionResult.Success -> {
|
||||
val card = result.data
|
||||
if (card.getUserWalletId() ==
|
||||
store.state.globalState.scanResponse?.card?.getUserWalletId()
|
||||
) {
|
||||
tangemSdkManager.scanCard(
|
||||
cardId = state.scanResponse?.card?.cardId,
|
||||
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
|
||||
state.scanResponse?.card?.isAccessCodeSet == true,
|
||||
)
|
||||
.doOnSuccess { card ->
|
||||
val currentCardId = store.state.globalState.scanResponse?.card
|
||||
?.userWalletId
|
||||
?.stringValue
|
||||
if (card.userWalletId.stringValue == currentCardId) {
|
||||
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
|
|
@ -81,9 +97,6 @@ class DetailsMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,22 +117,25 @@ class DetailsMiddleware {
|
|||
is DetailsAction.ResetToFactory.Proceed -> {
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.resetToFactorySettings(card)
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
tangemSdkManager.resetToFactorySettings(card.cardId)
|
||||
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
|
||||
.doOnSuccess {
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished())
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
val screen = if (userWalletsListManager.hasSavedUserWallets) {
|
||||
AppScreen.Welcome
|
||||
} else {
|
||||
AppScreen.Home
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
(result.error as? TangemSdkError)?.let { error ->
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
|
||||
.doOnFailure { error ->
|
||||
(error as? TangemSdkError)?.let { sdkError ->
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(sdkError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> { /* no-op */
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,8 +172,7 @@ class DetailsMiddleware {
|
|||
}
|
||||
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
|
||||
}
|
||||
else -> { /* no-op */
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,19 +186,115 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
else -> { /* no-op */
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ManagePrivacyMiddleware {
|
||||
fun handle(action: DetailsAction.AppSettings) {
|
||||
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
|
||||
when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
// TODO()
|
||||
if (tangemSdkManager.canEnrollBiometrics) {
|
||||
store.dispatch(DetailsAction.AppSettings.EnrollBiometrics)
|
||||
}
|
||||
when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
|
||||
}
|
||||
}
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> Unit
|
||||
is DetailsAction.AppSettings.EnrollBiometrics -> Unit
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Cancel -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||
}
|
||||
|
||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
if (state.saveWallets == enable) return@launch
|
||||
if (enable) {
|
||||
saveCurrentWallet()
|
||||
} else {
|
||||
deleteSavedWallets()
|
||||
if (state.saveAccessCodes) {
|
||||
deleteSavedAccessCodes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
if (state.saveAccessCodes == enable) return@launch
|
||||
if (enable) {
|
||||
if (!state.saveWallets) {
|
||||
saveCurrentWallet()
|
||||
}
|
||||
saveAccessCodes()
|
||||
} else {
|
||||
deleteSavedAccessCodes()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveCurrentWallet() {
|
||||
val scanResponse = store.state.detailsState.scanResponse ?: return
|
||||
val userWallet = UserWalletBuilder(scanResponse).build()
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Wallet saving failed")
|
||||
}
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldShowSaveWallet = false
|
||||
preferencesStorage.shouldSaveUserWallets = true
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = true,
|
||||
),
|
||||
)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSavedWallets() {
|
||||
userWalletsListManager.clear()
|
||||
.flatMap { walletStoresManager.clear() }
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldSaveUserWallets = false
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveWallets,
|
||||
enable = false,
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAccessCodes() {
|
||||
preferencesStorage.shouldSaveAccessCodes = true
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
enable = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun deleteSavedAccessCodes() {
|
||||
tangemSdkManager.clearSavedUserCodes()
|
||||
.doOnSuccess {
|
||||
preferencesStorage.shouldSaveAccessCodes = false
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
|
||||
setting = PrivacySetting.SaveAccessCode,
|
||||
enable = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import com.tangem.common.card.Card
|
||||
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
|
||||
|
|
@ -8,7 +8,10 @@ import com.tangem.domain.common.isTangemTwin
|
|||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.extensions.isWalletDataSupported
|
||||
import com.tangem.tap.domain.extensions.signedHashesCount
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import org.rekotlin.Action
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -51,12 +54,15 @@ private fun handlePrepareScreen(
|
|||
scanResponse = action.scanResponse,
|
||||
wallets = action.wallets,
|
||||
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appCurrency = store.state.globalState.appCurrency,
|
||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||
saveWallets = userWalletsListManager.hasSavedUserWallets,
|
||||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): DetailsState {
|
||||
private fun handlePrepareCardSettingsScreen(card: CardDTO, state: DetailsState): DetailsState {
|
||||
val cardSettingsState = CardSettingsState(
|
||||
cardInfo = card.toCardInfo(),
|
||||
manageSecurityState = prepareSecurityOptions(card),
|
||||
|
|
@ -66,14 +72,16 @@ private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): De
|
|||
return state.copy(cardSettingsState = cardSettingsState)
|
||||
}
|
||||
|
||||
private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
||||
private fun prepareSecurityOptions(card: CardDTO): ManageSecurityState {
|
||||
val securityOption = when {
|
||||
card.isAccessCodeSet -> {
|
||||
SecurityOption.AccessCode
|
||||
}
|
||||
|
||||
card.isPasscodeSet == true -> {
|
||||
SecurityOption.PassCode
|
||||
}
|
||||
|
||||
else -> {
|
||||
SecurityOption.LongTap
|
||||
}
|
||||
|
|
@ -94,7 +102,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
|||
)
|
||||
}
|
||||
|
||||
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
|
||||
private fun isResetToFactoryAllowedByCard(card: CardDTO): Boolean {
|
||||
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
|
||||
val notAllowedByCard = notAllowedByAnyWallet ||
|
||||
(card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) ||
|
||||
|
|
@ -114,7 +122,8 @@ private fun handleEraseWallet(
|
|||
}
|
||||
|
||||
private fun handleSecurityAction(
|
||||
action: DetailsAction.ManageSecurity, state: DetailsState,
|
||||
action: DetailsAction.ManageSecurity,
|
||||
state: DetailsState,
|
||||
): DetailsState {
|
||||
return when (action) {
|
||||
is DetailsAction.ManageSecurity.SelectOption -> {
|
||||
|
|
@ -144,20 +153,25 @@ private fun handleSecurityAction(
|
|||
}
|
||||
|
||||
private fun handlePrivacyAction(
|
||||
action: DetailsAction.AppSettings, state: DetailsState,
|
||||
action: DetailsAction.AppSettings,
|
||||
state: DetailsState,
|
||||
): DetailsState {
|
||||
return when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
|
||||
}
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) {
|
||||
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
|
||||
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
|
||||
}
|
||||
is DetailsAction.AppSettings.EnrollBiometrics -> state.copy(needEnrollBiometrics = true)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Enroll,
|
||||
is DetailsAction.AppSettings.EnrollBiometrics.Cancel,
|
||||
-> state.copy(needEnrollBiometrics = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareAllowedSecurityOptions(
|
||||
card: Card?, currentSecurityOption: SecurityOption?,
|
||||
card: CardDTO?,
|
||||
currentSecurityOption: SecurityOption?,
|
||||
): EnumSet<SecurityOption> {
|
||||
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
|
||||
|
||||
|
|
@ -173,7 +187,7 @@ private fun prepareAllowedSecurityOptions(
|
|||
return allowedSecurityOptions
|
||||
}
|
||||
|
||||
private fun Card.toCardInfo(): CardInfo {
|
||||
private fun CardDTO.toCardInfo(): CardInfo {
|
||||
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
|
||||
val issuer = this.issuer.name
|
||||
val signedHashes = this.signedHashesCount()
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue