Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-26 15:06:51 +04:00
parent d1798a2c49
commit 6dee2eeed7
112 changed files with 2146 additions and 1562 deletions

View file

@ -4,6 +4,7 @@ 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.extensions.toHexString
import com.tangem.common.flatMap
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.userWalletId
@ -61,7 +62,7 @@ class DetailsMiddleware {
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
is DetailsAction.ShowDisclaimer -> {
val uri = store.state.detailsState.cardTermsOfUseUrl
val uri = state.cardTermsOfUseUrl
if (uri != null) {
store.dispatch(NavigationAction.OpenDocument(uri))
}
@ -71,19 +72,26 @@ class DetailsMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
state.scanResponse?.let {
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
}
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanCard(cardId = state.scanResponse?.card?.cardId)
tangemSdkManager.scanCard(allowRequestAccessCodeFromRepository = true)
.doOnSuccess { card ->
val currentCardId = store.state.globalState.scanResponse?.card
?.userWalletId
?.stringValue
if (card.userWalletId.stringValue == currentCardId) {
val isSameWallet = state.scanResponse?.card?.userWalletId
?.equals(card.userWalletId)
?: false
// !!! Workaround !!!
// TODO: Remove after [REDACTED_JIRA]
val isTwinned = card.wallets.firstOrNull()?.publicKey?.toHexString()
?.equals(state.scanResponse?.secondTwinPublicKey)
?: false
if (isSameWallet || isTwinned) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
} else {
store.dispatchDialogShow(
@ -116,6 +124,7 @@ class DetailsMiddleware {
scope.launch {
tangemSdkManager.resetToFactorySettings(card.cardId)
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
.flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) }
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
@ -255,10 +264,10 @@ class DetailsMiddleware {
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveAccessCodes == enable) return@launch
if (enable) {
saveAccessCodes(state)
if (!state.saveWallets) {
saveCurrentWallet(state)
}
saveAccessCodes(state)
} else {
deleteSavedAccessCodes()
}
@ -334,12 +343,15 @@ class DetailsMiddleware {
useBiometricsForAccessCode = false,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = false,
),
)
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = false,
),
)
}
}
}
}

View file

@ -15,8 +15,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
@ -38,10 +36,6 @@ fun CardSettingsScreen(
modifier: Modifier = Modifier,
) {
val needReadCard = state.cardDetails == null
val backgroundColor by rememberUpdatedState(
newValue = if (needReadCard) TangemTheme.colors.background.primary
else TangemTheme.colors.background.secondary,
)
SettingsScreensScaffold(
content = {
@ -52,7 +46,7 @@ fun CardSettingsScreen(
}
},
titleRes = R.string.card_settings_title,
backgroundColor = backgroundColor,
backgroundColor = TangemTheme.colors.background.secondary,
onBackClick = onBackPressed,
)
}

View file

@ -25,6 +25,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
@ -36,6 +38,10 @@ fun DetailsScreen(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SystemBarsEffect {
setSystemBarsColor(color = TangemColorPalette.Light1)
}
SettingsScreensScaffold(
content = { Content(state = state, modifier = modifier) },
onBackClick = onBackPressed,

View file

@ -9,9 +9,19 @@ sealed class DisclaimerAction : Action {
val type: DisclaimerType,
) : DisclaimerAction()
data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
data class Show(
val onAcceptCallback: VoidCallback? = null,
val onDismissCallback: VoidCallback? = null,
) : DisclaimerAction()
data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction()
internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction()
internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
internal data class SetCallbacks(
val onAcceptCallback: VoidCallback? = null,
val onDismissCallback: VoidCallback? = null,
) : DisclaimerAction()
object OnBackPressed : DisclaimerAction()
}

View file

@ -28,7 +28,7 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
}
is DisclaimerAction.Show -> {
handleUpdateState = state.type.createUpdateState()
store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback))
store.dispatch(DisclaimerAction.SetCallbacks(action.onAcceptCallback, action.onDismissCallback))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
is DisclaimerAction.AcceptDisclaimer -> {
@ -36,7 +36,13 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
handleUpdateState = action.type.createUpdateState()
store.dispatch(NavigationAction.PopBackTo())
state.onAcceptCallback?.invoke()
store.dispatch(DisclaimerAction.SetOnAcceptCallback(null))
store.dispatch(DisclaimerAction.SetCallbacks(null, null))
}
is DisclaimerAction.OnBackPressed -> {
state.onDismissCallback?.invoke()
store.dispatch(DisclaimerAction.SetCallbacks(null, null))
store.dispatch(NavigationAction.PopBackTo())
}
}
}

View file

@ -19,8 +19,9 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
type = action.type,
accepted = action.accepted,
)
is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy(
is DisclaimerAction.SetCallbacks -> disclaimerState.copy(
onAcceptCallback = action.onAcceptCallback,
onDismissCallback = action.onDismissCallback,
)
else -> disclaimerState
}

View file

@ -11,6 +11,7 @@ data class DisclaimerState(
val accepted: Boolean = false,
val type: DisclaimerType = DisclaimerType.Tangem,
val onAcceptCallback: VoidCallback? = null,
val onDismissCallback: VoidCallback? = null,
) : StateType
sealed class DisclaimerType(

View file

@ -4,6 +4,8 @@ import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.BaseFragment
@ -32,6 +34,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.backgroundLightGray)
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.disclaimerState == newState.disclaimerState }
@ -65,4 +68,8 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
webView.loadUrl(state.type.uri.toString())
}
override fun handleOnBackPressed() {
store.dispatch(DisclaimerAction.OnBackPressed)
}
}

View file

@ -79,13 +79,17 @@ private fun readCard() = scope.launch {
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
} else {
changeButtonState(ButtonState.ENABLED)
}
// else { //todo hide this because
// changeButtonState(ButtonState.ENABLED)
// }
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onFailure = {
changeButtonState(ButtonState.ENABLED)
},
onSuccess = { scanResponse ->
scope.launch {
if (preferencesStorage.shouldSaveUserWallets) {
@ -93,21 +97,18 @@ private fun readCard() = scope.launch {
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
store.onCardScanned(scanResponse)
}
.doOnSuccess {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.hasAccessCode,
)
store.onUserWalletSelected(userWallet)
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.onCardScanned(scanResponse)
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -1,20 +1,16 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.doOnSuccess
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.extensions.onUserWalletSelected
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.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
@ -59,17 +55,6 @@ class OnboardingHelper {
backupCardsIds: List<String>? = null,
) {
when {
// When should save user wallets but manager is locked, then unlock manager with card
preferencesStorage.shouldSaveUserWallets &&
userWalletsListManager.isLockedSync -> scope.launch {
val userWallet = UserWalletBuilder(scanResponse).build()
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
userWalletsListManager.unlockWithCard(userWallet)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
}
}
// When should save user wallets, then save card without navigate to save wallet screen
preferencesStorage.shouldSaveUserWallets -> scope.launch {
store.dispatchOnMain(

View file

@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -31,6 +32,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
@ -69,7 +71,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanNoteResponse(response))
CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanResponse(response))
}
}
@ -151,10 +153,15 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
TwinCardsStep.CreateFirstWallet -> {
scope.launch {
userWalletsListManager.delete(
listOf(getScanResponse().card.userWalletId),
)
}
}
TwinCardsStep.None,
TwinCardsStep.Warning,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
-> Unit
@ -294,13 +301,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
scanResponse = scanResponse,
backupCardsIds = listOfNotNull(twinCardsState.twinCardsManager?.secondCardPublicKey),
)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
if (preferencesStorage.shouldSaveUserWallets) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}
}

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.core.ui.fragments.setStatusBarColor
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
@ -97,6 +98,11 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.backgroundWhite)
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
with(containerBinding) {
imvFrontCard.hide()

View file

@ -94,7 +94,6 @@ internal class SaveWalletMiddleware {
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }
.flatMap { userWalletsListManager.selectWallet(userWallet.walletId) }
.doOnFailure { error ->
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
}
@ -105,12 +104,6 @@ internal class SaveWalletMiddleware {
preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet ||
preferencesStorage.shouldSaveAccessCodes
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.hasAccessCode,
)
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))

View file

@ -48,6 +48,7 @@ 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.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
@ -328,12 +329,15 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
}
private suspend fun updateWallet(walletManager: WalletManager) {
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val wallet = walletManager.wallet
walletCurrenciesManager.update(
userWallet = selectedUserWallet,
blockchainNetwork = blockchainNetwork,
currency = Currency.Blockchain(
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
)
} else {
store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager)))

View file

