Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-01 14:40:31 +03:00
parent a0f1a6591c
commit e993dc0647
119 changed files with 4449 additions and 720 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")
}
}

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

@ -27,7 +27,7 @@ import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_R
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideDummyImplementation
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
@ -44,6 +44,10 @@ lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
lateinit var userWalletsListManager: UserWalletsListManager
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
private set
var userWalletsListManagerSafe: UserWalletsListManager? = null
private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@ -77,7 +81,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
userWalletsListManager = UserWalletsListManager.provideDummyImplementation()
userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
context = applicationContext,
tangemSdkManager = tangemSdkManager,
)
userWalletsListManagerSafe = userWalletsListManager
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
@ -176,4 +185,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
onActivityResultCallbacks.remove(callback)
}
override fun onUserInteraction() {
super.onUserInteraction()
lockUserWalletsTimer?.restart()
}
}

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
@ -39,8 +41,14 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDummyImplementation
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.CardBalanceStateAdapter
@ -64,10 +72,40 @@ lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
val walletStoresManager by lazy {
WalletStoresManager.provideDummyImplementation()
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
private val walletManagersRepository by lazy {
WalletManagersRepository.provideDefaultImplementation(
walletManagerFactory = WalletManagerFactory(
blockchainSdkConfig = store.state.globalState.configManager
?.config
?.blockchainSdkConfig
?: BlockchainSdkConfig(),
),
)
}
private val walletAmountsRepository by lazy {
WalletAmountsRepository.provideDefaultImplementation(
tangemTechService = store.state.domainNetworks.tangemTechService,
)
}
val walletStoresManager by lazy {
WalletStoresManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletCurrenciesManager by lazy {
WalletCurrenciesManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}

View file

@ -28,8 +28,8 @@ fun Store<*>.dispatchNotification(resId: Int) {
}
@Suppress("unused") // receiver type
fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
// TODO: Load tokens for selected user wallet. Will be created in further MRs
suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
}
fun Store<*>.dispatchToastNotification(resId: Int) {
@ -77,7 +77,6 @@ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}

View file

@ -71,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,
@ -90,6 +92,7 @@ data class AppState(
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

@ -5,11 +5,21 @@ import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.*
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
import com.tangem.common.UserCode
import com.tangem.common.UserCodeType
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.*
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
@ -28,6 +38,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.userWalletList.di.USER_WALLETS_BIOMETRIC_KEY_NAME
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
@ -42,8 +53,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val canEnrollBiometrics: Boolean
get() = tangemSdk.biometricManager.canEnrollBiometrics
val biometricManager: BiometricManager
get() = tangemSdk.biometricManager
suspend fun scanProduct(
userTokensRepository: UserTokensRepository,
cardId: String? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
useBiometricsForAccessCode: Boolean = false,
@ -52,8 +67,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message,
runnable = ScanProductTask(
card = null,
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
}
@ -89,7 +109,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun derivePublicKeys(
cardId: String,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
useBiometricsForAccessCode: Boolean = false,
): CompletionResult<DerivationTaskResponse> {
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
@ -102,6 +124,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
.map { CardDTO(it) }
}
suspend fun unlockBiometricKeys(): CompletionResult<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,
@ -110,6 +142,20 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
stringValue = accessCode,
),
)
.map {
biometricManager.unauthenticate(
keyName = tangemSdk.config.userCodesBiometricKeyName,
)
}
}
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
return createUserCodeRepository().clear()
.map {
biometricManager.unauthenticate(
keyName = tangemSdk.config.userCodesBiometricKeyName,
)
}
}
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {

View file

@ -1,11 +1,8 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.*
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
@ -13,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,16 +18,21 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
@ -81,6 +84,60 @@ class TapWalletManager {
}
}
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
val scanResponse = userWallet.scanResponse
val card = scanResponse.card
val attestationFailed = card.attestation.status == Attestation.Status.Failed
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
updateConfigManager(scanResponse)
withMainContext {
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(scanResponse))
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
store.dispatch(WalletAction.UserWalletChanged(userWallet))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
if (refresh) {
loadData(userWallet, refresh = true)
}
}
}
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
walletStoresManager.fetch(userWallet, refresh)
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
}
.doOnFailure { error ->
val errorAction = when (error) {
is WalletStoresError -> when (error) {
is WalletStoresError.FetchFiatRatesError,
is WalletStoresError.UpdateWalletManagerError,
-> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.CreationError,
)
is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
error = TapError.UnknownBlockchain,
)
is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
error = TapError.NoInternetConnection,
)
}
else -> WalletAction.LoadData.Failure(error = null)
}
Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
store.dispatchOnMain(errorAction)
}
}
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
@ -143,7 +200,7 @@ class TapWalletManager {
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<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

@ -22,4 +22,6 @@ data class UserWallet(
) {
val cardId: String
get() = scanResponse.card.cardId
internal var isSaved: Boolean = true
}

View file

@ -35,6 +35,12 @@ data class WalletDataModel(
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()

View file

@ -10,7 +10,8 @@ import java.math.BigDecimal
* Contains info about the blockchain and its currencies
* @param userWalletId ID of the [UserWallet] which uses that store
* @param blockchainNetwork Store's [BlockchainNetwork]
* @param walletManager Store's [WalletManager], may be null if it fails to create this manager
* @param walletManager Store's [WalletManager], may be null if it fails to create this manager. TODO: Remove after
* WalletMiddleware refactoring
* @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
* @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then
* [WalletRent.exemptionAmount]
@ -18,6 +19,7 @@ import java.math.BigDecimal
data class WalletStoreModel(
val userWalletId: UserWalletId,
val blockchainNetwork: BlockchainNetwork,
@Deprecated("Don't use it, will be removed")
val walletManager: WalletManager?,
val walletsData: List<WalletDataModel>,
val walletRent: WalletRent?,

View file

@ -0,0 +1,77 @@
package com.tangem.tap.domain.model.builders
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import java.math.BigDecimal
class WalletStoreBuilder(
private val userWallet: UserWallet,
) {
private var walletManager: WalletManager? = null
private var blockchainNetwork: BlockchainNetwork? = null
fun setWalletManager(walletManager: WalletManager?) = this.apply {
this.walletManager = walletManager
}
fun setBlockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply {
this.blockchainNetwork = blockchainNetwork
}
fun build(): WalletStoreModel {
val blockchainNetwork = this.blockchainNetwork
?: walletManager?.let(BlockchainNetwork::fromWalletManager)
?: error("Blockchain network and wallet manager must not be null")
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
return WalletStoreModel(
userWalletId = userWallet.walletId,
blockchainNetwork = blockchainNetwork,
walletManager = walletManager,
walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
walletRent = null,
)
}
}
private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
return WalletDataModel(
currency = Currency.Blockchain(
blockchain = blockchain,
derivationPath = derivationPath,
),
status = WalletDataModel.Loading,
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List<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

@ -46,6 +46,7 @@ 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) = {},
@ -56,6 +57,7 @@ object ScanCardProcessor {
onScanStateChange(true)
tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
useBiometricsForAccessCode = useBiometricsForAccessCode,
)
@ -202,7 +204,7 @@ object ScanCardProcessor {
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
onWalletNotCreated()
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)

View file

@ -59,6 +59,8 @@ class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
override val allowsAccessCodeFromRepository: Boolean = false
override fun run(
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,

View file

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

View file

@ -19,16 +19,21 @@ class TwinCardsManager(
card: CardDTO,
assetReader: AssetReader,
) {
private val currentCardId: String = card.cardId
private val firstCardId: String = card.cardId
private var secondCardId: String? = null
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
var secondCardPublicKey: String? = null
private set
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): CompletionResult<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 -> {}
@ -43,6 +48,7 @@ class TwinCardsManager(
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
firstCardId = firstCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
@ -51,6 +57,7 @@ class TwinCardsManager(
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
secondCardId = response.data.cardId
}
is CompletionResult.Failure -> {}
}
@ -59,8 +66,9 @@ class TwinCardsManager(
suspend fun complete(message: Message): Result<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)

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

@ -1,8 +1,70 @@
package com.tangem.tap.domain.userWalletList.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage
import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.DummyUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.*
fun UserWalletsListManager.Companion.provideDummyImplementation(): UserWalletsListManager {
return DummyUserWalletsListManager()
const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
const val USER_WALLETS_BIOMETRIC_KEY_NAME = "user_wallets"
fun UserWalletsListManager.Companion.provideBiometricImplementation(
context: Context,
tangemSdkManager: TangemSdkManager,
): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = context,
storageName = USER_WALLETS_STORAGE_NAME,
),
)
val keysRepository = BiometricUserWalletsKeysRepository(
biometricKeyName = USER_WALLETS_BIOMETRIC_KEY_NAME,
moshi = moshi,
secureStorage = secureStorage,
biometricManager = tangemSdkManager.biometricManager,
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
)
return BiometricUserWalletsListManager(
tangemSdkManager = tangemSdkManager,
keysRepository = keysRepository,
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
)
}

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,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,205 @@
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)
.setWalletManager(walletManager)
.setBlockchainNetwork(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

@ -2,7 +2,29 @@ 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,174 @@
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)
.setWalletManager(walletManager)
.setBlockchainNetwork(blockchainNetwork)
.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)
.setWalletManager(walletManager)
.build(),
)
}
}
internal data class State(
val fiatCurrency: FiatCurrency? = null,
)
}

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

@ -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,6 +2,9 @@ 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
@ -9,44 +12,53 @@ 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.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,14 +77,17 @@ class DetailsMiddleware {
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val scannedCard = result.data
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 (scannedCard.userWalletId.stringValue == currentCardId) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scannedCard))
if (card.userWalletId.stringValue == currentCardId) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
@ -82,8 +97,6 @@ class DetailsMiddleware {
)
}
}
is CompletionResult.Failure -> Unit
}
}
}
}
@ -104,17 +117,22 @@ class DetailsMiddleware {
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
when (val result = tangemSdkManager.resetToFactorySettings(card.cardId)) {
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 -> Unit
@ -174,12 +192,109 @@ class DetailsMiddleware {
}
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

@ -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.*
@ -53,6 +56,9 @@ private fun handlePrepareScreen(
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency,
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = userWalletsListManager.hasSavedUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
)
}
@ -151,12 +157,15 @@ private fun handlePrivacyAction(
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)
}
}

View file

@ -22,6 +22,8 @@ data class DetailsState(
val appCurrency: FiatCurrency = FiatCurrency.Default,
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,
val isBiometricsAvailable: Boolean = false,
val needEnrollBiometrics: Boolean = false,
) : StateType {
// if you do not delegate - the application crashes on startup,

View file

@ -1,23 +1,12 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
@ -26,6 +15,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
@ -64,6 +55,8 @@ private fun AppSettings(
)
}
EnrollBiometricsDialog(dialog = state.enrollBiometricsDialog)
Column(
modifier = modifier
.fillMaxSize(),
@ -84,6 +77,15 @@ private fun AppSettings(
}
}
@Composable
private fun EnrollBiometricsDialog(
modifier: Modifier = Modifier,
dialog: EnrollBiometricsDialog?,
) {
if (dialog == null) return
EnrollBiometricsDialogContent(modifier, dialog)
}
@Composable
private fun AppSettingsElement(
state: AppSettingsScreenState,
@ -224,6 +226,7 @@ fun AppSettingsScreenPreview() {
PrivacySetting.SaveAccessCode to false,
),
onSettingToggled = { _, _ -> },
enrollBiometricsDialog = null,
),
onBackPressed = { },
)

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.features.details.redux.PrivacySetting
data class AppSettingsScreenState(
val settings: Map<PrivacySetting, Boolean>,
val enrollBiometricsDialog: EnrollBiometricsDialog?,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit,
)
)

View file

@ -1,5 +1,7 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
@ -11,12 +13,14 @@ import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) {
private val _uiState = MutableStateFlow(updateState(store.state.detailsState))
val uiState: StateFlow<AppSettingsScreenState> = _uiState
fun updateState(state: DetailsState): AppSettingsScreenState {
return AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to state.saveWallets,
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
),
enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null,
onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) },
)
}
@ -24,4 +28,13 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
private fun createEnrollBiometricsDialog() = EnrollBiometricsDialog(
onCancel = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Cancel)
},
onEnroll = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Enroll)
},
)
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.Immutable
import com.tangem.wallet.R
@Immutable
data class DetailsScreenState(
val elements: List<SettingsElement>,
val tangemLinks: List<SocialNetworkLink>,
@ -13,6 +15,7 @@ data class DetailsScreenState(
val appNameRes: Int = R.string.app_name
}
@Immutable
enum class SettingsElement(
val iconRes: Int,
val titleRes: Int,
@ -30,6 +33,7 @@ enum class SettingsElement(
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy);
}
@Immutable
data class SocialNetworkLink(
val network: SocialNetwork,
val url: String,

View file

@ -36,7 +36,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen
SettingsElement.AppSettings -> if (state.isBiometricsAvailable) it else null
SettingsElement.AppCurrency -> if (state.scanResponse?.card?.isMultiwalletAllowed != true) it else null
SettingsElement.TermsOfUse -> if (state.scanResponse?.card?.isStart2Coin == true) it else null
else -> it
@ -44,7 +44,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
}
return DetailsScreenState(
settings,
elements = settings,
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
appCurrency = state.appCurrency.name,
@ -80,7 +80,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
}
SettingsElement.AppSettings -> {
Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) //TODO: To be available later
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
}
SettingsElement.LinkMoreCards -> {
Analytics.send(Settings.ButtonCreateBackup())

View file

@ -26,7 +26,6 @@ import org.rekotlin.StoreSubscriber
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private var composeView: ComposeView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -38,20 +37,16 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val context = container?.context ?: return null
): View {
store.dispatch(BackupAction.CheckForUnfinishedBackup)
composeView = ComposeView(context).apply {
return ComposeView(inflater.context).apply {
setContent {
AppCompatTheme {
ScreenContent()
}
}
}
return composeView
}
override fun onStart() {
@ -71,7 +66,6 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
override fun onDestroyView() {
super.onDestroyView()
rollbackStatusBarIconsColor()
composeView = null
}
override fun newState(state: HomeState) {

View file

@ -1,51 +1,33 @@
package com.tangem.tap.features.home.redux
import com.tangem.common.core.TangemError
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withIOContext
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.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.postUiDelayBg
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.FragmentShareTransition
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.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
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.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class HomeMiddleware {
companion object {
@ -71,34 +53,13 @@ private fun handleHomeAction(action: Action) {
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> {
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.dispatch(HomeAction.ScanInProgress(false))
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
nextHandler = {
showDisclaimerIfNeed(
scanResponse = scanResponse,
nextHandler = ::onScanSuccess,
)
},
)
},
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
is HomeAction.ReadCard -> readCard()
is HomeAction.GoToShop -> {
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
@ -108,134 +69,44 @@ private fun handleHomeAction(action: Action) {
}
}
/**
* 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 fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
nextHandler: (ScanResponse) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
return
}
fun isTheSamePrimaryCard(card: CardDTO): Boolean {
return backupService.primaryCardId?.let { it == card.cardId } ?: false
}
if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard(scanResponse.card)) {
changeButtonState(ButtonState.ENABLED)
showSaltPayTapVisaLogoCardDialog()
} else {
nextHandler(scanResponse)
}
}
private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanResponse) -> Unit) {
val disclaimerType = DisclaimerType.get(scanResponse)
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
if (disclaimerType.isAccepted()) {
nextHandler((scanResponse))
} else {
changeButtonState(ButtonState.ENABLED)
store.dispatch(
DisclaimerAction.Show {
private fun readCard() = scope.launch {
delay(timeMillis = 200)
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
nextHandler(scanResponse)
},
)
}
}
private fun onScanSuccess(scanResponse: ScanResponse) {
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()) {
} else {
changeButtonState(ButtonState.ENABLED)
}
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onSuccess = { scanResponse ->
scope.launch {
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) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.Update)
navigateTo(AppScreen.OnboardingWallet)
} else {
navigateTo(AppScreen.Wallet)
withIOContext { store.onCardScanned(scanResponse) }
}
if (preferencesStorage.shouldSaveUserWallets) {
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.onCardScanned(scanResponse)
}
is Result.Failure -> {
changeButtonState(ButtonState.ENABLED)
SaltPayExceptionHandler.handle(result.error)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
}
}
.doOnResult {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.onCardScanned(scanResponse)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
} else {
if (scanResponse.card.backupStatus?.isActive == false) {
changeButtonState(ButtonState.ENABLED)
showSaltPayTapVisaLogoCardDialog()
} else {
navigateTo(AppScreen.Wallet, null)
scope.launch { store.onCardScanned(scanResponse) }
}
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen)
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
scope.launch { store.onCardScanned(scanResponse) }
}
}
}
private fun showSaltPayTapVisaLogoCardDialog() {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.saltpay_error_empty_backup_title,
messageId = R.string.saltpay_error_empty_backup_message,
),
},
)
}
private fun onScanFailure(error: TangemError) {
store.dispatch(HomeAction.ScanInProgress(false))
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))
}
}

View file

@ -2,8 +2,18 @@ package com.tangem.tap.features.onboarding
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
@ -39,5 +49,42 @@ class OnboardingHelper {
ProductType.SaltPay -> AppScreen.OnboardingWallet
}
}
fun trySaveWalletAndNavigateToWalletScreen(
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List<String>? = null,
) {
when {
userWalletsListManager.hasSavedUserWallets -> scope.launch {
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchOnMain(SaveWalletAction.Save)
}
tangemSdkManager.canUseBiometry &&
preferencesStorage.shouldShowSaveWallet -> scope.launch {
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
}
else -> scope.launch {
store.onCardScanned(scanResponse)
}
}
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}

View file

@ -14,17 +14,15 @@ import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getTopUpUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
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.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
@ -190,10 +188,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
}
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
store.onCardScanned(scanResponse)
withMainContext { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) }
}
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
}
}

View file

@ -6,13 +6,11 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
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.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
@ -130,10 +128,7 @@ private fun handleOtherCardsAction(action: Action) {
}
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
store.onCardScanned(onboardingManager.scanResponse)
withMainContext { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) }
}
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
}
is OnboardingOtherCardsAction.Confetti.Hide,
is OnboardingOtherCardsAction.SetArtworkUrl,

View file

@ -45,7 +45,6 @@ sealed class TwinCardsAction : Action {
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
}
// for the onboarding
data class SetPairCardId(val cardId: String) : TwinCardsAction()
object TopUp : TwinCardsAction()
@ -53,6 +52,10 @@ sealed class TwinCardsAction : Action {
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
object Done : TwinCardsAction()
data class SaveScannedTwinCardAndNavigateToWallet(
val scanResponse: ScanResponse,
) : TwinCardsAction()
sealed class Balance {
object Update : TwinCardsAction()
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()

View file

@ -12,10 +12,10 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getTopUpUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
@ -25,6 +25,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.preferencesStorage
@ -88,7 +89,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
when (action) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep == TwinCardsStep.WelcomeOnly) return
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return
val scanResponse = getScanResponse()
onboardingManager?.apply {
@ -141,7 +142,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
preferencesStorage.saveTwinsOnboardingShown()
}
@ -289,21 +290,24 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatchOpenUrl(topUpUrl)
}
TwinCardsAction.Done -> {
scope.launch {
val scanResponse = getScanResponse()
store.onCardScanned(scanResponse)
withMainContext {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatch(GlobalAction.Onboarding.Stop)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
val scanResponse = getScanResponse()
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = scanResponse,
backupCardsIds = listOfNotNull(twinCardsState.twinCardsManager?.secondCardPublicKey),
)
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}
is TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet -> {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = action.scanResponse,
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
@ -62,9 +63,19 @@ data class TwinCardsState(
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
enum class TwinCardsStep {
None, WelcomeOnly, Welcome, Warning, CreateFirstWallet, CreateSecondWallet, CreateThirdWallet,
sealed class TwinCardsStep {
object None : TwinCardsStep()
data class WelcomeOnly(
val scanResponse: ScanResponse,
) : TwinCardsStep()
object Welcome : TwinCardsStep()
object Warning : TwinCardsStep()
object CreateFirstWallet : TwinCardsStep()
object CreateSecondWallet : TwinCardsStep()
object CreateThirdWallet : TwinCardsStep()
// for the onboarding
TopUpWallet, Done
object TopUpWallet : TwinCardsStep()
object Done : TwinCardsStep()
}

View file

@ -13,6 +13,7 @@ import coil.load
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.AndroidAssetReader
@ -23,8 +24,6 @@ import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
@ -43,7 +42,7 @@ import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private val mainBinding by lazy { binding.vMain }
private var previousStep = TwinCardsStep.None
private var previousStep: TwinCardsStep = TwinCardsStep.None
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
@ -127,7 +126,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state)
is TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state, state.currentStep.scanResponse)
TwinCardsStep.Welcome -> setupWelcomeState(state)
TwinCardsStep.Warning -> setupWarningState(state)
TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
@ -157,9 +156,9 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
}
private fun setupWelcomeOnlyState(state: TwinCardsState) {
private fun setupWelcomeOnlyState(state: TwinCardsState, scanResponse: ScanResponse) {
setupWelcomeState(state) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatch(TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet(scanResponse))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None))
}
}

View file

@ -21,6 +21,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
@ -69,17 +70,14 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
Analytics.send(Onboarding.Started())
}
}
when {
card == null -> {
// it's possible when found unfinished backup for standard Wallet cards
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> {
when {
// check for unfinished backup for saltPay cards. See more
@ -162,8 +160,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) }
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
)
}
}
is OnboardingWalletAction.ResumeBackup -> {
@ -233,6 +234,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
when (action) {
is BackupAction.StartBackup -> {
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
Analytics.send(Onboarding.Backup.Started())
backupService.discardSavedBackup()
val primaryCard = scanResponse?.primaryCard

View file

@ -5,7 +5,7 @@ import com.tangem.domain.common.ScanResponse
import org.rekotlin.Action
internal sealed interface SaveWalletAction : Action {
data class ProvideAdditionalInfo(
data class ProvideBackupInfo(
val scanResponse: ScanResponse,
val accessCode: String?,
val backupCardsIds: Set<String>?,

View file

@ -7,6 +7,7 @@ import com.tangem.common.flatMap
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.UserWalletBuilder
@ -34,10 +35,10 @@ internal class SaveWalletMiddleware {
private fun handleAction(action: SaveWalletAction, state: SaveWalletState) {
when (action) {
is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state)
is SaveWalletAction.Save.Success -> popBack()
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
is SaveWalletAction.ProvideAdditionalInfo,
is SaveWalletAction.Save.Success,
is SaveWalletAction.ProvideBackupInfo,
is SaveWalletAction.Dismiss,
is SaveWalletAction.CloseError,
is SaveWalletAction.Save.Error,
@ -60,52 +61,56 @@ internal class SaveWalletMiddleware {
}
private fun saveWallet(state: SaveWalletState) {
val scanResponse = state.additionalInfo?.scanResponse
val scanResponse = state.backupInfo?.scanResponse
?: store.state.globalState.scanResponse
?: return
scope.launch {
val userWallet = UserWalletBuilder(scanResponse)
.setBackupCardsIds(backupCardsIds = state.additionalInfo?.backupCardsIds)
.setBackupCardsIds(backupCardsIds = state.backupInfo?.backupCardsIds)
.build()
userWalletsListManager.save(userWallet)
.flatMap {
trySaveAccessCode(
userWallet = userWallet,
additionalInfo = state.additionalInfo,
)
}
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet) }
.doOnFailure { error ->
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
preferencesStorage.shouldSaveUserWallets = true
preferencesStorage.shouldSaveAccessCodes = true
val isSavedWalletSelected =
userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId
if (isSavedWalletSelected) {
store.dispatchOnMain(NavigationAction.PopBackTo())
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.onUserWalletSelected(userWallet)
}
}
}
private fun popBack() {
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private fun saveWalletWasShown() {
preferencesStorage.shouldShowSaveWallet = false
}
private suspend fun trySaveAccessCode(
userWallet: UserWallet,
additionalInfo: SaveWalletState.WalletAdditionalInfo?,
private suspend fun saveAccessCodeIfNeeded(
accessCode: String?,
cardsInWallet: Set<String>,
): CompletionResult<Unit> {
return when {
additionalInfo?.accessCode != null -> {
tangemSdkManager.saveAccessCode(
accessCode = additionalInfo.accessCode,
cardsIds = userWallet.cardsInWallet,
)
accessCode != null -> {
tangemSdkManager.unlockBiometricKeys()
.flatMap {
tangemSdkManager.saveAccessCode(
accessCode = accessCode,
cardsIds = cardsInWallet,
)
}
}
else -> {
CompletionResult.Success(Unit)

View file

@ -12,8 +12,8 @@ internal object SaveWalletReducer {
private fun internalReduce(action: SaveWalletAction, state: SaveWalletState): SaveWalletState {
return when (action) {
is SaveWalletAction.ProvideAdditionalInfo -> state.copy(
additionalInfo = SaveWalletState.WalletAdditionalInfo(
is SaveWalletAction.ProvideBackupInfo -> state.copy(
backupInfo = SaveWalletState.WalletBackupInfo(
scanResponse = action.scanResponse,
accessCode = action.accessCode,
backupCardsIds = action.backupCardsIds,
@ -27,11 +27,11 @@ internal object SaveWalletReducer {
isSaveInProgress = false,
)
is SaveWalletAction.Save.Success -> state.copy(
additionalInfo = null,
backupInfo = null,
isSaveInProgress = false,
)
is SaveWalletAction.Dismiss -> state.copy(
additionalInfo = null,
backupInfo = null,
)
is SaveWalletAction.CloseError -> state.copy(
error = null,

View file

@ -5,12 +5,12 @@ import com.tangem.domain.common.ScanResponse
import org.rekotlin.StateType
data class SaveWalletState(
val additionalInfo: WalletAdditionalInfo? = null,
val backupInfo: WalletBackupInfo? = null,
val isSaveInProgress: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val error: TangemError? = null,
) : StateType {
data class WalletAdditionalInfo(
data class WalletBackupInfo(
val scanResponse: ScanResponse,
val accessCode: String?,
val backupCardsIds: Set<String>?,

View file

@ -6,13 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels

View file

@ -5,26 +5,18 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -36,27 +28,17 @@ import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.util.*
@ -231,13 +213,9 @@ private fun sendTransaction(
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
}
updateWallet(walletManager)
delay(11000) // more than 10000 to avoid throttling
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
}
updateWallet(walletManager)
}
}
is SimpleResult.Failure -> {
@ -327,4 +305,17 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}
private suspend fun updateWallet(walletManager: WalletManager) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
walletCurrenciesManager.update(
userWallet = selectedUserWallet,
blockchainNetwork = blockchainNetwork,
)
} else {
store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))
}
}

View file

@ -30,25 +30,10 @@ import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.CheckClipboard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.PasteAddressPayId
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.SetTruncateHandler
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.TruncateOrRestore
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
import com.tangem.tap.features.send.redux.AmountActionUi.SetMaxAmount
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeIncludeFee
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeSelectedFee
import com.tangem.tap.features.send.redux.FeeActionUi.ToggleControlsVisibility
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.ReleaseSendState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
@ -60,12 +45,7 @@ import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import java.text.DecimalFormatSymbols
/**

View file

@ -7,6 +7,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.services.Result
import com.tangem.domain.DomainWrapped
@ -18,8 +19,7 @@ import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -34,9 +34,6 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -49,14 +46,12 @@ class TokensMiddleware {
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> {
handleAddingCustomToken(action)
}
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
is TokensAction.SetSearchInput -> {
Analytics.send(ManageTokens.TokenSearched())
handleLoadCurrencies(
scanResponse = store.state.globalState.scanResponse,
newSearchInput = action.searchInput
newSearchInput = action.searchInput,
)
}
is TokensAction.LoadMore -> {
@ -106,8 +101,8 @@ class TokensMiddleware {
.filter(supportedBlockchains.toSet())
store.dispatchOnMain(
TokensAction.LoadCurrencies.Success(
currencies, loadCoinsResult.data.moreAvailable
)
currencies, loadCoinsResult.data.moreAvailable,
),
)
}
is Result.Failure -> store.dispatchOnMain(TokensAction.LoadCurrencies.Failure)
@ -116,14 +111,14 @@ class TokensMiddleware {
}
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
val scanResponse = store.state.globalState.scanResponse ?: return
private fun handleSaveChanges(action: TokensAction.SaveChanges) = scope.launch {
val scanResponse = store.state.globalState.scanResponse ?: return@launch
val currentTokens = store.state.tokensState.addedWallets.toNonCustomTokensWithBlockchains(
scanResponse.card.derivationStyle
scanResponse.card.derivationStyle,
)
val currentBlockchains = store.state.tokensState.addedWallets.toNonCustomBlockchains(
scanResponse.card.derivationStyle
scanResponse.card.derivationStyle,
)
val blockchainsToAdd = action.addedBlockchains.filter { !currentBlockchains.contains(it) }
@ -149,7 +144,7 @@ class TokensMiddleware {
) {
store.dispatchDebugErrorNotification("Nothing to save")
store.dispatch(NavigationAction.PopBackTo())
return
return@launch
}
val currencyList = convertToCurrencies(
@ -201,8 +196,9 @@ class TokensMiddleware {
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
scanResponse.card.cardId,
derivations
cardId = scanResponse.card.cardId,
derivations = derivations,
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
when (result) {
is CompletionResult.Success -> {
@ -221,6 +217,7 @@ class TokensMiddleware {
)
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
@ -275,50 +272,78 @@ class TokensMiddleware {
scanResponse: ScanResponse,
currencyList: List<Currency>,
) {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val derivationStyle = scanResponse.card.derivationStyle
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val updatedUserWallet = selectedUserWallet.copy(
scanResponse = scanResponse,
)
val addActions = currencyList.mapIndexedNotNull { index, currency ->
when (currency) {
is Currency.Blockchain -> {
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
val derivationParams = derivationStyle?.let {
when (derivationPath) {
null -> DerivationParams.Default(derivationStyle)
else -> DerivationParams.Custom(derivationPath)
}
scope.launch {
userWalletsListManager.update(updatedUserWallet)
.flatMap {
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = currencyList,
)
}
}
} else {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val derivationStyle = scanResponse.card.derivationStyle
val addActions = currencyList.mapIndexedNotNull { index, currency ->
when (currency) {
is Currency.Blockchain -> {
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
val derivationParams = derivationStyle?.let {
when (derivationPath) {
null -> DerivationParams.Default(derivationStyle)
else -> DerivationParams.Custom(derivationPath)
}
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams,
) ?: return@mapIndexedNotNull null
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork,
walletManager = walletManager,
save = index == currencyList.lastIndex,
)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
WalletAction.MultiWallet.AddToken(
token = currency.token,
blockchain = blockchainNetwork,
save = index == currencyList.lastIndex,
)
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams,
) ?: return@mapIndexedNotNull null
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork,
walletManager = walletManager,
save = index == currencyList.lastIndex,
)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
WalletAction.MultiWallet.AddToken(
token = currency.token,
blockchain = blockchainNetwork,
save = index == currencyList.lastIndex,
)
}
}
addActions.forEach { store.dispatchOnMain(it) }
}
addActions.forEach { store.dispatchOnMain(it) }
}
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isNotEmpty()) store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies))
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
when {
currencies.isEmpty() -> Unit
userWalletsListManager.hasSavedUserWallets -> {
walletCurrenciesManager.removeCurrencies(
userWallet = userWalletsListManager.selectedUserWalletSync!!,
currenciesToRemove = currencies,
)
}
else -> {
store.dispatch(WalletAction.MultiWallet.RemoveWallets(currencies))
}
}
}
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
@ -327,7 +352,7 @@ class TokensMiddleware {
} ?: false
}
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
private fun handleAddingCustomToken() = scope.launch {
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
val scanResponse = store.state.globalState.scanResponse ?: return

View file

@ -1,17 +1,16 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.AddressType
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.UserWallet
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.models.WalletDialog
@ -27,7 +26,8 @@ sealed class WalletAction : Action {
object LoadData : WalletAction() {
object Refresh : WalletAction()
data class Failure(val error: TapError) : WalletAction()
object Success : WalletAction()
data class Failure(val error: TapError?) : WalletAction()
}
data class LoadWallet(
@ -54,20 +54,28 @@ sealed class WalletAction : Action {
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>,
val walletManagers: List<WalletManager>,
) : MultiWallet()
data class AddTokens(
val tokens: List<Token>,
val blockchain: BlockchainNetwork,
) : MultiWallet()
data class AddBlockchain(
val blockchain: BlockchainNetwork,
val walletManager: WalletManager?,
val save: Boolean,
) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>, val save: Boolean,
data class AddToken(
val token: Token,
val blockchain: BlockchainNetwork,
val save: Boolean,
) : MultiWallet()
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork, val save: Boolean) :
MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork, val save: Boolean) : MultiWallet()
data class SaveCurrencies(
val blockchainNetworks: List<BlockchainNetwork>, val card: CardDTO? = null,
) : MultiWallet()
@ -161,6 +169,8 @@ sealed class WalletAction : Action {
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object ChangeWallet : WalletAction()
object ShowSaveWalletIfNeeded : WalletAction()
sealed class TradeCryptoAction : WalletAction() {
object Sell : TradeCryptoAction()
@ -190,4 +200,8 @@ sealed class WalletAction : Action {
object ChooseAppCurrency : AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrency: FiatCurrency) : AppCurrencyAction()
}
data class UserWalletChanged(val userWallet: UserWallet) : WalletAction()
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction()
data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction()
}

View file

@ -14,16 +14,13 @@ import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import org.rekotlin.StateType
import java.math.BigDecimal
import kotlin.properties.ReadOnlyProperty
@ -80,6 +77,9 @@ data class WalletState(
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val hasSavedWallets: Boolean
get() = userWalletsListManager.hasSavedUserWallets
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager
@ -242,11 +242,11 @@ data class WalletState(
totalBalance = TotalBalance(
state = walletsData.findProgressState(),
fiatAmount = walletsData.calculateTotalFiatAmount(),
fiatCurrency = store.state.globalState.appCurrency
)
fiatCurrency = store.state.globalState.appCurrency,
),
)
} else this.copy(
totalBalance = null
totalBalance = null,
)
}

View file

@ -13,9 +13,11 @@ import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.persistence.FiatCurrenciesPrefStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
class AppCurrencyMiddleware(
@ -65,11 +67,19 @@ class AppCurrencyMiddleware(
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency)))
tapWalletManager.rates.clear()
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletAction.LoadFiatRate())
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch {
tapWalletManager.loadData(selectedUserWallet, refresh = true)
}
} else {
tapWalletManager.rates.clear()
store.dispatch(WalletAction.LoadFiatRate())
}
}
private fun List<CurrenciesResponse.Currency>.mapToUiModel(): List<FiatCurrency> {

View file

@ -3,8 +3,10 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
@ -17,6 +19,8 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
@ -26,9 +30,6 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -53,7 +54,7 @@ class MultiWalletMiddleware {
addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddTokens -> {
addTokens(action.tokens, action.blockchain, walletState, globalState, action.save)
addTokens(action.tokens, action.blockchain, walletState, globalState, save = false)
}
is WalletAction.MultiWallet.AddBlockchain -> {
action.walletManager?.let {
@ -121,19 +122,27 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val currency = action.currency
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) scope.launch {
walletCurrenciesManager.removeCurrency(
userWallet = selectedUserWallet,
currencyToRemove = action.currency,
)
} else {
val currency = action.currency
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { it == currency }
if (currency.isBlockchain()) {
currencies
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
}
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { it == currency }
if (currency.isBlockchain()) {
currencies
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
}
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
}
is WalletAction.MultiWallet.RemoveWallets -> {
val card = globalState.scanResponse?.card.guard {
@ -154,11 +163,36 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {
store.dispatch(WalletAction.Scan)
val selectedWallet = userWalletsListManager.selectedUserWalletSync
if (selectedWallet != null) {
scanAndUpdateCard(selectedWallet, walletState)
} else {
store.dispatch(WalletAction.Scan)
}
}
}
}
private fun scanAndUpdateCard(
selectedWallet: UserWallet,
state: WalletState?,
) = scope.launch {
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
cardId = selectedWallet.cardId,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
) { scanResponse ->
val userWallet = selectedWallet.copy(
scanResponse = scanResponse,
)
userWalletsListManager.update(userWallet)
.doOnSuccess {
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
}
}
}
private fun addDummyBalances(walletManagers: List<WalletManager>) {
walletManagers.forEach {
if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) {

View file

@ -11,17 +11,10 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -29,25 +22,20 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.Currency
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.features.wallet.models.getSendableAmounts
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
@ -219,20 +207,34 @@ class WalletMiddleware {
}
}
is WalletAction.LoadData,
is WalletAction.LoadData.Refresh -> {
is WalletAction.LoadData.Refresh,
-> {
val selectedWallet = userWalletsListManager.selectedUserWalletSync
scope.launch {
val scanNoteResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsData.isNotEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
if (selectedWallet != null) {
globalState.tapWalletManager.loadData(
userWallet = selectedWallet,
refresh = action is WalletAction.LoadData.Refresh,
)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
val scanNoteResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsData.isNotEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
}
}
}
is NetworkStateChanged -> {
globalState.scanResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) scope.launch {
globalState.tapWalletManager.loadData(selectedUserWallet)
} else {
globalState.scanResponse?.let { scanNoteResponse ->
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
}
}
}
is WalletAction.CopyAddress -> {
@ -255,7 +257,7 @@ class WalletMiddleware {
if (newAction is PrepareSendScreen && newAction.walletManager == null) {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Home))
FirebaseCrashlytics.getInstance().recordException(
IllegalStateException("PrepareSendScreen: walletManager is null")
IllegalStateException("PrepareSendScreen: walletManager is null"),
)
store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found)
} else {
@ -265,6 +267,75 @@ class WalletMiddleware {
}
}
}
is WalletAction.ShowSaveWalletIfNeeded -> {
showSaveWalletIfNeeded()
}
is WalletAction.ChangeWallet -> {
changeWallet()
}
is WalletAction.UserWalletChanged -> {
scope.launch {
globalState.tapWalletManager.loadData(action.userWallet)
}
}
is WalletAction.WalletStoresChanged -> {
scope.launch(Dispatchers.Default) {
fetchTotalFiatBalance(action.walletStores, walletState)
findMissedDerivations(action.walletStores)
tryToShowAppRatingWarning(action.walletStores)
}
}
is WalletAction.TotalFiatBalanceChanged -> Unit
}
}
private fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>, state: WalletState) {
scope.launch {
val totalFiatBalance = totalFiatBalanceCalculator.calculate(
prevAmount = state.totalBalance?.fiatAmount ?: BigDecimal.ZERO,
walletStores = walletStores,
)
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
}
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
scope.launch {
val missedDerivations = wallStores
.filter { store ->
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
}
.map { it.blockchainNetwork }
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
}
}
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
}
private fun showSaveWalletIfNeeded() {
if (preferencesStorage.shouldShowSaveWallet
&& tangemSdkManager.canUseBiometry
&& store.state.navigationState.backStack.lastOrNull() == AppScreen.Wallet
) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
}
}
private fun changeWallet() {
when {
userWalletsListManager.hasSavedUserWallets -> {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
else -> {
store.dispatch(WalletAction.Scan)
}
}
}

View file

@ -67,11 +67,8 @@ class WarningsMiddleware {
}
}
fun tryToShowAppRatingWarning(wallet: Wallet) {
val nonZeroWalletsCount = wallet.amounts.filter {
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
}.size
if (nonZeroWalletsCount > 0) {
fun tryToShowAppRatingWarning(hasNonZeroWallets: Boolean) {
if (hasNonZeroWallets) {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
@ -79,6 +76,13 @@ class WarningsMiddleware {
}
}
fun tryToShowAppRatingWarning(wallet: Wallet) {
val nonZeroWalletsCount = wallet.amounts.filter {
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
}.size
tryToShowAppRatingWarning(hasNonZeroWallets = nonZeroWalletsCount > 0)
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
globalState?.scanResponse?.let { scanResponse ->
val card = scanResponse.card

View file

@ -9,17 +9,9 @@ import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -174,9 +166,13 @@ class MultiWalletReducer {
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(
showBackupWarning = action.show,
)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(
missingDerivations = action.blockchains,
)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(
state = ProgressState.Loading,
)
}
}

View file

@ -3,34 +3,31 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.isZero
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.getFirstToken
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.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import org.rekotlin.Action
import timber.log.Timber
import java.math.BigDecimal
@ -124,7 +121,10 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
),
)
}
else -> { /* no-op */
else -> {
newState = newState.copy(
state = ProgressState.Error,
)
}
}
}
@ -297,8 +297,8 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
newState = newState.updateWalletData(
selectedWalletData?.copy(
walletAddresses = WalletAddresses(
address,
walletAddresses.list,
selectedAddress = address,
list = walletAddresses.list,
),
),
)
@ -319,13 +319,164 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
}
is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true)
is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false)
else -> { /* no-op */
is WalletAction.UserWalletChanged -> with(action.userWallet) {
val card = scanResponse.card
newState = WalletState(
cardId = card.cardId,
isMultiwalletAllowed = card.isMultiwalletAllowed,
cardImage = Artwork(
artworkId = artworkUrl,
),
isTestnet = card.isTestCard,
state = ProgressState.Loading,
wallets = newState.wallets,
showBackupWarning = card.isMultiwalletAllowed &&
card.settings.isBackupAllowed &&
card.backupStatus == CardDTO.BackupStatus.NoBackup,
)
}
is WalletAction.WalletStoresChanged -> {
newState = newState.copy(
wallets = action.walletStores.mapToReduxModel(newState.isMultiwalletAllowed),
)
}
is WalletAction.TotalFiatBalanceChanged -> {
newState = newState.copy(
totalBalance = action.balance.mapToReduxModel(),
)
}
is WalletAction.LoadData.Success -> {
val selectedCurrency = if (!newState.isMultiwalletAllowed) {
newState.wallets.firstOrNull()
?.walletsData
?.firstOrNull()
?.currency
} else {
newState.selectedCurrency
}
newState = newState.copy(
state = ProgressState.Done,
selectedCurrency = selectedCurrency,
)
}
else -> Unit
}
appStateHolder.walletState = newState
return newState
}
@JvmName("walletStoreModelToReduxModel")
private fun List<WalletStoreModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
return this.map { walletStoreModel ->
with(walletStoreModel) {
WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletStoreModel.walletRent),
)
}
}
}
@JvmName("walletDataModelToReduxModel")
private fun List<WalletDataModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
walletRent: WalletStoreModel.WalletRent?,
): List<WalletData> {
return this.map { walletDataModel ->
with(walletDataModel) {
val amount = status.amount
val amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
val appCurrency = store.state.globalState.appCurrency
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrency.symbol)
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol)
WalletData(
currency = currency,
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
WalletAddresses(
selectedAddress = selectedAddress,
list = walletAddresses,
)
},
existentialDepositString = existentialDeposit?.toPlainString(),
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
pendingTransactions = status.pendingTransactions,
mainButton = WalletMainButton.SendButton(
enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(),
),
walletRent = walletRent?.let {
WalletRent(
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}",
)
},
currencyData = BalanceWidgetData(
status = when (status) {
is WalletDataModel.Loading -> BalanceStatus.Loading
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
is WalletDataModel.Refreshing -> BalanceStatus.Refreshing
is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable,
is WalletDataModel.MissedDerivation,
-> BalanceStatus.Unreachable
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
},
currency = currency.currencyName,
currencySymbol = currency.currencySymbol,
blockchainAmount = status.amount,
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
token = when {
!isMultiWalletAllowed && currency is Currency.Token -> {
TokenData(
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
tokenSymbol = currency.currencySymbol,
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
)
}
else -> null
},
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
?.amountToCreateAccount
?.toString(),
errorMessage = status.errorMessage,
),
)
}
}
}
private fun TotalFiatBalance.mapToReduxModel(): TotalBalance {
return TotalBalance(
state = when (this) {
is TotalFiatBalance.Loading -> ProgressState.Loading
is TotalFiatBalance.Refreshing -> ProgressState.Refreshing
is TotalFiatBalance.Error -> ProgressState.Error
is TotalFiatBalance.Loaded -> ProgressState.Done
},
fiatAmount = amount,
fiatCurrency = store.state.globalState.appCurrency,
)
}
fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? {
if (wallet == null) return null

View file

@ -34,7 +34,7 @@ data class BalanceWidgetData(
)
data class TokenData(
val amountFormatted: String,
val amountFormatted: String?,
val amount: BigDecimal? = null,
val tokenSymbol: String,
val fiatAmountFormatted: String? = null,

View file

@ -1,11 +1,7 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
@ -22,33 +18,26 @@ import com.tangem.tap.common.TestActions
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.DetailsScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.appendIfNotNull
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.images.load
import com.tangem.tap.features.wallet.ui.test.TestWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.tap.walletCurrenciesManager
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
@ -158,15 +147,19 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
Analytics.send(Token.Refreshed())
store.dispatch(
WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList(),
),
),
val blockchainNetwork = BlockchainNetwork(
blockchain = selectedWallet.currency.blockchain,
derivationPath = selectedWallet.currency.derivationPath,
tokens = emptyList(),
)
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) scope.launch {
walletCurrenciesManager.update(selectedUserWallet, blockchainNetwork)
} else {
store.dispatch(
WalletAction.LoadWallet(blockchainNetwork),
)
}
}
}

