Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-01 14:40:31 +03:00
parent a0f1a6591c
commit e993dc0647
119 changed files with 4449 additions and 720 deletions

View file

@ -45,8 +45,20 @@ sealed class DetailsAction : Action {
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) :
AppSettings()
data class SwitchPrivacySetting(
val enable: Boolean,
val setting: PrivacySetting,
) : AppSettings() {
data class Success(
val enable: Boolean,
val setting: PrivacySetting,
) : AppSettings()
}
object EnrollBiometrics : AppSettings() {
object Enroll : AppSettings()
object Cancel : AppSettings()
}
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -2,6 +2,9 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.userWalletId
import com.tangem.tap.common.analytics.Analytics
@ -9,44 +12,53 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, state ->
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
handleAction(state, action)
if (!DemoHelper.tryHandle(stateProvider, action)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(detailsState, action)
}
}
next(action)
}
}
}
private fun handleAction(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
is DetailsAction.ShowDisclaimer -> {
val uri = store.state.detailsState.cardTermsOfUseUrl
if (uri != null) {
@ -65,14 +77,17 @@ class DetailsMiddleware {
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val scannedCard = result.data
tangemSdkManager.scanCard(
cardId = state.scanResponse?.card?.cardId,
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
state.scanResponse?.card?.isAccessCodeSet == true,
)
.doOnSuccess { card ->
val currentCardId = store.state.globalState.scanResponse?.card
?.userWalletId
?.stringValue
if (scannedCard.userWalletId.stringValue == currentCardId) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scannedCard))
if (card.userWalletId.stringValue == currentCardId) {
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
@ -82,8 +97,6 @@ class DetailsMiddleware {
)
}
}
is CompletionResult.Failure -> Unit
}
}
}
}
@ -104,17 +117,22 @@ class DetailsMiddleware {
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
when (val result = tangemSdkManager.resetToFactorySettings(card.cardId)) {
is CompletionResult.Success -> {
tangemSdkManager.resetToFactorySettings(card.cardId)
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
val screen = if (userWalletsListManager.hasSavedUserWallets) {
AppScreen.Welcome
} else {
AppScreen.Home
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) }
.doOnFailure { error ->
(error as? TangemSdkError)?.let { sdkError ->
Analytics.send(Settings.CardSettings.FactoryResetFinished(sdkError))
}
}
}
}
}
else -> Unit
@ -174,12 +192,109 @@ class DetailsMiddleware {
}
class ManagePrivacyMiddleware {
fun handle(action: DetailsAction.AppSettings) {
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
// TODO()
if (tangemSdkManager.canEnrollBiometrics) {
store.dispatch(DetailsAction.AppSettings.EnrollBiometrics)
}
when (action.setting) {
PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
}
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> Unit
is DetailsAction.AppSettings.EnrollBiometrics -> Unit
is DetailsAction.AppSettings.EnrollBiometrics.Enroll -> enrollBiometrics()
is DetailsAction.AppSettings.EnrollBiometrics.Cancel -> Unit
}
}
private fun enrollBiometrics() {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
}
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveWallets == enable) return@launch
if (enable) {
saveCurrentWallet()
} else {
deleteSavedWallets()
if (state.saveAccessCodes) {
deleteSavedAccessCodes()
}
}
}
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveAccessCodes == enable) return@launch
if (enable) {
if (!state.saveWallets) {
saveCurrentWallet()
}
saveAccessCodes()
} else {
deleteSavedAccessCodes()
}
}
private suspend fun saveCurrentWallet() {
val scanResponse = store.state.detailsState.scanResponse ?: return
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Wallet saving failed")
}
.doOnSuccess {
preferencesStorage.shouldShowSaveWallet = false
preferencesStorage.shouldSaveUserWallets = true
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = true,
),
)
store.onUserWalletSelected(userWallet)
}
}
private suspend fun deleteSavedWallets() {
userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
preferencesStorage.shouldSaveUserWallets = false
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveWallets,
enable = false,
),
)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
private fun saveAccessCodes() {
preferencesStorage.shouldSaveAccessCodes = true
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = true,
),
)
}
private suspend fun deleteSavedAccessCodes() {
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
preferencesStorage.shouldSaveAccessCodes = false
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
enable = false,
),
)
}
}
}
}

View file

@ -8,7 +8,10 @@ import com.tangem.domain.common.isTangemTwin
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import org.rekotlin.Action
import java.util.*
@ -53,6 +56,9 @@ private fun handlePrepareScreen(
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency,
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = userWalletsListManager.hasSavedUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
)
}
@ -151,12 +157,15 @@ private fun handlePrivacyAction(
state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) {
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
is DetailsAction.AppSettings.EnrollBiometrics -> state.copy(needEnrollBiometrics = true)
is DetailsAction.AppSettings.EnrollBiometrics.Enroll,
is DetailsAction.AppSettings.EnrollBiometrics.Cancel,
-> state.copy(needEnrollBiometrics = false)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,51 +1,33 @@
package com.tangem.tap.features.home.redux
import com.tangem.common.core.TangemError
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerType
import com.tangem.tap.features.disclaimer.redux.isAccepted
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class HomeMiddleware {
companion object {
@ -71,34 +53,13 @@ private fun handleHomeAction(action: Action) {
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> {
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.dispatch(HomeAction.ScanInProgress(false))
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
nextHandler = {
showDisclaimerIfNeed(
scanResponse = scanResponse,
nextHandler = ::onScanSuccess,
)
},
)
},
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
is HomeAction.ReadCard -> readCard()
is HomeAction.GoToShop -> {
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
@ -108,134 +69,44 @@ private fun handleHomeAction(action: Action) {
}
}
/**
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
* see BackupAction.CheckForUnfinishedBackup
* If user touches card other than Visa SaltPay - show dialog and block next processing
*/
private fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
nextHandler: (ScanResponse) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
return
}
fun isTheSamePrimaryCard(card: CardDTO): Boolean {
return backupService.primaryCardId?.let { it == card.cardId } ?: false
}
if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard(scanResponse.card)) {
changeButtonState(ButtonState.ENABLED)
showSaltPayTapVisaLogoCardDialog()
} else {
nextHandler(scanResponse)
}
}
private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanResponse) -> Unit) {
val disclaimerType = DisclaimerType.get(scanResponse)
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
if (disclaimerType.isAccepted()) {
nextHandler((scanResponse))
} else {
changeButtonState(ButtonState.ENABLED)
store.dispatch(
DisclaimerAction.Show {
private fun readCard() = scope.launch {
delay(timeMillis = 200)
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
nextHandler(scanResponse)
},
)
}
}
private fun onScanSuccess(scanResponse: ScanResponse) {
Analytics.send(IntroductionProcess.CardWasScanned())
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.isSaltPay()) {
if (scanResponse.isSaltPayVisa()) {
} else {
changeButtonState(ButtonState.ENABLED)
}
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onSuccess = { scanResponse ->
scope.launch {
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
delay(500)
withMainContext {
when (result) {
is Result.Success -> {
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.Update)
navigateTo(AppScreen.OnboardingWallet)
} else {
navigateTo(AppScreen.Wallet)
withIOContext { store.onCardScanned(scanResponse) }
}
if (preferencesStorage.shouldSaveUserWallets) {
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.onCardScanned(scanResponse)
}
is Result.Failure -> {
changeButtonState(ButtonState.ENABLED)
SaltPayExceptionHandler.handle(result.error)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
}
}
.doOnResult {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.onCardScanned(scanResponse)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
} else {
if (scanResponse.card.backupStatus?.isActive == false) {
changeButtonState(ButtonState.ENABLED)
showSaltPayTapVisaLogoCardDialog()
} else {
navigateTo(AppScreen.Wallet, null)
scope.launch { store.onCardScanned(scanResponse) }
}
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen)
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
scope.launch { store.onCardScanned(scanResponse) }
}
}
}
private fun showSaltPayTapVisaLogoCardDialog() {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.saltpay_error_empty_backup_title,
messageId = R.string.saltpay_error_empty_backup_message,
),
},
)
}
private fun onScanFailure(error: TangemError) {
store.dispatch(HomeAction.ScanInProgress(false))
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.wallet.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
internal class WalletViewModel : ViewModel() {
fun launch() {
bootstrapSelectedWalletStoresChanges()
bootstrapShowSaveWalletIfNeeded()
}
private fun bootstrapSelectedWalletStoresChanges() {
userWalletsListManager.selectedUserWallet
.flatMapLatest { selectedWallet ->
walletStoresManager.get(selectedWallet.walletId)
}
.onEach { walletStores ->
store.dispatch(WalletAction.WalletStoresChanged(walletStores))
}
.launchIn(viewModelScope)
}
private fun bootstrapShowSaveWalletIfNeeded() {
viewModelScope.launch {
delay(timeMillis = 1_800)
store.dispatchOnMain(WalletAction.ShowSaveWalletIfNeeded)
}
}
}

View file

@ -6,8 +6,6 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Portfolio
@ -28,15 +26,6 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(D
return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0
}
fun submitList(
list: List<WalletData>,
primaryBlockchain: Blockchain?,
primaryToken: Token? = null,
) {
// We used this method to sort the list of currencies. Sorting is disabled for now.
super.submitList(list)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder {
val layout = ItemCurrencyWalletBinding.inflate(
LayoutInflater.from(parent.context), parent, false,
@ -73,15 +62,7 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(D
BalanceStatus.Unreachable -> {
root.getString(R.string.wallet_balance_blockchain_unreachable)
}
BalanceStatus.NoAccount,
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain,
BalanceStatus.Loading,
BalanceStatus.Refreshing,
null,
-> null
else -> null
}
if (status == null || status == BalanceStatus.Loading) {
@ -101,8 +82,8 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(D
)
lContent.tvCurrency.text = wallet.currencyData.currency
lContent.tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted
lContent.tvAmount.text = wallet.currencyData.amountFormatted
lContent.tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted ?: ""
lContent.tvAmount.text = wallet.currencyData.amountFormatted ?: ""
lContent.tvStatus.isVisible = statusMessage != null
lContent.tvStatus.text = statusMessage

View file

@ -70,5 +70,7 @@ fun CurrencyIconView.load(
blockchain = currency.blockchain,
getLocalImage = true,
).load()
} else {
isBlockchainBadgeVisible = false
}
}

View file

@ -9,11 +9,7 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.CurrenciesRepository
@ -73,16 +69,17 @@ class MultiWalletView : WalletView() {
walletsAdapter.setHasStableIds(true)
binding?.rvMultiwallet?.layoutManager = LinearLayoutManager(fragment.requireContext())
binding?.rvMultiwallet?.adapter = walletsAdapter
binding?.rvMultiwallet?.itemAnimator = null
}
override fun onNewState(state: WalletState) {
val fragment = fragment ?: return
val binding = binding ?: return
handleTotalBalance(binding, state.totalBalance)
handleTotalBalance(binding, state.totalBalance, state.state)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
walletsAdapter.submitList(state.walletsData)
binding.pbLoadingUserTokens.show(state.loadingUserTokens)
@ -135,11 +132,14 @@ class MultiWalletView : WalletView() {
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,
progressState: ProgressState,
) = with(binding.lCardTotalBalance) {
root.isVisible = totalBalance != null
if (totalBalance != null) {
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing) return@with
if (totalBalance.state == ProgressState.Refreshing ||
progressState == ProgressState.Refreshing
) return@with
if (totalBalance.state == ProgressState.Loading) {
veilBalance.veil()

View file

@ -3,12 +3,7 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.*
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
@ -38,9 +33,12 @@ class SingleWalletView : WalletView() {
btnAddToken.hide()
rvPendingTransaction.hide()
pbLoadingUserTokens.hide()
lCardTotalBalance.root.hide()
lSingleWalletBalance.root.hide()
lWalletRescanWarning.root.hide()
lCardBalance.root.show()
lAddress.root.show()
lSingleWalletBalance.root.hide()
rowButtons.show()
}
override fun onViewCreated() {

View file

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