@ -221,7 +221,7 @@ class TokensMiddleware {
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
@ -383,7 +383,7 @@ class TokensMiddleware {
}
}
val addedCurrencies = store.state.walletState.wallets.map { walletStore ->
val addedCurrencies = store.state.walletState.walletsStores.map { walletStore ->
walletStore.walletsData.map { walletData -> walletData.currency }
}.flatten().map {
when (it) {

View file

@ -57,7 +57,7 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
val onSaveChanges = { tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain> ->
Analytics.send(ManageTokens.ButtonSaveChanges())
@ -144,7 +144,6 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc
}
override fun handleOnBackPressed() {
super.handleOnBackPressed()
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.analytics.Analytics
@ -81,6 +82,11 @@ fun CurrenciesScreen(
}
}
val statusBarColor = colorResource(id = R.color.backgroundLightGray)
SystemBarsEffect {
setSystemBarsColor(color = statusBarColor)
}
Scaffold(
floatingActionButton = {
if (tokensState.value.allowToAdd) {

View file

@ -12,11 +12,11 @@ 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.models.TotalBalance
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.wallet.R
import org.rekotlin.Action
@ -36,7 +36,7 @@ sealed class WalletAction : Action {
data class LoadWallet(
val blockchain: BlockchainNetwork? = null,
val walletManager: WalletManager? = null
val walletManager: WalletManager? = null,
) : WalletAction() {
data class Success(val wallet: Wallet, val blockchain: BlockchainNetwork) : WalletAction()
data class NoAccount(
@ -90,7 +90,8 @@ sealed class WalletAction : Action {
val blockchain: BlockchainNetwork,
) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class SelectWallet(val currency: Currency?) : MultiWallet()
data class SetSingleWalletCurrency(val currency: Currency?) : MultiWallet()
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(val currency: Currency) : MultiWallet()
@ -128,7 +129,7 @@ sealed class WalletAction : Action {
val wallet: Wallet? = null, val coinsList: List<Currency>? = null,
) : WalletAction() {
data class Success(
val fiatRates: Map<Currency, BigDecimal?>
val fiatRates: Map<Currency, BigDecimal?>,
) : WalletAction()
object Failure : WalletAction()
@ -163,7 +164,7 @@ sealed class WalletAction : Action {
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
data class RussianCardholdersWarningDialog(
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null,
) : DialogAction()
object Hide : DialogAction()
@ -181,12 +182,13 @@ sealed class WalletAction : Action {
data class Buy(
val checkUserLocation: Boolean = true,
) : TradeCryptoAction()
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
data class SendCrypto(
val currencyId: String,
val amount: String,
val destinationAddress: String,
val transactionId: String
val transactionId: String,
) : TradeCryptoAction()
}
@ -206,6 +208,9 @@ sealed class WalletAction : Action {
}
data class UserWalletChanged(val userWallet: UserWallet) : WalletAction()
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction()
data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction()
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction() {
data class UpdateWalletStores(val reduxWalletStores: List<WalletStore>) : WalletAction()
}
data class TotalFiatBalanceChanged(val balance: TotalBalance) : WalletAction()
}

View file

@ -36,7 +36,7 @@ data class WalletState(
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletStore> = listOf(),
val walletsStores: List<WalletStore> = listOf(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedCurrency: Currency? = null,
@ -50,6 +50,12 @@ data class WalletState(
val walletCardsCount: Int? = null,
) : StateType {
val walletsDataFromStores: List<WalletData>
get() = walletsStores.map { it.walletsData }.flatten()
val selectedWalletData: WalletData?
get() = walletsDataFromStores.firstOrNull { it.currency == selectedCurrency }
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { _, _ ->
@ -63,20 +69,17 @@ data class WalletState(
get() = store.state.globalState.exchangeManager.featureIsSwitchedOn()
val blockchains: List<Blockchain>
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
get() = walletsStores.mapNotNull { it.walletManager?.wallet?.blockchain }
val currencies: List<Currency>
get() = wallets.flatMap { it.walletsData }.map { it.currency }
val walletsData: List<WalletData>
get() = wallets.flatMap { it.walletsData }
get() = walletsStores.flatMap { it.walletsData }.map { it.currency }
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
get() = walletsStores.mapNotNull { it.walletManager }
val primaryWallet: WalletData? = wallets.firstOrNull()?.walletsData?.firstOrNull()
val primaryWallet: WalletData? = walletsStores.firstOrNull()?.walletsData?.firstOrNull()
val primaryWalletManager: WalletManager? = if (wallets.isNotEmpty()) wallets[0].walletManager else null
val primaryWalletManager: WalletManager? = if (walletsStores.isNotEmpty()) walletsStores[0].walletManager else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
@ -91,12 +94,12 @@ data class WalletState(
}
fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? {
return wallets.find { it.blockchainNetwork == blockchain }?.walletManager
return walletsStores.find { it.blockchainNetwork == blockchain }?.walletManager
}
fun getWalletData(blockchain: BlockchainNetwork?): WalletData? {
if (blockchain == null) return null
return walletsData.find {
return walletsDataFromStores.find {
it.currency is Currency.Blockchain &&
it.currency.blockchain == blockchain.blockchain &&
it.currency.derivationPath == blockchain.derivationPath
@ -105,7 +108,7 @@ data class WalletState(
fun getWalletStore(currency: Currency?): WalletStore? {
if (currency == null) return null
return wallets.firstOrNull {
return walletsStores.firstOrNull {
it.blockchainNetwork.derivationPath == currency.derivationPath &&
(it.blockchainNetwork.blockchain == currency.blockchain)
}
@ -120,7 +123,7 @@ data class WalletState(
fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? {
if (blockchainNetwork == null) return null
return wallets.firstOrNull {
return walletsStores.firstOrNull {
it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath &&
(it.blockchainNetwork.blockchain == blockchainNetwork.blockchain)
}
@ -131,10 +134,6 @@ data class WalletState(
return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency }
}
fun getSelectedWalletData(): WalletData? {
return walletsData.find { it.currency == selectedCurrency }
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
@ -142,10 +141,10 @@ data class WalletState(
walletData.currency.token == store.state.walletState.primaryToken)
}
fun replaceWalletInWallets(wallet: WalletStore?): List<WalletStore> {
if (wallet == null) return wallets
fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List<WalletStore> {
if (wallet == null) return walletsStores
var changed = false
val updatedWallets = wallets.map {
val updatedWallets = walletsStores.map {
if (it.blockchainNetwork == wallet.blockchainNetwork) {
changed = true
wallet
@ -153,7 +152,7 @@ data class WalletState(
it
}
}
return if (changed) updatedWallets else wallets + wallet
return if (changed) updatedWallets else walletsStores + wallet
}
fun updateWalletData(walletData: WalletData?): WalletState {
@ -161,26 +160,23 @@ data class WalletState(
return updateWalletsData(listOf(walletData))
}
fun updateWalletsData(
walletsData: List<WalletData>
): WalletState {
fun updateWalletsData(walletsData: List<WalletData>): WalletState {
val walletStores = walletsData
.map { BlockchainNetwork(it.currency.blockchain, it.currency.derivationPath, emptyList()) }
.distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) }
return updateWalletStores(walletStores)
return updateWalletsStores(walletStores)
}
fun updateWalletStore(walletStore: WalletStore?): WalletState {
return copy(wallets = replaceWalletInWallets(walletStore))
return copy(walletsStores = replaceWalletStoreInWalletsStores(walletStore))
.updateTotalBalance()
.updateProgressState()
}
private fun updateWalletStores(walletStores: List<WalletStore>): WalletState {
private fun updateWalletsStores(walletStores: List<WalletStore>): WalletState {
val walletStoresMutable = walletStores.toMutableList()
val updatedWallets = wallets.map { oldWalletStore ->
val updatedWallets = walletsStores.map { oldWalletStore ->
val walletStore = walletStoresMutable.find {
it.blockchainNetwork == oldWalletStore.blockchainNetwork
}
@ -191,20 +187,20 @@ data class WalletState(
oldWalletStore
}
}
return copy(wallets = updatedWallets + walletStoresMutable)
return copy(walletsStores = updatedWallets + walletStoresMutable)
.updateTotalBalance()
.updateProgressState()
}
fun removeWallet(walletData: WalletData?): WalletState {
fun removeWalletData(walletData: WalletData?): WalletState {
if (walletData == null) return this
return when (val currency = walletData.currency) {
is Currency.Blockchain -> {
val walletStores = wallets.filterNot {
val walletStores = walletsStores.filterNot {
it.blockchainNetwork.blockchain == currency.blockchain
&& it.blockchainNetwork.derivationPath == currency.derivationPath
}
copy(wallets = walletStores)
copy(walletsStores = walletStores)
.updateTotalBalance()
.updateProgressState()
}
@ -223,23 +219,8 @@ data class WalletState(
}
}
fun replaceSomeWallets(newWallets: List<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = newWallets.toMutableList()
val updatedWallets = walletsData.map { wallet ->
val newWallet = newWallets
.firstOrNull { wallet.currency == it.currency }
if (newWallet == null) {
wallet
} else {
remainingWallets.remove(newWallet)
newWallet
}
}
return updatedWallets + remainingWallets
}
private fun updateTotalBalance(): WalletState {
val walletsData = this.wallets
val walletsData = this.walletsStores
.flatMap(WalletStore::walletsData)
return if (walletsData.isNotEmpty()) {
@ -256,7 +237,7 @@ data class WalletState(
}
private fun updateProgressState(): WalletState {
val walletsData = this.wallets
val walletsData = this.walletsStores
.flatMap(WalletStore::walletsData)
return if (walletsData.isNotEmpty()) {
@ -264,7 +245,7 @@ data class WalletState(
this.copy(
state = walletsData.findProgressState(),
error = this.error.takeIf { newProgressState == ProgressState.Error }
error = this.error.takeIf { newProgressState == ProgressState.Error },
)
} else this
}
@ -276,6 +257,21 @@ data class WalletState(
}
}
fun List<WalletData>.replaceSomeWalletsData(newWallets: List<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = newWallets.toMutableList()
val updatedWallets = this.map { wallet ->
val newWallet = newWallets
.firstOrNull { wallet.currency == it.currency }
if (newWallet == null) {
wallet
} else {
remainingWallets.remove(newWallet)
newWallet
}
}
return updatedWallets + remainingWallets
}
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
enum class ErrorType { NoInternetConnection }
@ -287,7 +283,7 @@ sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
data class WalletAddresses(
val selectedAddress: AddressData,
val list: List<AddressData>
val list: List<AddressData>,
)
data class AddressData(
@ -301,7 +297,7 @@ data class AddressData(
data class Artwork(
val artworkId: String,
val artwork: Bitmap? = null
val artwork: Bitmap? = null,
) {
companion object {
const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png"
@ -374,7 +370,7 @@ data class WalletData(
}
}
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>){
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) {
if (!currency.isToken()) return
val blockchainFullName = currency.blockchain.fullName
@ -391,7 +387,7 @@ data class WalletData(
data class WalletStore(
val walletManager: WalletManager?,
val blockchainNetwork: BlockchainNetwork,
val walletsData: List<WalletData>
val walletsData: List<WalletData>,
) {
fun updateWallets(walletDataList: List<WalletData>): WalletStore {
val relevantWalletDataList = walletDataList.filter {

View file

@ -0,0 +1,133 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.toFiatRateString
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.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
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.ProgressState
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.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
import com.tangem.tap.store
internal fun List<WalletStoreModel>.mapToReduxModels(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
return this.map { walletStoreModel ->
walletStoreModel.mapToReduxModel(isMultiWalletAllowed)
}
}
internal fun TotalFiatBalance.mapToReduxModel(): TotalBalance {
return TotalBalance(
state = when (this) {
is TotalFiatBalance.Loading -> ProgressState.Loading
is TotalFiatBalance.Error -> ProgressState.Error
is TotalFiatBalance.Loaded -> ProgressState.Done
},
fiatAmount = amount,
fiatCurrency = store.state.globalState.appCurrency,
)
}
internal fun WalletStoreModel.mapToReduxModel(
isMultiWalletAllowed: Boolean,
): WalletStore {
return WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletRent),
)
}
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.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable -> BalanceStatus.Unreachable
is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation
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,
),
)
}
}
}

View file

@ -51,7 +51,7 @@ class MultiWalletMiddleware {
handleAddingWalletManagers(globalState, action.walletManagers)
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
if (action.currency != null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails))
}
}

View file

@ -44,7 +44,7 @@ class TradeCryptoMiddleware {
return
}
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val card = store.state.globalState.scanResponse?.card ?: return
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
@ -85,7 +85,7 @@ class TradeCryptoMiddleware {
}
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
@ -107,7 +107,7 @@ class TradeCryptoMiddleware {
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))

View file

@ -17,7 +17,7 @@ class WalletDialogsMiddleware {
store.dispatchDialogShow(WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.DialogAction.ChooseTradeActionDialog -> {
store.state.walletState.getSelectedWalletData()?.let {
store.state.walletState.selectedWalletData?.let {
Analytics.send(Token.ButtonExchange(AnalyticsParam.CurrencyType.Currency(it.currency)))
}
store.dispatchDialogShow(WalletDialog.ChooseTradeActionDialog)

View file

@ -97,11 +97,7 @@ class WalletMiddleware {
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(
action,
walletState,
globalState
)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is WalletAction.DialogAction -> walletDialogMiddleware.handle(action)
is WalletAction.LoadWallet -> {
@ -129,9 +125,7 @@ class WalletMiddleware {
true,
),
)
store.dispatch(
action = WalletAction.LoadWallet.Success(action.wallet, action.blockchain)
)
store.dispatch(WalletAction.LoadWallet.Success(action.wallet, action.blockchain))
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
@ -150,7 +144,7 @@ class WalletMiddleware {
action.coinsList != null -> action.coinsList
else -> {
if (walletState.isMultiwalletAllowed) {
walletState.walletsData.map { it.currency }
walletState.walletsDataFromStores.map { it.currency }
} else {
val derivationPath = walletState.primaryWallet?.currency?.derivationPath
val primaryBlockchain = walletState.primaryBlockchain
@ -175,7 +169,7 @@ class WalletMiddleware {
Timber.e(
throwable,
"Loading rates failed for [%s]",
currency.currencySymbol
currency.currencySymbol,
)
}
}
@ -188,14 +182,11 @@ class WalletMiddleware {
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(
globalState.scanResponse?.card?.cardId
)
val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse =
globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
val scanResponse = globalState.scanResponse?.copy(card = result.data)
scanResponse?.let { store.onCardScanned(scanResponse) }
}
is CompletionResult.Failure -> {}
}
@ -210,9 +201,7 @@ class WalletMiddleware {
store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
scope.launch {
val response = OnlineCardVerifier().getCardInfo(
action.card.cardId, action.card.cardPublicKey
)
val response = OnlineCardVerifier().getCardInfo(action.card.cardId, action.card.cardPublicKey)
when (response) {
is Result.Success -> {
val actionList = listOf(
@ -237,11 +226,12 @@ class WalletMiddleware {
refresh = action is WalletAction.LoadData.Refresh,
)
} else {
val scanNoteResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsData.isNotEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
val scanResponse = globalState.scanResponse ?: return@launch
if (walletState.walletsDataFromStores.isNotEmpty()) {
globalState.tapWalletManager.reloadData(scanResponse)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
globalState.tapWalletManager.loadData(scanResponse)
}
}
}
@ -249,8 +239,8 @@ class WalletMiddleware {
is NetworkStateChanged -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) scope.launch {
globalState.tapWalletManager.loadData(selectedUserWallet)
if (selectedUserWallet != null) {
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) }
} else {
globalState.scanResponse?.let { scanNoteResponse ->
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
@ -295,6 +285,7 @@ class WalletMiddleware {
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
updateWalletStores(action.walletStores, walletState)
fetchTotalFiatBalance(action.walletStores, walletState)
findMissedDerivations(action.walletStores)
tryToShowAppRatingWarning(action.walletStores)
@ -303,12 +294,29 @@ class WalletMiddleware {
}
}
private fun updateWalletStores(wallStores: List<WalletStoreModel>, state: WalletState) {
scope.launch(Dispatchers.Default) {
if (!state.isMultiwalletAllowed) {
wallStores.firstOrNull()?.walletsData?.firstOrNull()?.let {
store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it.currency))
}
}
val reduxWalletStores = wallStores.mapToReduxModels(state.isMultiwalletAllowed)
store.dispatchOnMain(
WalletAction.WalletStoresChanged.UpdateWalletStores(
reduxWalletStores = reduxWalletStores.toList(),
),
)
}
}
private fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>, state: WalletState) {
scope.launch(Dispatchers.Default) {
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(
prevAmount = state.totalBalance?.fiatAmount,
walletStores = walletStores,
)
)?.mapToReduxModel()
if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
@ -361,7 +369,7 @@ class WalletMiddleware {
}
private fun prepareSendAction(amount: Amount?, state: WalletState?): Action {
val selectedWalletData = state?.getSelectedWalletData()
val selectedWalletData = state?.selectedWalletData
val currency = selectedWalletData?.currency
val walletStore = state?.getWalletStore(currency)
@ -376,36 +384,34 @@ class WalletMiddleware {
if (currency != null && state.isMultiwalletAllowed) {
when (currency) {
is Currency.Blockchain -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData.fiatRate,
walletManager = walletStore.walletManager
walletManager = walletStore.walletManager,
)
}
is Currency.Token -> {
val amountToSend =
amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
prepareSendActionForToken(
amount = amountToSend,
state = state,
selectedWalletData = selectedWalletData,
walletStore = walletStore
walletStore = walletStore,
)
}
}
} else {
if (amounts?.size ?: 0 > 1) {
if ((amounts?.size ?: 0) > 1) {
WalletAction.DialogAction.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData?.fiatRate,
walletManager = walletStore?.walletManager
walletManager = walletStore?.walletManager,
)
}
}
@ -416,7 +422,7 @@ class WalletMiddleware {
amount: Amount,
state: WalletState?,
selectedWalletData: WalletData?,
walletStore: WalletStore?
walletStore: WalletStore?,
): PrepareSendScreen {
val coinRate = state?.getWalletData(walletStore?.blockchainNetwork)?.fiatRate
val tokenRate = if (state?.isMultiwalletAllowed == true) {
@ -431,7 +437,7 @@ class WalletMiddleware {
coinRate = coinRate,
walletManager = walletStore?.walletManager,
tokenAmount = amount,
tokenRate = tokenRate
tokenRate = tokenRate,
)
}
@ -447,7 +453,7 @@ class WalletMiddleware {
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(
PendingTransactionType.Outgoing
PendingTransactionType.Outgoing,
).filterByCoin()
val rentExempt = result.data
@ -465,8 +471,8 @@ class WalletMiddleware {
WalletAction.SetWalletRent(
wallet = walletManager.wallet,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
)
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency"),
),
)
} else {
dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet))

View file

@ -125,7 +125,8 @@ class WarningsMiddleware {
if (scanResponse.isTangemTwins() || scanResponse.isDemoCard()) return null
if (scanResponse.card.isMultiwalletAllowed) {
return if (scanResponse.card.hasSignedHashes()) {
val isBackupForbidden = with(scanResponse.card.settings) { !(isBackupAllowed || isHDWalletAllowed) }
return if (scanResponse.card.hasSignedHashes() && isBackupForbidden) {
WarningMessagesManager.signedHashesMultiWalletWarning()
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
@ -133,8 +134,7 @@ class WarningsMiddleware {
}
}
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator
val validator = store.state.walletState.walletManagers.firstOrNull() as? SignatureCountValidator
return if (validator == null) {
if (scanResponse.card.hasSignedHashes()) {
WarningMessagesManager.alreadySignedHashesWarning()

View file

@ -36,7 +36,7 @@ class MultiWalletReducer {
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
val wallets: List<WalletStore> = action.blockchains.map { blockchain ->
val walletStores: List<WalletStore> = action.blockchains.mapNotNull { blockchain ->
val walletManager = action.walletManagers.firstOrNull {
it.wallet.blockchain == blockchain.blockchain &&
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
@ -52,13 +52,13 @@ class MultiWalletReducer {
status = BalanceStatus.Loading,
currency = blockchain.blockchain.fullName,
currencySymbol = blockchain.blockchain.currency,
token = cardToken
token = cardToken,
),
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(
blockchain.blockchain,
blockchain.derivationPath
blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
)
@ -66,16 +66,19 @@ class MultiWalletReducer {
WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchain,
walletsData = listOf(walletData)
walletsData = listOf(walletData),
)
}
val selectedCurrency = if (!state.isMultiwalletAllowed) {
wallets.firstOrNull()?.walletsData?.firstOrNull()?.currency
walletStores.firstOrNull()?.walletsData?.firstOrNull()?.currency
} else {
state.selectedCurrency
}
state.copy(wallets = wallets, selectedCurrency = selectedCurrency)
state.copy(
walletsStores = walletStores,
selectedCurrency = selectedCurrency,
)
}
is WalletAction.MultiWallet.AddBlockchain -> {
val walletManager = action.walletManager ?: state.getWalletManager(action.blockchain)
@ -91,14 +94,14 @@ class MultiWalletReducer {
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
action.blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
)
val walletStore = WalletStore(
walletManager = walletManager,
blockchainNetwork = action.blockchain,
walletsData = listOf(walletData)
walletsData = listOf(walletData),
)
val newState = state.updateWalletStore(walletStore)
@ -142,31 +145,40 @@ class MultiWalletReducer {
status = tokenBalanceStatus,
amount = action.amount.value,
amountFormatted = action.amount.value?.toFormattedCurrencyString(
action.amount.decimals, action.amount.currencySymbol
action.amount.decimals, action.amount.currencySymbol,
),
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol)
} ?: UNKNOWN_AMOUNT_SIGN,
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO,
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
currency = Currency.Token(
token = action.token,
blockchain = action.blockchain.blockchain,
derivationPath = action.blockchain.derivationPath
derivationPath = action.blockchain.derivationPath,
),
walletRent = findWalletRent(state.getWalletStore(walletManager.wallet))
walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)),
)
state.updateWalletData(newTokenWalletData)
}
is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed)
is WalletAction.MultiWallet.SelectWallet -> state.copy(selectedCurrency = action.walletData?.currency)
is WalletAction.MultiWallet.SetIsMultiwalletAllowed ->
state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed)
is WalletAction.MultiWallet.SelectWallet -> {
state.copy(selectedCurrency = action.currency)
}
is WalletAction.MultiWallet.SetSingleWalletCurrency -> {
state.copy(selectedCurrency = action.currency)
}
is WalletAction.MultiWallet.TryToRemoveWallet -> state
is WalletAction.MultiWallet.RemoveWallet -> state.removeWallet(state.getWalletData(action.currency))
is WalletAction.MultiWallet.RemoveWallet -> {
state.removeWalletData(state.getWalletData(action.currency))
}
is WalletAction.MultiWallet.RemoveWallets -> {
var updatedState = state
action.currencies.forEach { updatedState = updatedState.removeWallet(state.getWalletData(it)) }
action.currencies.forEach { updatedState = updatedState.removeWalletData(state.getWalletData(it)) }
updatedState
}
is WalletAction.MultiWallet.SetPrimaryBlockchain -> state.copy(primaryBlockchain = action.blockchain)
@ -205,10 +217,10 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
currencyData = BalanceWidgetData(
status = BalanceStatus.Loading,
currency = this.name,
currencySymbol = this.symbol
currencySymbol = this.symbol,
),
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = currency
currency = currency,
)
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -106,18 +107,15 @@ class OnWalletLoadedReducer {
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
)
}
val newWallets = tokens + newWalletData
val wallets = walletState.replaceSomeWallets((newWallets))
val newWalletsData = tokens + newWalletData
val walletsData = walletState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData)
return walletState
.updateWalletsData(wallets)
return walletState.updateWalletsData(walletsData)
}
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
// val ratesRespository = store.state.globalState.tapWalletManager.ratesRepository
// val tokenCurrency =
val fiatCurrencyName = store.state.globalState.appCurrency.code
val token = wallet.getFirstToken()
val tokenData = if (token != null) {

View file

@ -36,6 +36,7 @@ private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
BalanceStatus.Unreachable,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain,
BalanceStatus.MissedDerivation,
-> ProgressState.Error
BalanceStatus.Loading,
null,

View file

@ -3,29 +3,38 @@ 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.*
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.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.*
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.replaceSomeWalletsData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import org.rekotlin.Action
@ -63,7 +72,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
is WalletAction.EmptyWallet -> {
newState = newState.copy(
state = ProgressState.Done,
wallets = listOf(
walletsStores = listOf(
WalletStore(
walletManager = null,
blockchainNetwork = BlockchainNetwork(
@ -85,7 +94,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
is WalletAction.LoadData.Failure -> {
when (action.error) {
is TapError.NoInternetConnection -> {
val wallets = newState.wallets
val wallets = newState.walletsStores
.map { store ->
store.copy(
walletsData = store.walletsData.map {
@ -95,20 +104,19 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
),
)
},
)
)
}
newState = newState.copy(
state = ProgressState.Error,
error = ErrorType.NoInternetConnection,
wallets = wallets,
walletsStores = wallets,
)
}
is TapError.UnknownBlockchain -> {
newState = newState.copy(
state = ProgressState.Done,
wallets = listOf(
walletsStores = listOf(
WalletStore(
walletManager = null,
blockchainNetwork = BlockchainNetwork(
@ -153,7 +161,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
BalanceStatus.Loading
}
if (action.blockchain == null) {
val wallets = newState.wallets.map { walletStore ->
val wallets = newState.walletsStores.map { walletStore ->
walletStore.copy(
walletsData = walletStore.walletsData.map { walletData ->
walletData.copy(
@ -170,7 +178,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
}
newState = newState.copy(
state = ProgressState.Loading,
wallets = wallets,
walletsStores = wallets,
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
@ -178,7 +186,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
walletManager.cardTokens.map {
Currency.fromBlockchainNetwork(action.blockchain, it)
}
val newWallets = newState.walletsData.filter { currencies.contains(it.currency) }
val newWalletsData = newState.walletsDataFromStores.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
@ -189,8 +197,8 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
mainButton = WalletMainButton.SendButton(false),
)
}
val wallets = newState.replaceSomeWallets(newWallets)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
val walletsData = newState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(walletsData)
newState = newState.updateWalletStore(walletStore)
}
}
@ -260,7 +268,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
newState = newState.updateWalletsData(updatedWallets)
val progressState =
if (newState.walletsData.any { it.currencyData.status == BalanceStatus.Loading }) {
if (newState.walletsDataFromStores.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
} else {
ProgressState.Done
@ -292,15 +300,13 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
}
is WalletAction.TradeCryptoAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedCurrency)
val walletAddresses =
newState.getWalletData(selectedWalletData?.currency)?.walletAddresses
?: return newState
val walletAddresses = newState.getWalletData(newState.selectedCurrency)?.walletAddresses
?: return newState
val address = walletAddresses.list.firstOrNull { it.type == action.type }
?: return newState
newState = newState.updateWalletData(
selectedWalletData?.copy(
newState.selectedWalletData?.copy(
walletAddresses = WalletAddresses(
selectedAddress = address,
list = walletAddresses.list,
@ -328,37 +334,41 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
val card = scanResponse.card
newState = WalletState(
cardId = card.cardId,
isMultiwalletAllowed = card.isMultiwalletAllowed,
isMultiwalletAllowed = isMultiCurrency,
cardImage = Artwork(
artworkId = artworkUrl,
),
isTestnet = card.isTestCard,
state = ProgressState.Loading,
wallets = newState.wallets,
showBackupWarning = card.isMultiwalletAllowed &&
showBackupWarning = isMultiCurrency &&
card.settings.isBackupAllowed &&
card.backupStatus == CardDTO.BackupStatus.NoBackup,
walletCardsCount = card.findCardsCount(),
totalBalance = if (isMultiCurrency) {
TotalBalance(ProgressState.Loading, BigDecimal.ZERO, store.state.globalState.appCurrency)
} else {
null
},
)
}
is WalletAction.WalletStoresChanged -> {
is WalletAction.WalletStoresChanged.UpdateWalletStores -> {
newState = newState.copy(
wallets = action.walletStores.mapToReduxModel(newState.isMultiwalletAllowed),
walletsStores = action.reduxWalletStores,
)
}
is WalletAction.TotalFiatBalanceChanged -> {
newState = newState.copy(
totalBalance = action.balance.mapToReduxModel(),
totalBalance = action.balance,
)
}
is WalletAction.LoadData.Success -> {
val selectedCurrency = if (!newState.isMultiwalletAllowed) {
newState.wallets.firstOrNull()
newState.walletsStores.firstOrNull()
?.walletsData
?.firstOrNull()
?.currency
} else {
newState.selectedCurrency
newState.selectedWalletData?.currency
}
newState = newState.copy(
@ -377,117 +387,6 @@ private fun CardDTO.findCardsCount(): Int? {
?.takeIf { this.isMultiwalletAllowed }
}
@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

@ -16,7 +16,8 @@ enum class BalanceStatus {
Refreshing,
NoAccount,
EmptyCard,
UnknownBlockchain
UnknownBlockchain,
MissedDerivation,
}
data class BalanceWidgetData(

View file

@ -1,16 +1,23 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.*
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.badoo.mvicore.modelWatcher
import com.tangem.common.doOnResult
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler
@ -18,26 +25,38 @@ 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.*
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.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.*
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.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.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.StoreSubscriber
class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
@ -48,6 +67,42 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
private val walletDataWatcher = modelWatcher<WalletData> {
WalletData::pendingTransactions {
showPendingTransactionsIfPresent(it)
}
WalletData::currency {
handleCurrencyIcon(it)
}
WalletData::currencyData {
setupBalanceData(it)
}
(WalletData::currencyData or WalletData::currency) { walletData ->
setupCurrency(walletData.currencyData, walletData.currency)
setupSwipeRefresh(walletData.currencyData, walletData.currency)
}
}
private val walletStateWatcher = modelWatcher<WalletState> {
(WalletState::selectedCurrency or WalletState::selectedWalletData) { state ->
val selectedWalletData = state.selectedWalletData
if (selectedWalletData != null) {
walletDataWatcher.invoke(selectedWalletData)
setupButtons(selectedWalletData, state.isExchangeServiceFeatureOn)
setupAddressCard(selectedWalletData)
handleWarnings(selectedWalletData)
}
}
(WalletState::selectedCurrency or WalletState::isExchangeServiceFeatureOn) { state ->
if (state.selectedWalletData != null) {
setupButtons(state.selectedWalletData!!, state.isExchangeServiceFeatureOn)
}
}
(WalletState::state or WalletState::error) { state ->
setupNoInternetHandling(state.state, state.error)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -79,6 +134,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
store.unsubscribe(this)
}
override fun onDestroy() {
walletDataWatcher.clear()
walletStateWatcher.clear()
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
@ -129,50 +190,10 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun newState(state: WalletState) {
if (activity == null || view == null) return
if (state.selectedCurrency == null) return
val selectedWallet = state.getSelectedWalletData() ?: return
if (state.selectedWalletData == null) return
walletStateWatcher.invoke(state)
showPendingTransactionsIfPresent(selectedWallet.pendingTransactions)
setupCurrency(selectedWallet.currencyData, selectedWallet.currency)
setupAddressCard(selectedWallet)
setupNoInternetHandling(state)
setupBalanceData(selectedWallet.currencyData)
setupButtons(selectedWallet, state.isExchangeServiceFeatureOn)
handleCurrencyIcon(selectedWallet)
handleWarnings(selectedWallet)
updateViewMeasurements()
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
Analytics.send(Token.Refreshed())
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(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList(),
),
),
)
store.dispatch(WalletAction.LoadFiatRate(coinsList = listOf(selectedWallet.currency)))
}
}
}
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
binding.srlWalletDetails.isRefreshing = false
}
}
private fun updateViewMeasurements() {
@ -203,6 +224,37 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
}
private fun setupSwipeRefresh(currencyData: BalanceWidgetData, currency: Currency) {
binding.srlWalletDetails.setOnRefreshListener {
if (currencyData.status != BalanceStatus.Loading && currencyData.status != BalanceStatus.Refreshing) {
Analytics.send(Token.Refreshed())
lifecycleScope.launch(Dispatchers.Default) {
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) {
walletCurrenciesManager.update(selectedUserWallet, currency)
.doOnResult {
withContext(Dispatchers.Main) {
binding.srlWalletDetails.isRefreshing = false
}
}
} else {
val blockchainNetwork = BlockchainNetwork(
blockchain = currency.blockchain,
derivationPath = currency.derivationPath,
tokens = emptyList(),
)
store.dispatch(WalletAction.LoadWallet(blockchainNetwork))
store.dispatch(WalletAction.LoadFiatRate(coinsList = listOf(currency)))
}
}
}
}
binding.srlWalletDetails.isRefreshing = currencyData.status == BalanceStatus.Loading ||
currencyData.status == BalanceStatus.Refreshing
}
private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) {
lWalletDetails.btnCopy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
@ -231,9 +283,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
rvWarningMessages.show(warningDetails.isNotEmpty())
}
private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) {
private fun handleCurrencyIcon(currency: Currency) = with(binding.lWalletDetails.lBalance) {
ivCurrency.load(
currency = wallet.currency,
currency = currency,
derivationStyle = store.state.globalState
.scanResponse
?.card
@ -281,9 +333,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
}
private fun setupNoInternetHandling(state: WalletState) {
if (state.state == ProgressState.Error) {
if (state.error == ErrorType.NoInternetConnection) {
private fun setupNoInternetHandling(progressState: ProgressState, errorType: ErrorType?) {
if (progressState == ProgressState.Error) {
if (errorType == ErrorType.NoInternetConnection) {
binding.srlWalletDetails.isRefreshing = false
(activity as? SnackbarHandler)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
@ -350,7 +402,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_remove -> {
store.state.walletState.getSelectedWalletData()?.let { walletData ->
store.state.walletState.selectedWalletData?.let { walletData ->
store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData.currency))
true
}

View file

@ -15,6 +15,7 @@ import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import coil.size.Scale
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.Analytics
@ -80,15 +81,18 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
viewModel.launch()
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_secondary)
store.subscribe(this) { state ->
state.select { it.walletState }
}
walletView.setFragment(this, binding)
viewModel.launch()
}
override fun onStop() {
@ -97,6 +101,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
walletView.removeFragment()
}
override fun onDestroy() {
walletView.onDestroyFragment()
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
@ -166,7 +175,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
binding.srlWallet.isRefreshing = state.state == ProgressState.Refreshing
binding.srlWallet.setOnRefreshListener {
if (state.state != ProgressState.Loading ||
if (state.state != ProgressState.Loading &&
state.state != ProgressState.Refreshing
) {
Analytics.send(Portfolio.Refreshed())

View file

@ -62,6 +62,9 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(D
BalanceStatus.Unreachable -> {
root.getString(R.string.wallet_balance_blockchain_unreachable)
}
BalanceStatus.MissedDerivation -> {
root.getString(R.string.wallet_balance_missing_derivation)
}
else -> null
}
@ -95,7 +98,7 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(D
if (wallet.walletAddresses != null) {
cardWallet.setOnClickListener {
Analytics.send(Portfolio.TokenTapped())
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet.currency))
}
} else {
cardWallet.setOnClickListener(null)

View file

@ -156,7 +156,7 @@ private class SolanaRentWarningActionEmitter {
}
private fun getBlockchainNetwork(): BlockchainNetwork {
val currency = store.state.walletState.getSelectedWalletData()!!.currency
val currency = store.state.walletState.selectedWalletData!!.currency
return BlockchainNetwork(currency.blockchain, currency.derivationPath, listOf())
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.modelWatcher
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.Analytics
@ -33,6 +35,45 @@ class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
private val watcher = modelWatcher<WalletState> {
val totalBalanceStrategy: DiffStrategy<WalletState> = { old, new ->
old.cardId != new.cardId ||
old.totalBalance != new.totalBalance ||
old.state != new.state ||
old.walletsStores.size != new.walletsStores.size
}
// !!! Workaround !!!
// Checking state properties instead of state params can reduce application performance,
// but here it is necessary because the WalletStore has an unsuitable equals method
WalletState::walletsDataFromStores {
walletsAdapter.submitList(it)
}
WalletState::loadingUserTokens {
binding?.pbLoadingUserTokens?.show(it)
}
WalletState::walletCardsCount { walletCardsCount ->
binding?.let {
setupWalletCardNumber(it, walletCardsCount)
}
}
WalletState::missingDerivations { missingDerivations ->
binding?.let {
handleRescanWarning(it, missingDerivations.isNotEmpty())
}
}
WalletState::showBackupWarning { showBackupWarnings ->
binding?.let {
handleBackupWarning(it, showBackupWarnings)
}
}
watch({ it }, totalBalanceStrategy) { walletState ->
binding?.let {
handleTotalBalance(it, walletState.totalBalance, walletState.state, walletState.walletsDataFromStores.size)
}
}
}
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
setFragment(fragment, binding)
onViewCreated()
@ -40,6 +81,7 @@ class MultiWalletView : WalletView() {
}
private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) {
watcher.clear()
tvTwinCardNumber.hide()
rvPendingTransaction.hide()
lCardBalance.root.hide()
@ -68,13 +110,7 @@ class MultiWalletView : WalletView() {
val fragment = fragment ?: return
val binding = binding ?: return
handleTotalBalance(binding, state.totalBalance, state.state, state.walletsData.size)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
setupWalletCardNumber(binding, state.walletCardsCount)
walletsAdapter.submitList(state.walletsData)
binding.pbLoadingUserTokens.show(state.loadingUserTokens)
watcher.invoke(state)
binding.btnAddToken.setOnClickListener {
val card = store.state.globalState.scanResponse!!.card
@ -91,7 +127,7 @@ class MultiWalletView : WalletView() {
store.dispatch(TokensAction.AllowToAddTokens(true))
store.dispatch(
TokensAction.SetAddedCurrencies(
wallets = state.walletsData,
wallets = state.walletsDataFromStores,
derivationStyle = card.derivationStyle,
),
)
@ -139,21 +175,26 @@ class MultiWalletView : WalletView() {
walletsCount: Int,
) = with(binding.lCardTotalBalance) {
if (walletsCount == 0) {
root.isVisible = false
if (progressState != ProgressState.Loading) {
root.isVisible = false
}
} else {
if (totalBalance == null) {
veilBalance.animateVisibility(show = true)
root.isVisible = progressState == ProgressState.Loading
if (progressState != ProgressState.Loading) {
root.isVisible = false
}
} else {
root.isVisible = true
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
if (totalBalance.state == ProgressState.Loading) {
veilBalance.veil()
} else {
veilBalance.unVeil()
}
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
@ -218,6 +259,11 @@ class MultiWalletView : WalletView() {
rowButtons.btnSend.text = fragment?.getText(R.string.wallet_button_create_wallet)
rowButtons.onSendClick = { store.dispatch(WalletAction.CreateWallet) }
}
override fun onDestroyFragment() {
super.onDestroyFragment()
watcher.clear()
}
}
private val WalletDetailsButtonsRow.btnBuy: Button

View file

@ -52,4 +52,4 @@ class SaltPaySingleWalletView : WalletView() {
),
).setup()
}
}
}

View file

@ -153,14 +153,12 @@ class SingleWalletView : WalletView() {
(binding.lAddress.root as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
val checkedId =
MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
chipGroupAddressType.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type =
MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain)
val type = MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {

View file

@ -17,6 +17,8 @@ abstract class WalletView {
binding = null
}
open fun onDestroyFragment() {}
abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding)
abstract fun onViewCreated()
abstract fun onNewState(state: WalletState)

View file

@ -1,9 +1,10 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.TotalFiatBalance
data class UserWalletModel(
val id: String,
val id: UserWalletId,
val name: String,
val artworkUrl: String,
val type: Type,

View file

@ -34,20 +34,16 @@ internal sealed interface WalletSelectorAction : Action {
}
data class SelectWallet(
val walletId: String,
) : WalletSelectorAction
data class UnlockWalletWithCard(
val walletId: String,
val userWalletId: UserWalletId,
) : WalletSelectorAction
data class RenameWallet(
val walletId: String,
val userWalletId: UserWalletId,
val newName: String,
) : WalletSelectorAction
data class RemoveWallets(
val walletIdsToRemove: List<String>,
val userWalletsIds: List<UserWalletId>,
) : WalletSelectorAction
object AddWallet : WalletSelectorAction {

View file

@ -19,13 +19,16 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -59,18 +62,17 @@ internal class WalletSelectorMiddleware {
addWallet()
}
is WalletSelectorAction.SelectWallet -> {
selectWallet(action.walletId)
}
is WalletSelectorAction.UnlockWalletWithCard -> {
unlockWalletWithCard(action.walletId)
selectWallet(action.userWalletId)
}
is WalletSelectorAction.RemoveWallets -> {
removeWallets(action.walletIdsToRemove, state)
deleteWallets(action.userWalletsIds, state)
}
is WalletSelectorAction.RenameWallet -> {
renameWallet(action.walletId, action.newName)
renameWallet(action.userWalletId, action.newName)
}
is WalletSelectorAction.ChangeAppCurrency -> {
refreshUserWalletsAmounts()
}
is WalletSelectorAction.ChangeAppCurrency,
is WalletSelectorAction.AddWallet.Success,
is WalletSelectorAction.AddWallet.Error,
is WalletSelectorAction.SelectedWalletChanged,
@ -96,13 +98,17 @@ internal class WalletSelectorMiddleware {
private fun updateBalances(walletStores: Map<UserWalletId, List<WalletStoreModel>>, state: WalletSelectorState) {
walletStores.forEach { (walletId, walletStores) ->
scope.launch {
val updatedWallet = state.wallets
.find { it.id == walletId.stringValue }
?.updateWalletStoresAndCalculateFiatBalance(walletStores)
scope.launch(Dispatchers.Default) {
val foundWallet = state.wallets
.find { it.id == walletId }
if (updatedWallet != null) {
store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet))
if (foundWallet != null) {
val updatedWallet = foundWallet
.updateWalletStoresAndCalculateFiatBalance(walletStores)
if (foundWallet != updatedWallet) {
store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet))
}
}
}
}
@ -135,45 +141,38 @@ internal class WalletSelectorMiddleware {
.doOnSuccess {
Analytics.send(MyWallets.CardWasScanned)
userWalletsListManager.selectWallet(userWallet.walletId)
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
updateAccessCodeRequestPolicy(userWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
}
}
}
private fun selectWallet(id: String) {
private fun selectWallet(userWalletId: UserWalletId) {
scope.launch {
userWalletsListManager.selectWallet(UserWalletId(id))
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
.doOnSuccess { selectedWallet ->
updateAccessCodeRequestPolicy(selectedWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedWallet)
}
}
}
private fun unlockWalletWithCard(id: String) {
scope.launch {
userWalletsListManager.get(UserWalletId(id))
userWalletsListManager.get(userWalletId)
.flatMap { userWallet ->
updateUserWalletWithScannedCard(userWallet)
}
.flatMap { updatedUserWallet ->
unlockUserWallet(updatedUserWallet)
if (userWallet.isLocked) {
unlockUserWalletWithScannedCard(userWallet)
} else {
userWalletsListManager.selectWallet(userWalletId)
}
}
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
.doOnSuccess {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
}
}
}
}
private suspend fun updateUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<UserWallet> {
private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<Unit> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse)
return tangemSdkManager.scanCard(userWallet.cardId)
.map { scannedCard ->
userWallet.copy(
@ -182,23 +181,26 @@ internal class WalletSelectorMiddleware {
),
)
}
}
private suspend fun unlockUserWallet(userWallet: UserWallet): CompletionResult<Unit> {
return userWalletsListManager.unlockWithCard(userWallet)
.doOnSuccess {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
.flatMap { updatedUserWallet ->
userWalletsListManager.save(updatedUserWallet, canOverride = true)
}
.doOnFailure {
tangemSdkManager.changeDisplayedCardIdNumbersCount(
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse,
)
}
}
private fun removeWallets(walletIdsToRemove: List<String>, state: WalletSelectorState) {
private fun deleteWallets(userWalletsIds: List<UserWalletId>, state: WalletSelectorState) {
Analytics.send(MyWallets.Button.DeleteWalletTapped)
scope.launch {
when (walletIdsToRemove.size) {
when (userWalletsIds.size) {
state.wallets.size -> clearUserWallets()
else -> removeUserWallets(walletIdsToRemove, state)
else -> deleteUserWallets(
userWalletsIds = userWalletsIds,
currentSelectedWalletId = state.selectedWalletId,
)
}
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
@ -206,11 +208,11 @@ internal class WalletSelectorMiddleware {
}
}
private fun renameWallet(walletId: String, newName: String) {
private fun renameWallet(userWalletId: UserWalletId, newName: String) {
Analytics.send(MyWallets.Button.EditWalletTapped)
scope.launch {
userWalletsListManager.get(walletId = UserWalletId(walletId))
userWalletsListManager.get(userWalletId)
.map { it.copy(name = newName) }
.flatMap { userWalletsListManager.save(it, canOverride = true) }
.doOnFailure { error ->
@ -219,6 +221,50 @@ internal class WalletSelectorMiddleware {
}
}
private fun refreshUserWalletsAmounts() {
scope.launch {
walletStoresManager.updateAmounts(
userWallets = userWalletsListManager.userWallets.first(),
)
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
}
}
private suspend fun clearUserWallets(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.flatMap { tangemSdkManager.clearSavedUserCodes() }
.doOnSuccess {
// !!! Workaround !!!
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
delay(timeMillis = 280)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
private suspend fun deleteUserWallets(
userWalletsIds: List<UserWalletId>,
currentSelectedWalletId: UserWalletId?,
): CompletionResult<Unit> {
return userWalletsListManager.delete(userWalletsIds)
.flatMap { walletStoresManager.delete(userWalletsIds) }
.flatMap { deleteAccessCodes(userWalletsIds) }
.doOnSuccess {
val selectedWallet = userWalletsListManager.selectedUserWalletSync
when {
selectedWallet == null -> {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
}
currentSelectedWalletId != selectedWallet.walletId -> {
store.onUserWalletSelected(selectedWallet)
}
}
}
}
private suspend inline fun scanCardInternal(
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
) {
@ -236,37 +282,13 @@ internal class WalletSelectorMiddleware {
)
}
private suspend fun clearUserWallets(): CompletionResult<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
private suspend fun deleteAccessCodes(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
val cardsIds = userWalletsListManager.userWallets.firstOrNull().orEmpty()
.asSequence()
.filter { it.walletId in userWalletsIds }
.flatMap { it.cardsInWallet }
private suspend fun removeUserWallets(
walletIdsToRemove: List<String>,
state: WalletSelectorState,
): CompletionResult<Unit> {
val prevSelectedWalletId = state.selectedWalletId
return userWalletsListManager.delete(walletIdsToRemove.map { UserWalletId(it) })
.flatMap { walletStoresManager.delete(walletIdsToRemove) }
.doOnSuccess {
val selectedWallet = userWalletsListManager.selectedUserWalletSync ?: return@doOnSuccess
val isSelectedWalletRemoved = prevSelectedWalletId != selectedWallet.walletId.stringValue
if (isSelectedWalletRemoved) {
updateAccessCodeRequestPolicy(selectedWallet)
store.onUserWalletSelected(selectedWallet)
}
}
}
private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.hasAccessCode,
)
return tangemSdkManager.deleteSavedUserCodes(cardsIds.toSet())
}
private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance(

View file

@ -1,9 +1,7 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.extensions.replaceByOrAdd
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.UserWallet
import org.rekotlin.Action
@ -21,7 +19,7 @@ internal object WalletSelectorReducer {
wallets = action.userWallets.updateWalletsModels(state.wallets),
)
is WalletSelectorAction.SelectedWalletChanged -> state.copy(
selectedWalletId = action.selectedWallet.walletId.stringValue,
selectedWalletId = action.selectedWallet.walletId,
)
is WalletSelectorAction.IsLockedChanged -> state.copy(
isLocked = action.isLocked,
@ -56,7 +54,6 @@ internal object WalletSelectorReducer {
)
is WalletSelectorAction.WalletStoresChanged,
is WalletSelectorAction.SelectWallet,
is WalletSelectorAction.UnlockWalletWithCard,
is WalletSelectorAction.RemoveWallets,
is WalletSelectorAction.RenameWallet,
-> state
@ -66,7 +63,7 @@ internal object WalletSelectorReducer {
private fun List<UserWallet>.updateWalletsModels(prevWallets: List<UserWalletModel>): List<UserWalletModel> {
return this.map { userWallet ->
prevWallets
.find { it.id == userWallet.walletId.stringValue }
.find { it.id == userWallet.walletId }
?.let {
it.copy(
name = userWallet.name,
@ -77,7 +74,7 @@ internal object WalletSelectorReducer {
}
?: with(userWallet) {
UserWalletModel(
id = walletId.stringValue,
id = walletId,
name = name,
artworkUrl = artworkUrl,
type = getType(),
@ -92,12 +89,17 @@ internal object WalletSelectorReducer {
userWalletModel: UserWalletModel,
): List<UserWalletModel> {
return ArrayList(this).apply {
replaceByOrAdd(userWalletModel) { it.id == userWalletModel.id }
val index = indexOfFirst { it.id == userWalletModel.id }
if (index == -1) {
add(userWalletModel)
} else {
this[index] = userWalletModel
}
}
}
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
return if (scanResponse.card.isMultiwalletAllowed) {
return if (isMultiCurrency) {
UserWalletModel.Type.MultiCurrency(
cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active)
?.cardCount?.inc()

View file

@ -1,12 +1,13 @@
package com.tangem.tap.features.walletSelector.redux
import com.tangem.common.core.TangemError
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
data class WalletSelectorState(
val wallets: List<UserWalletModel> = emptyList(),
val selectedWalletId: String? = null,
val selectedWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val fiatCurrency: FiatCurrency = FiatCurrency.Default,
val isCardSavingInProgress: Boolean = false,

View file

@ -31,8 +31,8 @@ internal fun WalletSelectorScreenState.updateWithNewState(
return this.copy(
multiCurrencyWallets = multiCurrencyWallets,
singleCurrencyWallets = singleCurrencyWallets,
selectedWalletId = newState.selectedWalletId,
editingWalletsIds = editingWalletsIds.filter { it in walletsIds },
selectedUserWalletId = newState.selectedWalletId,
editingUserWalletsIds = editingUserWalletsIds.filter { it in walletsIds },
isLocked = newState.isLocked,
showUnlockProgress = newState.isUnlockInProgress,
showAddCardProgress = newState.isCardSavingInProgress,
@ -70,7 +70,7 @@ private fun List<UserWalletModel>.toUiModels(
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
tokenName = type.blockchainName ?: "",
tokenName = type.blockchainName,
)
}
}

View file

@ -7,7 +7,13 @@ 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.*
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.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -39,18 +45,12 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
@OptIn(ExperimentalComposeUiApi::class)
@Composable
override fun ScreenContent(
modifier: Modifier,
state: WalletSelectorScreenState,
) {
override fun ScreenContent(modifier: Modifier, state: WalletSelectorScreenState) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val renameWalletDialog by rememberUpdatedState(newValue = state.renameWalletDialog)
Box(
modifier = modifier
.nestedScroll(connection = rememberNestedScrollInteropConnection()),
) {
Box(modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
WalletSelectorScreenContent(
state = state,
onWalletClick = viewModel::walletClicked,
@ -82,10 +82,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
}
@Composable
private fun RenameWalletDialog(
modifier: Modifier = Modifier,
dialog: RenameWalletDialog?,
) {
private fun RenameWalletDialog(modifier: Modifier = Modifier, dialog: RenameWalletDialog?) {
if (dialog == null) return
RenameWalletDialogContent(modifier, dialog)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.compose.runtime.Immutable
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
@ -10,9 +11,9 @@ import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletI
internal data class WalletSelectorScreenState(
val multiCurrencyWallets: List<MultiCurrencyUserWalletItem> = emptyList(),
val singleCurrencyWallets: List<SingleCurrencyUserWalletItem> = emptyList(),
val selectedWalletId: String? = null,
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val editingWalletsIds: List<String> = listOf(),
val editingUserWalletsIds: List<UserWalletId> = listOf(),
val renameWalletDialog: RenameWalletDialog? = null,
val showAddCardProgress: Boolean = false,
val showUnlockProgress: Boolean = false,

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.walletSelector.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
@ -35,40 +36,38 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.dispatch(WalletSelectorAction.AddWallet)
}
fun walletClicked(walletId: String) = with(state.value) {
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
when {
isLocked -> {
store.dispatch(WalletSelectorAction.UnlockWalletWithCard(walletId))
editingUserWalletsIds.isNotEmpty() && isWalletLocked(userWalletId, this) -> Unit
editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> {
editWallet(userWalletId)
}
editingWalletsIds.isNotEmpty() && !editingWalletsIds.contains(walletId) -> {
editWallet(walletId)
editingUserWalletsIds.isNotEmpty() && editingUserWalletsIds.contains(userWalletId) -> {
cancelWalletEditing(userWalletId)
}
editingWalletsIds.isNotEmpty() && editingWalletsIds.contains(walletId) -> {
cancelWalletEditing(walletId)
}
selectedWalletId != walletId -> {
store.dispatch(WalletSelectorAction.SelectWallet(walletId))
selectedUserWalletId != userWalletId -> {
store.dispatch(WalletSelectorAction.SelectWallet(userWalletId))
}
}
}
fun walletLongClicked(walletId: String) = with(state.value) {
if (!isLocked && editingWalletsIds.isEmpty()) {
editWallet(walletId)
fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) {
if (!isWalletLocked(userWalletId, this) && editingUserWalletsIds.isEmpty()) {
editWallet(userWalletId)
}
}
fun cancelWalletsEditing() {
stateInternal.update { prevState ->
prevState.copy(
editingWalletsIds = emptyList(),
editingUserWalletsIds = emptyList(),
)
}
}
fun renameWallet() = with(state.value) {
if (editingWalletsIds.isNotEmpty() && renameWalletDialog == null) {
val editedWalletId = editingWalletsIds.first()
if (editingUserWalletsIds.isNotEmpty() && renameWalletDialog == null) {
val editedWalletId = editingUserWalletsIds.first()
val editedWallet = (multiCurrencyWallets + singleCurrencyWallets)
.find { it.id == editedWalletId }
@ -80,7 +79,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
stateInternal.update { prevState ->
prevState.copy(
renameWalletDialog = null,
editingWalletsIds = emptyList(),
editingUserWalletsIds = emptyList(),
)
}
},
@ -103,8 +102,8 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
fun deleteWallets() = with(state.value) {
if (editingWalletsIds.isNotEmpty()) {
store.dispatch(WalletSelectorAction.RemoveWallets(walletIdsToRemove = editingWalletsIds))
if (editingUserWalletsIds.isNotEmpty()) {
store.dispatch(WalletSelectorAction.RemoveWallets(userWalletsIds = editingUserWalletsIds))
}
}
@ -122,22 +121,27 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.unsubscribe(this)
}
private fun editWallet(walletId: String) {
private fun editWallet(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
prevState.copy(
editingWalletsIds = prevState.editingWalletsIds + walletId,
editingUserWalletsIds = prevState.editingUserWalletsIds + userWalletId,
)
}
}
private fun cancelWalletEditing(walletId: String) {
private fun cancelWalletEditing(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
prevState.copy(
editingWalletsIds = prevState.editingWalletsIds - walletId,
editingUserWalletsIds = prevState.editingUserWalletsIds - userWalletId,
)
}
}
private fun isWalletLocked(userWalletId: UserWalletId, state: WalletSelectorScreenState): Boolean = with(state) {
multiCurrencyWallets.find { it.id == userWalletId }?.isLocked
?: singleCurrencyWallets.find { it.id == userWalletId }?.isLocked ?: isLocked
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState }

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.walletSelector.ui.components
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
@ -7,7 +8,7 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
internal object MockData {
private val multiCurrencyUserWallet = MultiCurrencyUserWalletItem(
id = "wallet_1",
id = UserWalletId("wallet_1"),
balance = UserWalletItem.Balance(
amount = "6781.05 $",
isLoading = false,
@ -20,7 +21,7 @@ internal object MockData {
)
private val singleCurrencyUserWallet = SingleCurrencyUserWalletItem(
id = "wallet_4",
id = UserWalletId("wallet_4"),
balance = UserWalletItem.Balance(
amount = "6781.05 $",
isLoading = false,
@ -34,11 +35,11 @@ internal object MockData {
val state = WalletSelectorScreenState(
multiCurrencyWallets = listOf(
multiCurrencyUserWallet,
multiCurrencyUserWallet.copy(id = "wallet_2"),
multiCurrencyUserWallet.copy(id = "wallet_3", tokensCount = 2, cardsInWallet = 1),
multiCurrencyUserWallet.copy(id = UserWalletId("wallet_2")),
multiCurrencyUserWallet.copy(id = UserWalletId("wallet_3"), tokensCount = 2, cardsInWallet = 1),
),
singleCurrencyWallets = listOf(singleCurrencyUserWallet),
selectedWalletId = multiCurrencyUserWallet.id,
selectedUserWalletId = multiCurrencyUserWallet.id,
isLocked = false,
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
@ -23,61 +23,84 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButtonIconRight
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
import com.tangem.wallet.R
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletSelectorScreenContent(
modifier: Modifier = Modifier,
state: WalletSelectorScreenState,
onWalletClick: (walletId: String) -> Unit,
onWalletLongClick: (walletId: String) -> Unit,
onWalletClick: (UserWalletId) -> Unit,
onWalletLongClick: (UserWalletId) -> Unit,
onUnlockClick: () -> Unit,
onAddCardClick: () -> Unit,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
Column(modifier = modifier) {
Header(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
editingWalletsIds = state.editingWalletsIds,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
Column(
modifier = Modifier
.verticalScroll(
state = rememberScrollState(),
),
) {
WalletsList(
multiCurrencyWallets = state.multiCurrencyWallets,
singleCurrencyWallets = state.singleCurrencyWallets,
selectedWalletId = state.selectedWalletId,
checkedWalletIds = state.editingWalletsIds,
LazyColumn {
stickyHeader {
Header(
editingWalletsIds = state.editingUserWalletsIds,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
}
item {
WalletsTitle(textResId = R.string.user_wallet_list_multi_header, wallets = state.multiCurrencyWallets)
}
itemsIndexed(
items = state.multiCurrencyWallets,
key = { _, wallet -> wallet.id.stringValue },
) { _, wallet ->
WalletItem(
wallet = wallet,
isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId },
isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds },
onWalletClick = onWalletClick,
onWalletLongClick = onWalletLongClick,
)
SpacerH24()
}
item {
WalletsTitle(textResId = R.string.user_wallet_list_single_header, wallets = state.singleCurrencyWallets)
}
itemsIndexed(
items = state.singleCurrencyWallets,
key = { _, wallet -> wallet.id.stringValue },
) { _, wallet ->
WalletItem(
wallet = wallet,
isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId },
isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds },
onWalletClick = onWalletClick,
onWalletLongClick = onWalletLongClick,
)
}
item {
Footer(
modifier = Modifier
.padding(
top = dimensionResource(id = R.dimen.spacing24),
bottom = dimensionResource(id = R.dimen.spacing16),
)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
isLocked = state.isLocked,
@ -86,25 +109,28 @@ internal fun WalletSelectorScreenContent(
onUnlockClick = onUnlockClick,
onAddCardClick = onAddCardClick,
)
SpacerH16()
}
}
}
@Composable
private fun Header(
modifier: Modifier = Modifier,
editingWalletsIds: List<String>,
editingWalletsIds: List<UserWalletId>,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val editingWalletsSize by rememberUpdatedState(newValue = editingWalletsIds.size)
val hasEditingWallets by remember {
derivedStateOf { editingWalletsSize > 0 }
}
val hasEditingWallets by remember { derivedStateOf { editingWalletsSize > 0 } }
Column(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
) {
Hand()
Box(
modifier = Modifier
@ -114,7 +140,9 @@ private fun Header(
) {
if (hasEditingWallets) {
EditWalletsBar(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
editingWalletsSize = editingWalletsSize,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
@ -132,44 +160,15 @@ private fun Header(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun WalletsList(
modifier: Modifier = Modifier,
multiCurrencyWallets: List<UserWalletItem>,
singleCurrencyWallets: List<UserWalletItem>,
selectedWalletId: String?,
checkedWalletIds: List<String>,
onWalletClick: (walletId: String) -> Unit,
onWalletLongClick: (walletId: String) -> Unit,
) {
Column(modifier = modifier) {
val walletsSection = @Composable { wallets: List<UserWalletItem> ->
wallets.forEachIndexed { index, wallet ->
if (index == 0) {
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = wallet.headerText.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
WalletItem(
modifier = Modifier
.combinedClickable(
onClick = { onWalletClick(wallet.id) },
onLongClick = { onWalletLongClick(wallet.id) },
)
.padding(all = TangemTheme.dimens.spacing16),
wallet = wallet,
isSelected = wallet.id == selectedWalletId,
isChecked = wallet.id in checkedWalletIds,
)
}
}
walletsSection(multiCurrencyWallets)
walletsSection(singleCurrencyWallets)
private fun WalletsTitle(@StringRes textResId: Int, wallets: List<*>) {
if (wallets.isNotEmpty()) {
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = stringResource(id = textResId),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@ -278,11 +277,6 @@ private fun WalletSelectorScreenContentSample(
.background(color = TangemTheme.colors.background.primary),
) {
WalletSelectorScreenContent(
modifier = Modifier
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
state = MockData.state.copy(isLocked = true),
onWalletClick = { /* no-op */ },
onWalletLongClick = { /* no-op */ },
@ -320,13 +314,8 @@ private fun WalletSelectorScreenContent_EditWallets_Sample(
.background(TangemTheme.colors.background.primary),
) {
WalletSelectorScreenContent(
modifier = Modifier
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
state = MockData.state
.copy(editingWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)),
.copy(editingUserWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)),
onWalletClick = { /* no-op */ },
onWalletLongClick = { /* no-op */ },
onUnlockClick = { /* no-op */ },

View file

@ -1,7 +1,9 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
@ -26,6 +28,7 @@ import com.tangem.core.ui.components.SpacerH2
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
@ -33,15 +36,23 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
import com.tangem.wallet.R
import com.valentinilk.shimmer.shimmer
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletItem(
modifier: Modifier = Modifier,
wallet: UserWalletItem,
isSelected: Boolean,
isChecked: Boolean,
onWalletClick: (UserWalletId) -> Unit,
onWalletLongClick: (UserWalletId) -> Unit,
) {
Row(
modifier = modifier,
modifier = Modifier
.combinedClickable(
onClick = { onWalletClick(wallet.id) },
onLongClick = { onWalletLongClick(wallet.id) },
)
.height(72.dp)
.padding(all = TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
WalletCardImage(

View file

@ -1,21 +1,14 @@
package com.tangem.tap.features.walletSelector.ui.model
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
import com.tangem.domain.common.util.UserWalletId
internal sealed interface UserWalletItem {
val id: String
val id: UserWalletId
val name: String
val imageUrl: String
val balance: Balance
val isLocked: Boolean
val headerText: TextReference
get() = when (this) {
is MultiCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_multi_header)
is SingleCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_single_header)
}
data class Balance(
val amount: String,
val isLoading: Boolean,
@ -23,7 +16,7 @@ internal sealed interface UserWalletItem {
}
internal data class MultiCurrencyUserWalletItem(
override val id: String,
override val id: UserWalletId,
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,
@ -33,7 +26,7 @@ internal data class MultiCurrencyUserWalletItem(
) : UserWalletItem
internal data class SingleCurrencyUserWalletItem(
override val id: String,
override val id: UserWalletId,
override val name: String,
override val imageUrl: String,
override val balance: UserWalletItem.Balance,

View file

@ -59,11 +59,6 @@ internal class WelcomeMiddleware {
}
.doOnSuccess { selectedUserWallet ->
if (selectedUserWallet != null) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
selectedUserWallet.hasAccessCode,
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(selectedUserWallet)
@ -78,8 +73,7 @@ internal class WelcomeMiddleware {
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false)
userWalletsListManager.unlockWithCard(userWallet)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
@ -100,6 +94,9 @@ internal class WelcomeMiddleware {
private suspend inline fun scanCardInternal(
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }