Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-08 11:30:29 +03:00
commit 6f4e6374d3
214 changed files with 8994 additions and 1031 deletions

View file

@ -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>

View file

@ -2,4 +2,4 @@
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
</component>

View file

@ -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" />

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,8 @@
package com.tangem.tap
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
interface ActivityResultCaller {
val activityResultLauncher: ActivityResultLauncher<Intent>?
}

View file

@ -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) {
}
}

View 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))
}
}
}
}
}

View file

@ -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()
}
}

View file

@ -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()

View file

@ -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
}
}

View file

@ -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),
)

View file

@ -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()
}
}

View file

@ -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))
}

View file

@ -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}" }
}

View file

@ -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),
)
}

View file

@ -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,
)
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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))

View file

@ -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))
}

View file

@ -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()

View file

@ -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)

View file

@ -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),
}

View file

@ -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,

View file

@ -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,

View file

@ -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)

View file

@ -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,
),
)
}

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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]

View file

@ -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()
}

View 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
}

View file

@ -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
}
}

View file

@ -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,
)
}

View file

@ -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,
)
}
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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,
)

View file

@ -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()

View file

@ -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

View file

@ -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 {
)
}
}
}
}

View file

@ -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)
}

View file

@ -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() }

View file

@ -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(),
)
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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,
}
}

View file

@ -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)))

View file

@ -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(
}
}
}
}
}

View file

@ -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,
)

View file

@ -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) {

View file

@ -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,

View file

@ -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}"
}
}

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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")
}
}
}

View file

@ -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
}
}

View file

@ -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,
)

View file

@ -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?)
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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(),
)
}
}
}

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
},
)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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) },
)
}
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}
}
}

View file

@ -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))
}
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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,
),
)
}
}
}
}
}

View file

@ -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