View file

@ -8,6 +8,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
@ -41,6 +42,7 @@ import com.tangem.tap.features.wallet.ui.wallet.SaltPaySingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
@ -54,6 +56,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private var walletView: WalletView = SingleWalletView()
private val viewModel by viewModels<WalletViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -63,7 +67,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
val popBackTo = if (userWalletsListManager.hasSavedUserWallets) {
userWalletsListManager.lock()
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatch(NavigationAction.PopBackTo(popBackTo))
}
},
)
@ -78,6 +88,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
state.select { it.walletState }
}
walletView.setFragment(this, binding)
viewModel.launch()
}
override fun onStop() {
@ -91,7 +102,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
store.dispatch(WalletAction.ChangeWallet)
}
setupWarningsRecyclerView()
walletView.changeWalletView(this, binding)
@ -162,6 +173,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
store.dispatch(WalletAction.LoadData.Refresh)
}
}
val navigationIconRes = if (state.hasSavedWallets) R.drawable.ic_wallet_24 else R.drawable.ic_tap_card_24
binding.toolbar.setNavigationIcon(navigationIconRes)
}
private fun showWarningsIfPresent(warnings: List<WarningMessage>) {
@ -211,9 +225,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
store.state.globalState.scanResponse?.let { scanNoteResponse ->
store.dispatch(
DetailsAction.PrepareScreen(
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
scanResponse = scanNoteResponse,
wallets = store.state.walletState.walletManagers.map { it.wallet },
cardTou = CardTou(),
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))

Some files were not shown because too many files have changed in this diff Show more